Thu May 20 02:41:05 CEST 2010  Francois Deppierraz <francois@ctrlaltdel.ch>
  * stringutils.py: Unicode helper functions + associated tests
  
  This file contains a bunch of helper functions which converts
  unicode string from and to argv, filenames and stdout.
diff -rN -u old-tahoe-534/src/allmydata/test/test_stringutils.py new-tahoe-534/src/allmydata/test/test_stringutils.py
--- old-tahoe-534/src/allmydata/test/test_stringutils.py	1970-01-01 01:00:00.000000000 +0100
+++ new-tahoe-534/src/allmydata/test/test_stringutils.py	2010-05-20 02:44:22.521766039 +0200
@@ -0,0 +1,259 @@
+# coding=utf-8
+
+TEST_FILENAMES = (
+  u'Ärtonwall.mp3',
+  u'test_file',
+  u'Blah blah.txt',
+)
+
+# The following main helps to generate a test class for other operating
+# systems.
+
+if __name__ == "__main__":
+    import sys, os
+    import tempfile
+    import shutil
+    import platform
+    
+    if len(sys.argv) != 2:
+        print "Usage: %s lumière" % sys.argv[0]
+        sys.exit(1)
+    
+    print
+    print "class MyWeirdOS(StringUtils, unittest.TestCase):"
+    print "    uname = '%s'" % ' '.join(platform.uname())
+    print "    argv = %s" % repr(sys.argv[1])
+    print "    platform = '%s'" % sys.platform
+    print "    filesystemencoding = '%s'" % sys.getfilesystemencoding()
+    print "    stdoutencoding = '%s'" % sys.stdout.encoding
+
+    try:
+        tmpdir = tempfile.mkdtemp()
+        for fname in TEST_FILENAMES:
+            open(os.path.join(tmpdir, fname), 'w').close() 
+
+        # Use Unicode API under Windows or MacOS X
+        if sys.platform in ('win32', 'darwin'):
+            dirlist = os.listdir(unicode(tmpdir))
+        else:
+            dirlist = os.listdir(tmpdir)
+
+        print "    dirlist = %s" % repr(dirlist)
+    except:
+        print "    # Oops, I cannot write filenames containing non-ascii characters"
+    print
+
+    shutil.rmtree(tmpdir)
+    sys.exit(0)
+
+from twisted.trial import unittest
+from mock import patch
+import sys
+
+from allmydata.util.stringutils import argv_to_unicode, unicode_to_url, \
+    unicode_to_stdout, unicode_platform, listdir_unicode, open_unicode, \
+    FilenameEncodingError, get_term_encoding
+from twisted.python import usage
+
+class StringUtilsErrors(unittest.TestCase):
+    @patch('sys.stdout')
+    def test_get_term_encoding(self, mock):
+        mock.encoding = None
+        
+        self.failUnlessEqual(get_term_encoding(), 'ascii')
+
+    @patch('sys.stdout')
+    def test_argv_to_unicode(self, mock):
+        mock.encoding = 'utf-8'
+
+        self.failUnlessRaises(usage.UsageError,
+                              argv_to_unicode,
+                              u'lumière'.encode('latin1'))
+
+    def test_unicode_to_url(self):
+        pass
+
+    @patch('sys.stdout')
+    def test_unicode_to_stdout(self, mock):
+        # Encoding koi8-r cannot represent 'è'
+        mock.encoding = 'koi8-r'
+        self.failUnlessEqual(unicode_to_stdout(u'lumière'), 'lumi?re')
+
+    @patch('os.listdir')
+    def test_unicode_normalization(self, mock):
+        # Pretend to run on an Unicode platform such as Windows
+        orig_platform = sys.platform
+        sys.platform = 'win32'
+
+        mock.return_value = [u'A\u0308rtonwall.mp3']
+        self.failUnlessEqual(listdir_unicode(u'/dummy'), [u'\xc4rtonwall.mp3'])
+
+        sys.platform = orig_platform
+
+# The following tests applies only to platforms which don't store filenames as
+# Unicode entities on the filesystem.
+class StringUtilsNonUnicodePlatform(unittest.TestCase):
+    def setUp(self):
+        # Mock sys.platform because unicode_platform() uses it
+        self.original_platform = sys.platform
+        sys.platform = 'linux'
+
+    def tearDown(self):
+        sys.platform = self.original_platform
+
+    @patch('sys.getfilesystemencoding')
+    @patch('os.listdir')
+    def test_listdir_unicode(self, mock_listdir, mock_getfilesystemencoding):
+        # What happen if a latin1-encoded filenames is encountered on an UTF-8
+        # filesystem?
+        mock_listdir.return_value = [
+            u'lumière'.encode('utf-8'),
+            u'lumière'.encode('latin1')]
+
+        mock_getfilesystemencoding.return_value = 'utf-8'
+       
+        self.failUnlessRaises(FilenameEncodingError,
+                              listdir_unicode,
+                              u'/dummy')
+        
+        # We're trying to list a directory whose name cannot be represented in
+        # the filesystem encoding.  This should fail.
+        mock_getfilesystemencoding.return_value = 'ascii'
+        self.failUnlessRaises(FilenameEncodingError,
+                              listdir_unicode,
+                              u'/lumière')
+
+    @patch('sys.getfilesystemencoding')
+    def test_open_unicode(self, mock):
+        mock.return_value = 'ascii'
+
+        self.failUnlessRaises(FilenameEncodingError,
+                              open_unicode,
+                              u'lumière')
+
+class StringUtils():
+    def setUp(self):
+        # Mock sys.platform because unicode_platform() uses it
+        self.original_platform = sys.platform
+        sys.platform = self.platform
+
+    def tearDown(self):
+        sys.platform = self.original_platform
+
+    @patch('sys.stdout')
+    def test_argv_to_unicode(self, mock):
+        if 'argv' not in dir(self):
+            raise unittest.SkipTest("There's no way to pass non-ASCII arguments in CLI on this (mocked) platform")
+
+        mock.encoding = self.stdoutencoding
+
+        argu = u'lumière'
+        argv = self.argv
+
+        self.failUnlessEqual(argv_to_unicode(argv), argu)
+
+    def test_unicode_to_url(self):
+        self.failUnless(unicode_to_url(u'lumière'), u'lumière'.encode('utf-8'))
+
+    @patch('sys.stdout')
+    def test_unicode_to_stdout(self, mock):
+        if 'argv' not in dir(self):
+            raise unittest.SkipTest("There's no way to pass non-ASCII arguments in CLI on this (mocked) platform")
+
+        mock.encoding = self.stdoutencoding
+        self.failUnlessEqual(unicode_to_stdout(u'lumière'), self.argv)
+
+    def test_unicode_platform(self):
+        matrix = {
+          'linux2': False,
+          'win32':  True,
+          'darwin': True,
+        }
+
+        self.failUnlessEqual(unicode_platform(), matrix[self.platform])
+ 
+    @patch('sys.getfilesystemencoding')
+    @patch('os.listdir')
+    def test_listdir_unicode(self, mock_listdir, mock_getfilesystemencoding):
+
+        mock_listdir.return_value = self.dirlist
+        mock_getfilesystemencoding.return_value = self.filesystemencoding
+       
+        filenames = listdir_unicode(u'/dummy')
+
+        for fname in TEST_FILENAMES:
+            self.failUnless(isinstance(fname, unicode))
+
+            if fname not in filenames:
+                self.fail("Cannot find %r in %r" % (fname, filenames))
+
+    @patch('os.open')
+    def test_open_unicode(self, mock):
+
+        self.failUnlessRaises(IOError,
+                              open_unicode,
+                              u'/dummy_directory/lumière.txt')
+
+
+class UbuntuKarmicUTF8(StringUtils, unittest.TestCase):
+    uname = 'Linux korn 2.6.31-14-generic #48-Ubuntu SMP Fri Oct 16 14:05:01 UTC 2009 x86_64'
+    argv = 'lumi\xc3\xa8re'
+    platform = 'linux2'
+    filesystemencoding = 'UTF-8'
+    stdoutencoding = 'UTF-8'
+    dirlist = ['test_file', '\xc3\x84rtonwall.mp3', 'Blah blah.txt']
+
+
+class UbuntuKarmicLatin1(StringUtils, unittest.TestCase):
+    uname = 'Linux korn 2.6.31-14-generic #48-Ubuntu SMP Fri Oct 16 14:05:01 UTC 2009 x86_64'
+    argv = 'lumi\xe8re'
+    platform = 'linux2'
+    filesystemencoding = 'ISO-8859-1'
+    stdoutencoding = 'ISO-8859-1'
+    dirlist = ['test_file', 'Blah blah.txt', '\xc4rtonwall.mp3']
+
+class WindowsXP(StringUtils, unittest.TestCase):
+    uname = 'Windows XP 5.1.2600 x86 x86 Family 15 Model 75 Step ping 2, AuthenticAMD'
+    argv = 'lumi\xe8re'
+    platform = 'win32'
+    filesystemencoding = 'mbcs'
+    stdoutencoding = 'cp850'
+    dirlist = [u'Blah blah.txt', u'test_file', u'\xc4rtonwall.mp3']
+
+    todo = "Unicode arguments on the command-line is not yet supported under Windows, see bug #565."
+
+class WindowsXP_UTF8(StringUtils, unittest.TestCase):
+    uname = 'Windows XP 5.1.2600 x86 x86 Family 15 Model 75 Step ping 2, AuthenticAMD'
+    argv = 'lumi\xe8re'
+    platform = 'win32'
+    filesystemencoding = 'mbcs'
+    stdoutencoding = 'cp65001'
+    dirlist = [u'Blah blah.txt', u'test_file', u'\xc4rtonwall.mp3']
+
+    todo = "Unicode arguments on the command-line is not yet supported under Windows, see bug #565."
+
+class WindowsVista(StringUtils, unittest.TestCase):
+    uname = 'Windows Vista 6.0.6000 x86 x86 Family 6 Model 15 Stepping 11, GenuineIntel'
+    argv = 'lumi\xe8re'
+    platform = 'win32'
+    filesystemencoding = 'mbcs'
+    stdoutencoding = 'cp850'
+    dirlist = [u'Blah blah.txt', u'test_file', u'\xc4rtonwall.mp3']
+
+    todo = "Unicode arguments on the command-line is not yet supported under Windows, see bug #565."
+
+class MacOSXLeopard(StringUtils, unittest.TestCase):
+    uname = 'Darwin g5.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:57:01 PDT 2009; root:xnu-1228.15.4~1/RELEASE_PPC Power Macintosh powerpc'
+    argv = 'lumi\xc3\xa8re'
+    platform = 'darwin'
+    filesystemencoding = 'utf-8'
+    stdoutencoding = 'UTF-8'
+    dirlist = [u'A\u0308rtonwall.mp3', u'Blah blah.txt', u'test_file']
+
+class MacOSXLeopard7bit(StringUtils, unittest.TestCase):
+    uname = 'Darwin g5.local 9.8.0 Darwin Kernel Version 9.8.0: Wed Jul 15 16:57:01 PDT 2009; root:xnu-1228.15.4~1/RELEASE_PPC Power Macintosh powerpc'
+    #argv = 'lumiere'
+    platform = 'darwin'
+    filesystemencoding = 'utf-8'
+    stdoutencoding = 'US-ASCII'
+    dirlist = [u'A\u0308rtonwall.mp3', u'Blah blah.txt', u'test_file']
diff -rN -u old-tahoe-534/src/allmydata/util/stringutils.py new-tahoe-534/src/allmydata/util/stringutils.py
--- old-tahoe-534/src/allmydata/util/stringutils.py	1970-01-01 01:00:00.000000000 +0100
+++ new-tahoe-534/src/allmydata/util/stringutils.py	2010-05-20 02:44:22.561766240 +0200
@@ -0,0 +1,128 @@
+"""
+Functions used to convert inputs from whatever encoding used in the system to
+unicode and back.
+"""
+
+import sys
+import os
+import unicodedata
+from allmydata.util.assertutil import precondition
+from twisted.python import usage
+
+def get_term_encoding():
+    """
+    Returns expected encoding for writing to the terminal and reading
+    arguments from the command-line.
+    """
+
+    if sys.stdout.encoding == None:
+        return 'ascii'
+    else:
+        return sys.stdout.encoding
+
+def argv_to_unicode(s):
+    """
+    Decode given argv element to unicode.
+    """
+    # Try to decode the command-line argument with the encoding returned by
+    # get_term_encoding(), if this fails print an error message to the user.
+
+    precondition(isinstance(s, str), s)
+
+    try:
+        return unicode(s, get_term_encoding())
+    except UnicodeDecodeError:
+        raise usage.UsageError("Argument '%s' cannot be decoded as %s." %
+                               (s, get_term_encoding()))
+
+def unicode_to_url(s):
+    """
+    Encode an unicode object used in an URL.
+    """
+    # According to RFC 2718, non-ascii characters in url's must be UTF-8 encoded.
+
+    precondition(isinstance(s, unicode), s)
+    return s.encode('utf-8')
+
+def unicode_to_stdout(s):
+    """
+    Encode an unicode object for representation on stdout.
+    """
+
+    precondition(isinstance(s, unicode), s)
+    return s.encode(get_term_encoding(), 'replace')
+
+def unicode_platform():
+    """
+    Does the current platform handle Unicode filenames natively ?
+    """
+
+    return sys.platform in ('win32', 'darwin')
+
+class FilenameEncodingError(Exception):
+    """
+    Filename cannot be encoded using the current encoding of your filesystem
+    (%s). Please configure your locale correctly or rename this file.
+    """
+
+    pass
+
+def listdir_unicode_unix(path):
+    """
+    This function emulates an Unicode API under Unix similar to one available
+    under Windows or MacOS X.
+
+    If badly encoded filenames are encountered, an exception is raised.
+    """
+    precondition(isinstance(path, unicode), path)
+
+    encoding = sys.getfilesystemencoding()
+    try:
+        byte_path = path.encode(encoding)
+    except UnicodeEncodeError:
+        raise FilenameEncodingError(path)
+
+    try:
+        return [unicode(fn, encoding) for fn in os.listdir(byte_path)]
+    except UnicodeDecodeError:
+        raise FilenameEncodingError(fn)
+
+def listdir_unicode(path, encoding = None):
+    """
+    Wrapper around listdir() which provides safe access to the convenient
+    Unicode API even under Unix.
+    """
+
+    precondition(isinstance(path, unicode), path)
+
+    # On Windows and MacOS X, the Unicode API is used
+    if unicode_platform():
+        dirlist = os.listdir(path)
+
+    # On other platforms (ie. Unix systems), the byte-level API is used
+    else:
+        dirlist = listdir_unicode_unix(path)
+
+    # Normalize the resulting unicode filenames
+    #
+    # This prevents different OS from generating non-equal unicode strings for
+    # the same filename representation
+    return [unicodedata.normalize('NFC', fname) for fname in dirlist]
+
+def open_unicode(path, mode='r'):
+    """
+    Wrapper around open() which provides safe access to the convenient Unicode
+    API even under Unix.
+    """
+
+    precondition(isinstance(path, unicode), path)
+
+    if unicode_platform():
+        return open(path, mode)
+    else:
+        encoding = sys.getfilesystemencoding()
+
+        try:
+            return open(path.encode(encoding), mode)
+        except UnicodeEncodeError:
+            raise FilenameEncodingError(path)
