]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/encodingutil.py
Teach magic-folder join to use argv_to_abspath
[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     if local_dir.startswith('-'):
99         raise usage.UsageError("Argument %s cannot start with a -." % (quote_output(s),))
100
101 def argv_to_abspath(s, **kwargs):
102     """
103     Convenience function to decode an argv element to an absolute path, with ~ expanded.
104     If this fails, raise a UsageError.
105     """
106     decoded = argv_to_unicode(s)
107     if decoded.startswith(u'-'):
108         raise usage.UsageError("Path argument %s cannot start with '-'.\nUse %s if you intended to refer to a file."
109                                % (quote_output(s), quote_output(os.path.join('.', s))))
110     return abspath_expanduser_unicode(decoded, **kwargs)
111
112 def unicode_to_argv(s, mangle=False):
113     """
114     Encode the given Unicode argument as a bytestring.
115     If the argument is to be passed to a different process, then the 'mangle' argument
116     should be true; on Windows, this uses a mangled encoding that will be reversed by
117     code in runner.py.
118     """
119     precondition(isinstance(s, unicode), s)
120
121     if mangle and sys.platform == "win32":
122         # This must be the same as 'mangle' in bin/tahoe-script.template.
123         return str(re.sub(ur'[^\x20-\x7F]', lambda m: u'\x7F%x;' % (ord(m.group(0)),), s))
124     else:
125         return s.encode(io_encoding)
126
127 def unicode_to_url(s):
128     """
129     Encode an unicode object used in an URL.
130     """
131     # According to RFC 2718, non-ascii characters in URLs must be UTF-8 encoded.
132
133     # FIXME
134     return to_str(s)
135     #precondition(isinstance(s, unicode), s)
136     #return s.encode('utf-8')
137
138 def to_str(s):
139     if s is None or isinstance(s, str):
140         return s
141     return s.encode('utf-8')
142
143 def from_utf8_or_none(s):
144     precondition(isinstance(s, (NoneType, str)), s)
145     if s is None:
146         return s
147     return s.decode('utf-8')
148
149 PRINTABLE_ASCII = re.compile(r'^[\n\r\x20-\x7E]*$',          re.DOTALL)
150 PRINTABLE_8BIT  = re.compile(r'^[\n\r\x20-\x7E\x80-\xFF]*$', re.DOTALL)
151
152 def is_printable_ascii(s):
153     return PRINTABLE_ASCII.search(s) is not None
154
155 def unicode_to_output(s):
156     """
157     Encode an unicode object for representation on stdout or stderr.
158     """
159     precondition(isinstance(s, unicode), s)
160
161     try:
162         out = s.encode(io_encoding)
163     except (UnicodeEncodeError, UnicodeDecodeError):
164         raise UnicodeEncodeError(io_encoding, s, 0, 0,
165                                  "A string could not be encoded as %s for output to the terminal:\n%r" %
166                                  (io_encoding, repr(s)))
167
168     if PRINTABLE_8BIT.search(out) is None:
169         raise UnicodeEncodeError(io_encoding, s, 0, 0,
170                                  "A string encoded as %s for output to the terminal contained unsafe bytes:\n%r" %
171                                  (io_encoding, repr(s)))
172     return out
173
174
175 def _unicode_escape(m, quote_newlines):
176     u = m.group(0)
177     if u == u'"' or u == u'$' or u == u'`' or u == u'\\':
178         return u'\\' + u
179     elif u == u'\n' and not quote_newlines:
180         return u
181     if len(u) == 2:
182         codepoint = (ord(u[0])-0xD800)*0x400 + ord(u[1])-0xDC00 + 0x10000
183     else:
184         codepoint = ord(u)
185     if codepoint > 0xFFFF:
186         return u'\\U%08x' % (codepoint,)
187     elif codepoint > 0xFF:
188         return u'\\u%04x' % (codepoint,)
189     else:
190         return u'\\x%02x' % (codepoint,)
191
192 def _str_escape(m, quote_newlines):
193     c = m.group(0)
194     if c == '"' or c == '$' or c == '`' or c == '\\':
195         return '\\' + c
196     elif c == '\n' and not quote_newlines:
197         return c
198     else:
199         return '\\x%02x' % (ord(c),)
200
201 MUST_DOUBLE_QUOTE_NL = re.compile(ur'[^\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
202 MUST_DOUBLE_QUOTE    = re.compile(ur'[^\n\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
203
204 # if we must double-quote, then we have to escape ", $ and `, but need not escape '
205 ESCAPABLE_UNICODE = re.compile(ur'([\uD800-\uDBFF][\uDC00-\uDFFF])|'  # valid surrogate pairs
206                                ur'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]',
207                                re.DOTALL)
208
209 ESCAPABLE_8BIT    = re.compile( r'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', re.DOTALL)
210
211 def quote_output(s, quotemarks=True, quote_newlines=None, encoding=None):
212     """
213     Encode either a Unicode string or a UTF-8-encoded bytestring for representation
214     on stdout or stderr, tolerating errors. If 'quotemarks' is True, the string is
215     always quoted; otherwise, it is quoted only if necessary to avoid ambiguity or
216     control bytes in the output. (Newlines are counted as control bytes iff
217     quote_newlines is True.)
218
219     Quoting may use either single or double quotes. Within single quotes, all
220     characters stand for themselves, and ' will not appear. Within double quotes,
221     Python-compatible backslash escaping is used.
222
223     If not explicitly given, quote_newlines is True when quotemarks is True.
224     """
225     precondition(isinstance(s, (str, unicode)), s)
226     if quote_newlines is None:
227         quote_newlines = quotemarks
228
229     if isinstance(s, str):
230         try:
231             s = s.decode('utf-8')
232         except UnicodeDecodeError:
233             return 'b"%s"' % (ESCAPABLE_8BIT.sub(lambda m: _str_escape(m, quote_newlines), s),)
234
235     must_double_quote = quote_newlines and MUST_DOUBLE_QUOTE_NL or MUST_DOUBLE_QUOTE
236     if must_double_quote.search(s) is None:
237         try:
238             out = s.encode(encoding or io_encoding)
239             if quotemarks or out.startswith('"'):
240                 return "'%s'" % (out,)
241             else:
242                 return out
243         except (UnicodeDecodeError, UnicodeEncodeError):
244             pass
245
246     escaped = ESCAPABLE_UNICODE.sub(lambda m: _unicode_escape(m, quote_newlines), s)
247     return '"%s"' % (escaped.encode(encoding or io_encoding, 'backslashreplace'),)
248
249 def quote_path(path, quotemarks=True):
250     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks, quote_newlines=True)
251
252 def quote_local_unicode_path(path, quotemarks=True):
253     precondition(isinstance(path, unicode), path)
254
255     if sys.platform == "win32" and path.startswith(u"\\\\?\\"):
256         path = path[4 :]
257         if path.startswith(u"UNC\\"):
258             path = u"\\\\" + path[4 :]
259
260     return quote_output(path, quotemarks=quotemarks, quote_newlines=True)
261
262 def quote_filepath(path, quotemarks=True):
263     return quote_local_unicode_path(unicode_from_filepath(path), quotemarks=quotemarks)
264
265 def extend_filepath(fp, segments):
266     # We cannot use FilePath.preauthChild, because
267     # * it has the security flaw described in <https://twistedmatrix.com/trac/ticket/6527>;
268     # * it may return a FilePath in the wrong mode.
269
270     for segment in segments:
271         fp = fp.child(segment)
272
273     if isinstance(fp.path, unicode) and not use_unicode_filepath:
274         return FilePath(fp.path.encode(filesystem_encoding))
275     else:
276         return fp
277
278 def to_filepath(path):
279     precondition(isinstance(path, basestring), path=path)
280
281     if isinstance(path, unicode) and not use_unicode_filepath:
282         path = path.encode(filesystem_encoding)
283
284     return FilePath(path)
285
286 def _decode(s):
287     precondition(isinstance(s, basestring), s=s)
288
289     if isinstance(s, bytes):
290         return s.decode(filesystem_encoding)
291     else:
292         return s
293
294 def unicode_from_filepath(fp):
295     precondition(isinstance(fp, FilePath), fp=fp)
296     return _decode(fp.path)
297
298 def unicode_segments_from(base_fp, ancestor_fp):
299     precondition(isinstance(base_fp, FilePath), base_fp=base_fp)
300     precondition(isinstance(ancestor_fp, FilePath), ancestor_fp=ancestor_fp)
301
302     if hasattr(FilePath, 'asTextMode'):
303         return base_fp.asTextMode().segmentsFrom(ancestor_fp.asTextMode())
304     else:
305         bpt, apt = (type(base_fp.path), type(ancestor_fp.path))
306         _assert(bpt == apt, bpt=bpt, apt=apt)
307         return map(_decode, base_fp.segmentsFrom(ancestor_fp))
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))