4 # fuse_xattrs - Add xattrs support using sidecar files
6 # Copyright (C) 2016 Felipe Barriga Richards <felipe {at} felipebarriga.cl>
8 # This program can be distributed under the terms of the GNU GPL.
9 # See the file COPYING.
14 from pathlib import Path
19 if xattr.__version__ != '0.9.1':
20 print("WARNING, only tested with xattr version 0.9.1")
22 # Linux / BSD Compatibility
23 ENOATTR = errno.ENODATA
24 if hasattr(errno, "ENOATTR"):
25 ENOATTR = errno.ENOATTR
28 # - listxattr: list too long
29 # - sidecar file permissions
30 # - corrupt metadata files
32 skipNamespaceTests = False
35 class TestXAttrBase(unittest.TestCase):
37 self.sourceDir = "./source/"
38 self.mountDir = "./mount/"
39 self.randomFilename = "foo.txt"
41 self.randomFile = self.mountDir + self.randomFilename
42 self.randomFileSidecar = self.randomFile + ".xattr"
44 self.randomSourceFile = self.sourceDir + self.randomFilename
45 self.randomSourceFileSidecar = self.randomSourceFile + ".xattr"
47 if os.path.isfile(self.randomFile):
48 os.remove(self.randomFile)
50 if os.path.isfile(self.randomFileSidecar):
51 os.remove(self.randomFileSidecar)
53 Path(self.randomFile).touch()
54 self.assertTrue(os.path.isfile(self.randomFile))
55 self.assertFalse(os.path.isfile(self.randomFileSidecar))
58 if os.path.isfile(self.randomFile):
59 os.remove(self.randomFile)
61 if os.path.isfile(self.randomFileSidecar):
62 os.remove(self.randomFileSidecar)
65 ##########################################################################################
66 # The following tests are generic for any FS that supports extended attributes
67 class TestGenericXAttrs(TestXAttrBase):
69 def test_xattr_set(self):
70 xattr.setxattr(self.randomFile, "user.foo", bytes("bar", "utf-8"))
72 def test_xattr_get_non_existent(self):
74 with self.assertRaises(OSError) as ex:
75 xattr.getxattr(self.randomFile, key)
76 self.assertEqual(ex.exception.errno, ENOATTR)
78 def test_xattr_get(self):
82 xattr.setxattr(self.randomFile, key, bytes(value, enc))
83 read_value = xattr.getxattr(self.randomFile, key)
84 self.assertEqual(value, read_value.decode(enc))
86 # FIXME: On OSX fuse is replacing the return value 0 with -1
87 def test_xattr_set_empty(self):
91 xattr.setxattr(self.randomFile, key, bytes(value, enc))
92 read_value = xattr.getxattr(self.randomFile, key)
93 self.assertEqual(value, read_value.decode(enc))
95 def test_xattr_set_override(self):
100 xattr.setxattr(self.randomFile, key, bytes(value1, enc))
101 xattr.setxattr(self.randomFile, key, bytes(value2, enc))
102 read_value = xattr.getxattr(self.randomFile, key)
103 self.assertEqual(value2, read_value.decode(enc))
105 def test_xattr_set_create(self):
110 xattr.setxattr(self.randomFile, key, bytes(value1, enc), xattr.XATTR_CREATE)
111 with self.assertRaises(FileExistsError) as ex:
112 xattr.setxattr(self.randomFile, key, bytes(value2, enc), xattr.XATTR_CREATE)
113 self.assertEqual(ex.exception.errno, 17)
114 self.assertEqual(ex.exception.strerror, "File exists")
116 read_value = xattr.getxattr(self.randomFile, key)
117 self.assertEqual(value1, read_value.decode(enc))
119 def test_xattr_set_replace(self):
123 with self.assertRaises(OSError) as ex:
124 xattr.setxattr(self.randomFile, key, bytes(value, enc), xattr.XATTR_REPLACE)
125 self.assertEqual(ex.exception.errno, ENOATTR)
127 with self.assertRaises(OSError) as ex:
128 xattr.getxattr(self.randomFile, key)
129 self.assertEqual(ex.exception.errno, ENOATTR)
131 def test_xattr_list_empty(self):
132 attrs = xattr.listxattr(self.randomFile)
133 self.assertEqual(len(attrs), 0)
135 def test_xattr_list(self):
143 xattr.setxattr(self.randomFile, key1, bytes(value, enc))
144 xattr.setxattr(self.randomFile, key2, bytes(value, enc))
145 xattr.setxattr(self.randomFile, key3, bytes(value, enc))
147 # get and check the list
148 attrs = xattr.listxattr(self.randomFile)
150 self.assertEqual(len(attrs), 3)
151 self.assertTrue(key1 in attrs)
152 self.assertTrue(key2 in attrs)
153 self.assertTrue(key3 in attrs)
155 def test_xattr_unicode(self):
157 key = "user.fooシüßてЙĘ𝄠✠"
161 xattr.setxattr(self.randomFile, key, bytes(value, enc))
164 attrs = xattr.listxattr(self.randomFile)
165 self.assertEqual(len(attrs), 1)
166 self.assertEqual(attrs[0], key)
169 read_value = xattr.getxattr(self.randomFile, key)
170 self.assertEqual(value, read_value.decode(enc))
173 xattr.removexattr(self.randomFile, key)
175 def test_xattr_remove(self):
181 xattr.setxattr(self.randomFile, key, bytes(value, enc))
184 xattr.removexattr(self.randomFile, key)
186 # list should be empty
187 attrs = xattr.listxattr(self.randomFile)
188 self.assertEqual(len(attrs), 0)
190 # should fail when trying to read it
191 with self.assertRaises(OSError) as ex:
192 xattr.getxattr(self.randomFile, key)
193 self.assertEqual(ex.exception.errno, ENOATTR)
195 # removing twice should fail
196 with self.assertRaises(OSError) as ex:
197 xattr.getxattr(self.randomFile, key)
198 self.assertEqual(ex.exception.errno, ENOATTR)
200 def test_xattr_set_name_max_length(self):
202 if sys.platform != 'linux':
203 max_len = 127 # OSX VFS only support names up to 127 bytes
206 key = "user." + "x" * (max_len - 5)
208 self.assertEqual(len(key), max_len)
209 xattr.setxattr(self.randomFile, key, bytes(value, enc)) # NOTE: WILL FAIL IF RUNNING ON HFS+
211 key = "user." + "x" * (max_len - 5 + 1)
212 self.assertEqual(len(key), max_len + 1)
213 with self.assertRaises(OSError) as ex:
214 xattr.setxattr(self.randomFile, key, bytes(value, enc))
216 if sys.platform == 'linux':
217 self.assertEqual(ex.exception.errno, errno.ERANGE) # This is the behavior of BTRFS
219 self.assertEqual(ex.exception.errno, errno.ENAMETOOLONG)
221 #########################################################
222 # On Linux: The test fails as the max value is detected before reaching the filesystem
223 # On OSX: Works on fuse_xattr but fails on HFS+ (doesn't has limit)
224 @unittest.skipIf(sys.platform == "linux", "Skipping test on Linux")
225 def test_xattr_set_value_max_size(self):
228 value = "x" * (64 * 1024 + 1) # we want max 64KiB of data
229 with self.assertRaises(OSError) as ex:
230 xattr.setxattr(self.randomFile, key, bytes(value, enc)) # NOTE: This test fail on HFS+ (doesn't have limit)
232 self.assertEqual(ex.exception.errno, errno.ENOSPC)
234 # on fuse_xattr we get "Argument list too long"
235 # the error is thrown by fuse, not by fuse_xattr code
237 #########################################################
238 # Those tests are going to pass on Linux but fail on BSD
239 # BSD doesn't support namespaces.
240 @unittest.skipIf(skipNamespaceTests, "Namespace tests disabled")
241 def test_xattr_set_namespaces(self):
242 with self.assertRaises(OSError) as ex:
243 xattr.setxattr(self.randomFile, "system.foo", bytes("bar", "utf-8"))
244 self.assertEqual(ex.exception.errno, 95)
245 self.assertEqual(ex.exception.strerror, "Operation not supported")
247 with self.assertRaises(OSError) as ex:
248 xattr.setxattr(self.randomFile, "trust.foo", bytes("bar", "utf-8"))
249 self.assertEqual(ex.exception.errno, 95)
250 self.assertEqual(ex.exception.strerror, "Operation not supported")
252 with self.assertRaises(OSError) as ex:
253 xattr.setxattr(self.randomFile, "foo.foo", bytes("bar", "utf-8"))
254 self.assertEqual(ex.exception.errno, 95)
255 self.assertEqual(ex.exception.strerror, "Operation not supported")
257 with self.assertRaises(PermissionError) as ex:
258 xattr.setxattr(self.randomFile, "security.foo", bytes("bar", "utf-8"))
259 self.assertEqual(ex.exception.errno, 1)
260 self.assertEqual(ex.exception.strerror, "Operation not permitted")
263 ######################################################################
264 # The following tests should fail if they are running on a normal FS
265 class TestFuseXATTR(TestXAttrBase):
267 def test_hide_sidecar(self):
268 xattr.setxattr(self.randomFile, "user.foo", bytes("bar", "utf-8"))
269 self.assertTrue(os.path.isfile(self.randomFile))
270 self.assertFalse(os.path.isfile(self.randomFileSidecar))
272 sidecarFilename = self.randomFilename + ".xattr"
273 files_mount = os.listdir(self.mountDir)
274 self.assertTrue(self.randomFilename in files_mount)
275 self.assertTrue(sidecarFilename not in files_mount)
277 files_source = os.listdir(self.sourceDir)
278 self.assertTrue(self.randomFilename in files_source)
279 self.assertTrue(sidecarFilename in files_source)
281 def test_fuse_xattr_create_new_file(self):
282 test_filename = "test_create_new_file"
283 self.assertFalse(os.path.isfile(self.sourceDir + test_filename))
284 self.assertFalse(os.path.isfile(self.mountDir + test_filename))
286 open(self.mountDir + test_filename, "a").close()
287 self.assertTrue(os.path.isfile(self.sourceDir + test_filename))
288 self.assertTrue(os.path.isfile(self.mountDir + test_filename))
289 # FIXME: if one assert fails, the file isn't going to be deleted
290 os.remove(self.mountDir + test_filename)
292 def test_fuse_xattr_remove_file_with_sidecar(self):
293 xattr.setxattr(self.randomFile, "user.foo", bytes("bar", "utf-8"))
294 self.assertTrue(os.path.isfile(self.randomFile))
295 self.assertTrue(os.path.isfile(self.randomSourceFile))
296 self.assertTrue(os.path.isfile(self.randomSourceFileSidecar))
298 os.remove(self.randomFile)
299 self.assertFalse(os.path.isfile(self.randomFile))
300 self.assertFalse(os.path.isfile(self.randomSourceFile))
301 self.assertFalse(os.path.isfile(self.randomSourceFileSidecar))
303 def test_fuse_xattr_remove_file_without_sidecar(self):
304 self.assertTrue(os.path.isfile(self.randomFile))
305 self.assertTrue(os.path.isfile(self.randomSourceFile))
306 self.assertFalse(os.path.isfile(self.randomSourceFileSidecar))
308 os.remove(self.randomFile)
309 self.assertFalse(os.path.isfile(self.randomFile))
310 self.assertFalse(os.path.isfile(self.randomSourceFile))
311 self.assertFalse(os.path.isfile(self.randomSourceFileSidecar))
313 if __name__ == '__main__':