summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBob Ippolito <bob@redivi.com>2021-09-22 21:20:01 -0700
committerGitHub <noreply@github.com>2021-09-22 21:20:01 -0700
commitdc336dea43ed57a14445339eade738845adaab2e (patch)
tree2924c95456f61edec8315202a7944ec814a6362a
parent3e146d58daa9b17d4ff9d32a185d5d9bbaf45b16 (diff)
parent5c06ab1418daa853575106073cd67ed0fec6ce99 (diff)
downloadxattr-dc336dea43ed57a14445339eade738845adaab2e.tar.gz
Merge pull request #93 from tipabu/non-utf8-values
Various dump-related fixes
-rw-r--r--xattr/tests/test_tool.py117
-rwxr-xr-xxattr/tool.py62
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))