diff options
author | Bob Ippolito <bob@redivi.com> | 2021-09-22 21:20:01 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-22 21:20:01 -0700 |
commit | dc336dea43ed57a14445339eade738845adaab2e (patch) | |
tree | 2924c95456f61edec8315202a7944ec814a6362a | |
parent | 3e146d58daa9b17d4ff9d32a185d5d9bbaf45b16 (diff) | |
parent | 5c06ab1418daa853575106073cd67ed0fec6ce99 (diff) | |
download | xattr-dc336dea43ed57a14445339eade738845adaab2e.tar.gz |
Merge pull request #93 from tipabu/non-utf8-values
Various dump-related fixes
-rw-r--r-- | xattr/tests/test_tool.py | 117 | ||||
-rwxr-xr-x | xattr/tool.py | 62 |
2 files changed, 157 insertions, 22 deletions
diff --git a/xattr/tests/test_tool.py b/xattr/tests/test_tool.py new file mode 100644 index 0000000..644364f --- /dev/null +++ b/xattr/tests/test_tool.py @@ -0,0 +1,117 @@ +import contextlib +import errno +import io +import os +import shutil +import sys +import tempfile +import unittest +import uuid + +import xattr +import xattr.tool + + +class TestTool(unittest.TestCase): + def setUp(self): + self.test_dir = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.test_dir) + + orig_stdout = sys.stdout + + def unpatch_stdout(sys=sys, orig_stdout=orig_stdout): + sys.stdout = orig_stdout + + self.addCleanup(unpatch_stdout) + sys.stdout = self.mock_stdout = io.StringIO() + + def getoutput(self): + value = self.mock_stdout.getvalue() + self.mock_stdout.seek(0) + self.mock_stdout.truncate(0) + return value + + @contextlib.contextmanager + def temp_file(self): + test_file = os.path.join(self.test_dir, str(uuid.uuid4())) + fd = os.open(test_file, os.O_CREAT | os.O_WRONLY) + try: + yield test_file, fd + finally: + os.close(fd) + + def set_xattr(self, fd, name, value): + try: + xattr.setxattr(fd, name, value) + except OSError as e: + if e.errno == errno.ENOTSUP: + raise unittest.SkipTest('xattrs are not supported') + raise + + def test_utf8(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test-utf8', + u'\N{SNOWMAN}'.encode('utf8')) + self.set_xattr(fd, 'user.test-utf8-and-nul', + u'\N{SNOWMAN}\0'.encode('utf8')) + + xattr.tool.main(['prog', '-p', 'user.test-utf8', file_path]) + self.assertEqual(u'\N{SNOWMAN}\n', self.getoutput()) + + xattr.tool.main(['prog', '-p', 'user.test-utf8-and-nul', file_path]) + self.assertEqual(u''' +0000 E2 98 83 00 .... + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + output = self.getoutput() + self.assertIn(u'user.test-utf8: \N{SNOWMAN}\n', output) + self.assertIn(u''' +user.test-utf8-and-nul: +0000 E2 98 83 00 .... + +'''.lstrip(), output) + + def test_non_utf8(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test-not-utf8', b'cannot\xffdecode') + + xattr.tool.main(['prog', '-p', 'user.test-not-utf8', file_path]) + self.assertEqual(u''' +0000 63 61 6E 6E 6F 74 FF 64 65 63 6F 64 65 cannot.decode + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + self.assertIn(u''' +user.test-not-utf8: +0000 63 61 6E 6E 6F 74 FF 64 65 63 6F 64 65 cannot.decode + +'''.lstrip(), self.getoutput()) + + def test_nul(self): + with self.temp_file() as (file_path, fd): + self.set_xattr(fd, 'user.test', b'foo\0bar') + self.set_xattr(fd, 'user.test-long', + b'some rather long value with\0nul\0chars in it') + + xattr.tool.main(['prog', '-p', 'user.test', file_path]) + self.assertEqual(u''' +0000 66 6F 6F 00 62 61 72 foo.bar + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-p', 'user.test-long', file_path]) + self.assertEqual(u''' +0000 73 6F 6D 65 20 72 61 74 68 65 72 20 6C 6F 6E 67 some rather long +0010 20 76 61 6C 75 65 20 77 69 74 68 00 6E 75 6C 00 value with.nul. +0020 63 68 61 72 73 20 69 6E 20 69 74 chars in it + +'''.lstrip(), self.getoutput()) + + xattr.tool.main(['prog', '-l', file_path]) + self.assertIn(u''' +0000 66 6F 6F 00 62 61 72 foo.bar + +'''.lstrip(), self.getoutput()) diff --git a/xattr/tool.py b/xattr/tool.py index a7cfb47..0623560 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -62,12 +62,19 @@ def usage(e=None): print(" -z: compress or decompress (if compressed) attribute value in zip format") if e: - sys.exit(64) + return 64 else: - sys.exit(0) + return 0 -_FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) +if sys.version_info < (3,): + ascii = repr + uchr = unichr +else: + uchr = chr + + +_FILTER = u''.join([(len(ascii(chr(x))) == 3) and uchr(x) or u'.' for x in range(256)]) def _dump(src, length=16): @@ -80,11 +87,11 @@ def _dump(src, length=16): return ''.join(result) -def main(): +def main(argv): try: - (optargs, args) = getopt.getopt(sys.argv[1:], "hlpwdzs", ["help"]) + (optargs, args) = getopt.getopt(argv[1:], "hlpwdzs", ["help"]) except getopt.GetoptError as e: - usage(e) + return usage(e) attr_name = None long_format = False @@ -98,7 +105,7 @@ def main(): for opt, arg in optargs: if opt in ("-h", "--help"): - usage() + return usage() elif opt == "-l": long_format = True elif opt == "-s": @@ -106,31 +113,31 @@ def main(): elif opt == "-p": read = True if write or delete: - usage("-p not allowed with -w or -d") + return usage("-p not allowed with -w or -d") elif opt == "-w": write = True if read or delete: - usage("-w not allowed with -p or -d") + return usage("-w not allowed with -p or -d") elif opt == "-d": delete = True if read or write: - usage("-d not allowed with -p or -w") + return usage("-d not allowed with -p or -w") elif opt == "-z": compress = zlib.compress decompress = zlib.decompress if write or delete: if long_format: - usage("-l not allowed with -w or -p") + return usage("-l not allowed with -w or -p") if read or write or delete: if not args: - usage("No attr_name") + return usage("No attr_name") attr_name = args.pop(0) if write: if not args: - usage("No attr_value") + return usage("No attr_value") attr_value = args.pop(0).encode('utf-8') if len(args) > 1: @@ -188,31 +195,42 @@ def main(): file_prefix = "" for attr_name in attr_names: + should_dump = False try: try: attr_value = decompress(attrs[attr_name]) except zlib.error: attr_value = attrs[attr_name] - attr_value = attr_value.decode('utf-8') + try: + if b'\0' in attr_value: + # force dumping + raise NullsInString + attr_value = attr_value.decode('utf-8') + except (UnicodeDecodeError, NullsInString): + attr_value = attr_value.decode('latin-1') + should_dump = True except KeyError: onError("%sNo such xattr: %s" % (file_prefix, attr_name)) continue if long_format: - try: - if '\0' in attr_value: - raise NullsInString - print("".join((file_prefix, "%s: " % (attr_name,), attr_value))) - except (UnicodeDecodeError, NullsInString): + if should_dump: print("".join((file_prefix, "%s:" % (attr_name,)))) print(_dump(attr_value)) + else: + print("".join((file_prefix, "%s: " % (attr_name,), attr_value))) else: if read: - print("".join((file_prefix, attr_value))) + if should_dump: + if file_prefix: + print(file_prefix) + print(_dump(attr_value)) + else: + print("".join((file_prefix, attr_value))) else: print("".join((file_prefix, attr_name))) - sys.exit(status) + return status if __name__ == "__main__": - main() + sys.exit(main(sys.argv)) |