]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/encodingutil.py
Add long_path=False option to abspath_expanduser_unicode.
[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, long_path=True):
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, long_path=long_path)
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, basestring), path=path)
278
279     if isinstance(path, unicode) and not use_unicode_filepath:
280         path = path.encode(filesystem_encoding)
281
282     return FilePath(path)
283
284 def _decode(s):
285     precondition(isinstance(s, basestring), s=s)
286
287     if isinstance(s, bytes):
288         return s.decode(filesystem_encoding)
289     else:
290         return s
291
292 def unicode_from_filepath(fp):
293     precondition(isinstance(fp, FilePath), fp=fp)
294     return _decode(fp.path)
295
296 def unicode_segments_from(base_fp, ancestor_fp):
297     precondition(isinstance(base_fp, FilePath), base_fp=base_fp)
298     precondition(isinstance(ancestor_fp, FilePath), ancestor_fp=ancestor_fp)
299
300     if hasattr(FilePath, 'asTextMode'):
301         return base_fp.asTextMode().segmentsFrom(ancestor_fp.asTextMode())
302     else:
303         bpt, apt = (type(base_fp.path), type(ancestor_fp.path))
304         _assert(bpt == apt, bpt=bpt, apt=apt)
305         return map(_decode, base_fp.segmentsFrom(ancestor_fp))
306
307 def unicode_platform():
308     """
309     Does the current platform handle Unicode filenames natively?
310     """
311     return is_unicode_platform
312
313 class FilenameEncodingError(Exception):
314     """
315     Filename cannot be encoded using the current encoding of your filesystem
316     (%s). Please configure your locale correctly or rename this file.
317     """
318     pass
319
320 def listdir_unicode_fallback(path):
321     """
322     This function emulates a fallback Unicode API similar to one available
323     under Windows or MacOS X.
324
325     If badly encoded filenames are encountered, an exception is raised.
326     """
327     precondition(isinstance(path, unicode), path)
328
329     try:
330         byte_path = path.encode(filesystem_encoding)
331     except (UnicodeEncodeError, UnicodeDecodeError):
332         raise FilenameEncodingError(path)
333
334     try:
335         return [unicode(fn, filesystem_encoding) for fn in os.listdir(byte_path)]
336     except UnicodeDecodeError:
337         raise FilenameEncodingError(fn)
338
339 def listdir_unicode(path):
340     """
341     Wrapper around listdir() which provides safe access to the convenient
342     Unicode API even under platforms that don't provide one natively.
343     """
344     precondition(isinstance(path, unicode), path)
345
346     # On Windows and MacOS X, the Unicode API is used
347     # On other platforms (ie. Unix systems), the byte-level API is used
348
349     if is_unicode_platform:
350         return os.listdir(path)
351     else:
352         return listdir_unicode_fallback(path)
353
354 def listdir_filepath(fp):
355     return listdir_unicode(unicode_from_filepath(fp))