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