Repository: libfuse/python-fuse (language: python)

Issue:
UTF-8 extended attributes are cut off
## DESCRIPTION

When using `getxattr` and returning an UTF-8 string, the length is improperly calculated: it uses the length of the string character-wise, but not byte-wise, which results in either incorrectly returned strings.

This happened to me when using Japanese characters in the extended attributes. Below I am appending an example of this behavior. For me it happens both on `python-fuse` version 1.0.7 and 1.0.8.

## EXAMPLE

```python
import fuse
import stat, errno
from fuse import Fuse, Stat, Direntry

fuse.fuse_python_api = (0, 2)

BROKEN_FILE = '/utf8_attr'
FATTR_NAME = 'user.xdg.comment'
FATTR_VALUE = 'ああ、メッセージは切り取られていない'

class EmptyStat(Stat):
  def __init__(self):
    self.st_mode = 0
    self.st_ino = 0
    self.st_dev = 0
    self.st_nlink = 0
    self.st_uid = 0
    self.st_gid = 0
    self.st_size = 0
    self.st_atime = 0
    self.st_mtime = 0
    self.st_ctime = 0

class GetAttrBug(Fuse):
  def getattr(self, path):
    ret_stat = EmptyStat()
    if path == '/':
      ret_stat.st_mode = stat.S_IFDIR | int(0e755)
      return ret_stat

    if path == BROKEN_FILE:
      ret_stat.st_mode = stat.S_IFREG | int(0e000)
      return ret_stat
    
    return -errno.ENOENT

  def readdir(self, path, offset):
    yield Direntry('.')
    yield Direntry('..')
    yield Direntry(BROKEN_FILE[1:])

  def open(self, path, flags):
    return -errno.EACCES

  def read(self, path, size, offset):
    return

  def listxattr(self, path, size):
    if size == 0: return 1
    else: return [ FATTR_NAME ]

  def getxattr(self, path, attr, size):
    if size == 0: return len(FATTR_VALUE.encode('utf8'))
    else: return FATTR_VALUE

if __name__ == '__main__':
  server = GetAttrBug(dash_s_do='setsingle')
  server.parse(errex=1)
  server.main()
```

In this example, `FATTR_VALUE` is **18** characters long, but takes up **54** bytes.

After running the example, and using `os.getxattr` to retrieve it back, we'll get a shorter string, **6 characters long** and **18** bytes long.

```sh
# start the bug example
$ python3 test.py test

# read the file within the fuse
$ python3 -c 'import os; print(os.getxattr("./test/utf8_attr", "user.xdg.comment").decode())'
#   returns: [len=6,  bytelength=18] "ああ、メッセ"
#  expected: [len=18, bytelength=54] "ああ、メッセージは切り取られていない"
```


Discussion / hints:

Fix patch:
diff --git a/example/xattr.py b/example/xattr.py
new file mode 100644
index 0000000..fe5ffab
--- /dev/null
+++ b/example/xattr.py
@@ -0,0 +1,61 @@
+# Example by github @vignedev from https://github.com/libfuse/python-fuse/issues/77
+
+import fuse
+import stat, errno
+from fuse import Fuse, Stat, Direntry
+
+fuse.fuse_python_api = (0, 2)
+
+BROKEN_FILE = '/utf8_attr'
+FATTR_NAME = 'user.xdg.comment'
+FATTR_VALUE = 'ああ、メッセージは切り取られていない'
+
+class EmptyStat(Stat):
+  def __init__(self):
+    self.st_mode = 0
+    self.st_ino = 0
+    self.st_dev = 0
+    self.st_nlink = 0
+    self.st_uid = 0
+    self.st_gid = 0
+    self.st_size = 0
+    self.st_atime = 0
+    self.st_mtime = 0
+    self.st_ctime = 0
+
+class GetAttrBug(Fuse):
+  def getattr(self, path):
+    ret_stat = EmptyStat()
+    if path == '/':
+      ret_stat.st_mode = stat.S_IFDIR | int(0e755)
+      return ret_stat
+
+    if path == BROKEN_FILE:
+      ret_stat.st_mode = stat.S_IFREG | int(0e000)
+      return ret_stat
+    
+    return -errno.ENOENT
+
+  def readdir(self, path, offset):
+    yield Direntry('.')
+    yield Direntry('..')
+    yield Direntry(BROKEN_FILE[1:])
+
+  def open(self, path, flags):
+    return -errno.EACCES
+
+  def read(self, path, size, offset):
+    return
+
+  def listxattr(self, path, size):
+    if size == 0: return 1
+    else: return [ FATTR_NAME ]
+
+  def getxattr(self, path, attr, size):
+    if size == 0: return len(FATTR_VALUE.encode('utf8'))
+    else: return FATTR_VALUE
+
+if __name__ == '__main__':
+  server = GetAttrBug(dash_s_do='setsingle')
+  server.parse(errex=1)
+  server.main()
diff --git a/example/xmp.py b/example/xmp.py
index 26bee73..d99b07e 100644
--- a/example/xmp.py
+++ b/example/xmp.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-#    Copyright (C) 2001  Jeff Epler  <jepler@unpythonic.dhs.org>
+#    Copyright (C) 2001  Jeff Epler  <jepler@gmail.com>
 #    Copyright (C) 2006  Csaba Henk  <csaba.henk@creo.hu>
 #
 #    This program can be distributed under the terms of the GNU LGPL.
diff --git a/fuse.py b/fuse.py
index 68d5e50..2b80c09 100644
--- a/fuse.py
+++ b/fuse.py
@@ -1,5 +1,5 @@
 #
-#    Copyright (C) 2001  Jeff Epler  <jepler@unpythonic.dhs.org>
+#    Copyright (C) 2001  Jeff Epler  <jepler@gmail.com>
 #    Copyright (C) 2006  Csaba Henk  <csaba.henk@creo.hu>
 #
 #    This program can be distributed under the terms of the GNU LGPL.
diff --git a/fuseparts/_fusemodule.c b/fuseparts/_fusemodule.c
index 25523af..691b286 100644
--- a/fuseparts/_fusemodule.c
+++ b/fuseparts/_fusemodule.c
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2001  Jeff Epler  <jepler@unpythonic.dhs.org>
+    Copyright (C) 2001  Jeff Epler  <jepler@gmail.com>
 
     This program can be distributed under the terms of the GNU LGPL.
     See the file COPYING.
@@ -94,12 +94,16 @@
         PyErr_SetString(PyExc_ValueError, "non-decodable filename");
         return NULL;
     }
-
+    static inline Py_ssize_t PyString_Size(PyObject *s) {
+        Py_ssize_t result = -1;
+        (void)PyUnicode_AsUTF8AndSize(s, &result);
+        return result;
+    }
 #else
     #define PyString_AsString PyUnicode_AsUTF8
+    #define PyString_Size PyUnicode_GET_LENGTH
 #endif
     #define PyString_Check PyUnicode_Check
-    #define PyString_Size PyUnicode_GET_LENGTH
 #endif
 
 #ifdef FIX_PATH_DECODING
@@ -683,14 +687,14 @@ read_func(const char *path, char *buf, size_t s, off_t off)
 	if(PyObject_CheckBuffer(v)) {
 		PyObject_GetBuffer(v, &buffer, PyBUF_SIMPLE);
 
-		if(buffer.len <= s) {
+		if((size_t)buffer.len <= s) {
 			memcpy(buf, buffer.buf, buffer.len);
 			ret = buffer.len;
 		}
 
 		PyBuffer_Release(&buffer);
 	} else if(PyBytes_Check(v)) {
-		if(PyBytes_Size(v) > s)
+		if((size_t)PyBytes_Size(v) > s)
 			goto OUT_DECREF;
 		memcpy(buf, PyBytes_AsString(v), PyBytes_Size(v));
 		ret = PyBytes_Size(v);
@@ -944,7 +948,7 @@ getxattr_func(const char *path, const char *name, char *value, size_t size)
         /* If the size of the value buffer is too small to hold the result,  errno
          * is set to ERANGE.
          */
-		if (PyString_Size(v) > size) {
+		if ((size_t)PyString_Size(v) > size) {
             ret = -ERANGE;
 			goto OUT_DECREF;
         }
@@ -980,9 +984,9 @@ listxattr_func(const char *path, char *list, size_t size)
 	}
 
 	for (;;) {
-		int ilen;
+		size_t ilen;
 
-	        w = PyIter_Next(iter);
+		w = PyIter_Next(iter);
 		if (!w) {
 			ret = lx - list;
 			break;
@@ -1302,7 +1306,9 @@ pyfuse_loop_mt(struct fuse *f)
 #ifdef WITH_THREAD
 	PyThreadState *save;
 
+#if PY_VERSION_HEX < 0x03070000
 	PyEval_InitThreads();
+#endif
 	interp = PyThreadState_Get()->interp;
 	save = PyEval_SaveThread();
 	err = fuse_loop_mt(f);
diff --git a/setup.py b/setup.py
index 4916579..64ad5a8 100755
--- a/setup.py
+++ b/setup.py
@@ -103,7 +103,7 @@ def setup(**kwargs):
        package_data={'': ['COPYING', 'AUTHORS', 'FAQ', 'INSTALL',
                           'README.md', 'README.new_fusepy_api.rst',
                           'README.package_maintainers.rst']},
-       author = 'Jeff Epler <jepler@unpythonic.dhs.org>, Csaba Henk <csaba.henk@creo.hu>, Steven James, Miklos Szeredi <miklos@szeredi.hu>, Sébastien Delafond<sdelafond@gmail.com>',
+       author = 'Csaba Henk <csaba.henk@creo.hu>, Steven James, Miklos Szeredi <miklos@szeredi.hu>, Sébastien Delafond<sdelafond@gmail.com>',
        maintainer = 'Sébastien Delafond',
        maintainer_email = 'sdelafond@gmail.com',
        ext_modules = [fusemodule],


Test patch:
diff --git a/tests/test_basic.py b/tests/test_basic.py
index 380ce30..6402fba 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -61,3 +61,6 @@ def test_fioc(filesystem):
         fcntl.ioctl(f.fileno(), FIOC_GET_SIZE, b)
         assert struct.unpack("L", b)[0] == 42
 
+@pytest.mark.fstype("xattr")
+def test_xattr(filesystem):
+    assert os.getxattr(filesystem / "utf8_attr", "user.xdg.comment").decode("utf-8") == 'ああ、メッセージは切り取られていない'