]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/encodingutil.py
Add FilePath support functions in encodingutil.py.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / util / encodingutil.py
1 """
2 Functions used to convert inputs from whatever encoding used in the system to
3 unicode and back.
4 """
5
6 import sys, os, re, locale
7 from types import NoneType
8
9 from allmydata.util.assertutil import precondition, _assert
10 from twisted.python import usage
11 from twisted.python.filepath import FilePath
12 from allmydata.util import log
13 from allmydata.util.fileutil import abspath_expanduser_unicode
14
15
16 def canonical_encoding(encoding):
17     if encoding is None:
18         log.msg("Warning: falling back to UTF-8 encoding.", level=log.WEIRD)
19         encoding = 'utf-8'
20     encoding = encoding.lower()
21     if encoding == "cp65001":
22         encoding = 'utf-8'
23     elif encoding == "us-ascii" or encoding == "646" or encoding == "ansi_x3.4-1968":
24         encoding = 'ascii'
25
26     return encoding
27
28 def check_encoding(encoding):
29     # sometimes Python returns an encoding name that it doesn't support for conversion
30     # fail early if this happens
31     try:
32         u"test".encode(encoding)
33     except (LookupError, AttributeError):
34         raise AssertionError("The character encoding '%s' is not supported for conversion." % (encoding,))
35
36 filesystem_encoding = None
37 io_encoding = None
38 is_unicode_platform = False
39 use_unicode_filepath = False
40
41 def _reload():
42     global filesystem_encoding, io_encoding, is_unicode_platform, use_unicode_filepath
43
44     filesystem_encoding = canonical_encoding(sys.getfilesystemencoding())
45     check_encoding(filesystem_encoding)
46
47     if sys.platform == 'win32':
48         # On Windows we install UTF-8 stream wrappers for sys.stdout and
49         # sys.stderr, and reencode the arguments as UTF-8 (see scripts/runner.py).
50         io_encoding = 'utf-8'
51     else:
52         ioenc = None
53         if hasattr(sys.stdout, 'encoding'):
54             ioenc = sys.stdout.encoding
55         if ioenc is None:
56             try:
57                 ioenc = locale.getpreferredencoding()
58             except Exception:
59                 pass  # work around <http://bugs.python.org/issue1443504>
60         io_encoding = canonical_encoding(ioenc)
61
62     check_encoding(io_encoding)
63
64     is_unicode_platform = sys.platform in ["win32", "darwin"]
65
66     # Despite the Unicode-mode FilePath support added to Twisted in
67     # <https://twistedmatrix.com/trac/ticket/7805>, we can't yet use
68     # Unicode-mode FilePaths with INotify on non-Windows platforms
69     # due to <https://twistedmatrix.com/trac/ticket/7928>.
70     use_unicode_filepath = sys.platform == "win32"
71
72 _reload()
73
74
75 def get_filesystem_encoding():
76     """
77     Returns expected encoding for local filenames.
78     """
79     return filesystem_encoding
80
81 def get_io_encoding():
82     """
83     Returns expected encoding for writing to stdout or stderr, and for arguments in sys.argv.
84     """
85     return io_encoding
86
87 def argv_to_unicode(s):
88     """
89     Decode given argv element to unicode. If this fails, raise a UsageError.
90     """
91     precondition(isinstance(s, str), s)
92
93     try:
94         return unicode(s, io_encoding)
95     except UnicodeDecodeError:
96         raise usage.UsageError("Argument %s cannot be decoded as %s." %
97                                (quote_output(s), io_encoding))
98
99 def argv_to_abspath(s, **kwargs):
100     """
101     Convenience function to decode an argv element to an absolute path, with ~ expanded.
102     If this fails, raise a UsageError.
103     """
104     decoded = argv_to_unicode(s)
105     if decoded.startswith(u'-'):
106         raise usage.UsageError("Path argument %s cannot start with '-'.\nUse %s if you intended to refer to a file."
107                                % (quote_output(s), quote_output(os.path.join('.', s))))
108     return abspath_expanduser_unicode(decoded, **kwargs)
109
110 def unicode_to_argv(s, mangle=False):
111     """
112     Encode the given Unicode argument as a bytestring.
113     If the argument is to be passed to a different process, then the 'mangle' argument
114     should be true; on Windows, this uses a mangled encoding that will be reversed by
115     code in runner.py.
116     """
117     precondition(isinstance(s, unicode), s)
118
119     if mangle and sys.platform == "win32":
120         # This must be the same as 'mangle' in bin/tahoe-script.template.
121         return str(re.sub(ur'[^\x20-\x7F]', lambda m: u'\x7F%x;' % (ord(m.group(0)),), s))
122     else:
123         return s.encode(io_encoding)
124
125 def unicode_to_url(s):
126     """
127     Encode an unicode object used in an URL.
128     """
129     # According to RFC 2718, non-ascii characters in URLs must be UTF-8 encoded.
130
131     # FIXME
132     return to_str(s)
133     #precondition(isinstance(s, unicode), s)
134     #return s.encode('utf-8')
135
136 def to_str(s):
137     if s is None or isinstance(s, str):
138         return s
139     return s.encode('utf-8')
140
141 def from_utf8_or_none(s):
142     precondition(isinstance(s, (NoneType, str)), s)
143     if s is None:
144         return s
145     return s.decode('utf-8')
146
147 PRINTABLE_ASCII = re.compile(r'^[\n\r\x20-\x7E]*$',          re.DOTALL)
148 PRINTABLE_8BIT  = re.compile(r'^[\n\r\x20-\x7E\x80-\xFF]*$', re.DOTALL)
149
150 def is_printable_ascii(s):
151     return PRINTABLE_ASCII.search(s) is not None
152
153 def unicode_to_output(s):
154     """
155     Encode an unicode object for representation on stdout or stderr.
156     """
157     precondition(isinstance(s, unicode), s)
158
159     try:
160         out = s.encode(io_encoding)
161     except (UnicodeEncodeError, UnicodeDecodeError):
162         raise UnicodeEncodeError(io_encoding, s, 0, 0,
163                                  "A string could not be encoded as %s for output to the terminal:\n%r" %
164                                  (io_encoding, repr(s)))
165
166     if PRINTABLE_8BIT.search(out) is None:
167         raise UnicodeEncodeError(io_encoding, s, 0, 0,
168                                  "A string encoded as %s for output to the terminal contained unsafe bytes:\n%r" %
169                                  (io_encoding, repr(s)))
170     return out
171
172
173 def _unicode_escape(m, quote_newlines):
174     u = m.group(0)
175     if u == u'"' or u == u'$' or u == u'`' or u == u'\\':
176         return u'\\' + u
177     elif u == u'\n' and not quote_newlines:
178         return u
179     if len(u) == 2:
180         codepoint = (ord(u[0])-0xD800)*0x400 + ord(u[1])-0xDC00 + 0x10000
181     else:
182         codepoint = ord(u)
183     if codepoint > 0xFFFF:
184         return u'\\U%08x' % (codepoint,)
185     elif codepoint > 0xFF:
186         return u'\\u%04x' % (codepoint,)
187     else:
188         return u'\\x%02x' % (codepoint,)
189
190 def _str_escape(m, quote_newlines):
191     c = m.group(0)
192     if c == '"' or c == '$' or c == '`' or c == '\\':
193         return '\\' + c
194     elif c == '\n' and not quote_newlines:
195         return c
196     else:
197         return '\\x%02x' % (ord(c),)
198
199 MUST_DOUBLE_QUOTE_NL = re.compile(ur'[^\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
200 MUST_DOUBLE_QUOTE    = re.compile(ur'[^\n\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
201
202 # if we must double-quote, then we have to escape ", $ and `, but need not escape '
203 ESCAPABLE_UNICODE = re.compile(ur'([\uD800-\uDBFF][\uDC00-\uDFFF])|'  # valid surrogate pairs
204                                ur'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]',
205                                re.DOTALL)
206
207 ESCAPABLE_8BIT    = re.compile( r'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', re.DOTALL)
208
209 def quote_output(s, quotemarks=True, quote_newlines=None, encoding=None):
210     """
211     Encode either a Unicode string or a UTF-8-encoded bytestring for representation
212     on stdout or stderr, tolerating errors. If 'quotemarks' is True, the string is
213     always quoted; otherwise, it is quoted only if necessary to avoid ambiguity or
214     control bytes in the output. (Newlines are counted as control bytes iff
215     quote_newlines is True.)
216
217     Quoting may use either single or double quotes. Within single quotes, all
218     characters stand for themselves, and ' will not appear. Within double quotes,
219     Python-compatible backslash escaping is used.
220
221     If not explicitly given, quote_newlines is True when quotemarks is True.
222     """
223     precondition(isinstance(s, (str, unicode)), s)
224     if quote_newlines is None:
225         quote_newlines = quotemarks
226
227     if isinstance(s, str):
228         try:
229             s = s.decode('utf-8')
230         except UnicodeDecodeError:
231             return 'b"%s"' % (ESCAPABLE_8BIT.sub(lambda m: _str_escape(m, quote_newlines), s),)
232
233     must_double_quote = quote_newlines and MUST_DOUBLE_QUOTE_NL or MUST_DOUBLE_QUOTE
234     if must_double_quote.search(s) is None:
235         try:
236             out = s.encode(encoding or io_encoding)
237             if quotemarks or out.startswith('"'):
238                 return "'%s'" % (out,)
239             else:
240                 return out
241         except (UnicodeDecodeError, UnicodeEncodeError):
242             pass
243
244     escaped = ESCAPABLE_UNICODE.sub(lambda m: _unicode_escape(m, quote_newlines), s)
245     return '"%s"' % (escaped.encode(encoding or io_encoding, 'backslashreplace'),)
246
247 def quote_path(path, quotemarks=True):
248     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks, quote_newlines=True)
249
250 def quote_local_unicode_path(path, quotemarks=True):
251     precondition(isinstance(path, unicode), path)
252
253     if sys.platform == "win32" and path.startswith(u"\\\\?\\"):
254         path = path[4 :]
255         if path.startswith(u"UNC\\"):
256             path = u"\\\\" + path[4 :]
257
258     return quote_output(path, quotemarks=quotemarks, quote_newlines=True)
259
260 def quote_filepath(path, quotemarks=True):
261     return quote_local_unicode_path(unicode_from_filepath(path), quotemarks=quotemarks)
262
263 def extend_filepath(fp, segments):
264     # We cannot use FilePath.preauthChild, because
265     # * it has the security flaw described in <https://twistedmatrix.com/trac/ticket/6527>;
266     # * it may return a FilePath in the wrong mode.
267
268     for segment in segments:
269         fp = fp.child(segment)
270
271     if isinstance(fp.path, unicode) and not use_unicode_filepath:
272         return FilePath(fp.path.encode(filesystem_encoding))
273     else:
274         return fp
275
276 def to_filepath(path):
277     precondition(isinstance(path, unicode if use_unicode_filepath else basestring),
278                  path=path)
279
280     if isinstance(path, unicode) and not use_unicode_filepath:
281         path = path.encode(filesystem_encoding)
282
283     if sys.platform == "win32":
284         _assert(isinstance(path, unicode), path=path)
285         if path.startswith(u"\\\\?\\") and len(path) > 4:
286             # FilePath normally strips trailing path separators, but not in this case.
287             path = path.rstrip(u"\\")
288
289     return FilePath(path)
290
291 def _decode(s):
292     precondition(isinstance(s, basestring), s=s)
293
294     if isinstance(s, bytes):
295         return s.decode(filesystem_encoding)
296     else:
297         return s
298
299 def unicode_from_filepath(fp):
300     precondition(isinstance(fp, FilePath), fp=fp)
301     return _decode(fp.path)
302
303 def unicode_segments_from(base_fp, ancestor_fp):
304     precondition(isinstance(base_fp, FilePath), base_fp=base_fp)
305     precondition(isinstance(ancestor_fp, FilePath), ancestor_fp=ancestor_fp)
306
307     return base_fp.asTextMode().segmentsFrom(ancestor_fp.asTextMode())
308
309 def unicode_platform():
310     """
311     Does the current platform handle Unicode filenames natively?
312     """
313     return is_unicode_platform
314
315 class FilenameEncodingError(Exception):
316     """
317     Filename cannot be encoded using the current encoding of your filesystem
318     (%s). Please configure your locale correctly or rename this file.
319     """
320     pass
321
322 def listdir_unicode_fallback(path):
323     """
324     This function emulates a fallback Unicode API similar to one available
325     under Windows or MacOS X.
326
327     If badly encoded filenames are encountered, an exception is raised.
328     """
329     precondition(isinstance(path, unicode), path)
330
331     try:
332         byte_path = path.encode(filesystem_encoding)
333     except (UnicodeEncodeError, UnicodeDecodeError):
334         raise FilenameEncodingError(path)
335
336     try:
337         return [unicode(fn, filesystem_encoding) for fn in os.listdir(byte_path)]
338     except UnicodeDecodeError:
339         raise FilenameEncodingError(fn)
340
341 def listdir_unicode(path):
342     """
343     Wrapper around listdir() which provides safe access to the convenient
344     Unicode API even under platforms that don't provide one natively.
345     """
346     precondition(isinstance(path, unicode), path)
347
348     # On Windows and MacOS X, the Unicode API is used
349     # On other platforms (ie. Unix systems), the byte-level API is used
350
351     if is_unicode_platform:
352         return os.listdir(path)
353     else:
354         return listdir_unicode_fallback(path)
355
356 def listdir_filepath(fp):
357     return listdir_unicode(unicode_from_filepath(fp))