From 8066019b71da7c9240404e8d80b03175a469dbf0 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Thu, 27 May 2021 21:19:23 -0700 Subject: tool: Fix dumping on py2 Previously, translate() would complain TypeError: character mapping must return integer, None or unicode --- xattr/tool.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xattr/tool.py b/xattr/tool.py index a7cfb47..89b9221 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -67,7 +67,14 @@ def usage(e=None): sys.exit(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): -- cgit v1.2.1 From 6cad60fd562ce3716d8cfb18bbdf3bf46639c348 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Thu, 27 May 2021 21:41:58 -0700 Subject: tool: Dump non-utf8 data similar to what's done for NULs Addresses #90. Side-effect: dump non-ascii data as the bytes instead of the unicode code point. This seems to better match user expectations when presented with the dump formatting, anyway. --- xattr/tool.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/xattr/tool.py b/xattr/tool.py index 89b9221..5bc4000 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -195,27 +195,39 @@ 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: + print(file_prefix, end="") + sys.stdout.flush() + with os.fdopen(sys.stdout.fileno(), 'wb', closefd=False) as fp: + fp.write(attr_value.encode('latin-1') + b'\n') + else: + print("".join((file_prefix, attr_value))) else: print("".join((file_prefix, attr_name))) -- cgit v1.2.1 From 48f7881ab24e5acfb4d16c4bb6137b918b8a9393 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Thu, 27 May 2021 21:43:07 -0700 Subject: tool: Be willing to dump for -p like we do for -l This both side-steps the stdout buffer flushing issue and prevents ambiguities where terminals don't display NUL. --- xattr/tool.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/xattr/tool.py b/xattr/tool.py index 5bc4000..ac12654 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -222,10 +222,9 @@ def main(): else: if read: if should_dump: - print(file_prefix, end="") - sys.stdout.flush() - with os.fdopen(sys.stdout.fileno(), 'wb', closefd=False) as fp: - fp.write(attr_value.encode('latin-1') + b'\n') + if file_prefix: + print(file_prefix) + print(_dump(attr_value)) else: print("".join((file_prefix, attr_value))) else: -- cgit v1.2.1 From 5c06ab1418daa853575106073cd67ed0fec6ce99 Mon Sep 17 00:00:00 2001 From: Tim Burke Date: Fri, 27 Aug 2021 15:44:42 -0700 Subject: Add tests for xattr.tool As a side-effect, make it easier to run xattr.tool.main from tests. --- xattr/tests/test_tool.py | 117 +++++++++++++++++++++++++++++++++++++++++++++++ xattr/tool.py | 28 ++++++------ 2 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 xattr/tests/test_tool.py 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 ac12654..0623560 100755 --- a/xattr/tool.py +++ b/xattr/tool.py @@ -62,9 +62,9 @@ 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 if sys.version_info < (3,): @@ -87,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 @@ -105,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": @@ -113,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: @@ -230,7 +230,7 @@ def main(): else: print("".join((file_prefix, attr_name))) - sys.exit(status) + return status if __name__ == "__main__": - main() + sys.exit(main(sys.argv)) -- cgit v1.2.1