]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/util/encodingutil.py
d14b08f6be448782a454fefd1da22d6d8e9e45eb
[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):
92     """
93     Convenience function to decode an argv element to an absolute path, with ~ expanded.
94     If this fails, raise a UsageError.
95     """
96     return abspath_expanduser_unicode(argv_to_unicode(s))
97
98 def unicode_to_argv(s, mangle=False):
99     """
100     Encode the given Unicode argument as a bytestring.
101     If the argument is to be passed to a different process, then the 'mangle' argument
102     should be true; on Windows, this uses a mangled encoding that will be reversed by
103     code in runner.py.
104     """
105     precondition(isinstance(s, unicode), s)
106
107     if mangle and sys.platform == "win32":
108         # This must be the same as 'mangle' in bin/tahoe-script.template.
109         return str(re.sub(ur'[^\x20-\x7F]', lambda m: u'\x7F%x;' % (ord(m.group(0)),), s))
110     else:
111         return s.encode(io_encoding)
112
113 def unicode_to_url(s):
114     """
115     Encode an unicode object used in an URL.
116     """
117     # According to RFC 2718, non-ascii characters in URLs must be UTF-8 encoded.
118
119     # FIXME
120     return to_str(s)
121     #precondition(isinstance(s, unicode), s)
122     #return s.encode('utf-8')
123
124 def to_str(s):
125     if s is None or isinstance(s, str):
126         return s
127     return s.encode('utf-8')
128
129 def from_utf8_or_none(s):
130     precondition(isinstance(s, (NoneType, str)), s)
131     if s is None:
132         return s
133     return s.decode('utf-8')
134
135 PRINTABLE_ASCII = re.compile(r'^[\n\r\x20-\x7E]*$',          re.DOTALL)
136 PRINTABLE_8BIT  = re.compile(r'^[\n\r\x20-\x7E\x80-\xFF]*$', re.DOTALL)
137
138 def is_printable_ascii(s):
139     return PRINTABLE_ASCII.search(s) is not None
140
141 def unicode_to_output(s):
142     """
143     Encode an unicode object for representation on stdout or stderr.
144     """
145     precondition(isinstance(s, unicode), s)
146
147     try:
148         out = s.encode(io_encoding)
149     except (UnicodeEncodeError, UnicodeDecodeError):
150         raise UnicodeEncodeError(io_encoding, s, 0, 0,
151                                  "A string could not be encoded as %s for output to the terminal:\n%r" %
152                                  (io_encoding, repr(s)))
153
154     if PRINTABLE_8BIT.search(out) is None:
155         raise UnicodeEncodeError(io_encoding, s, 0, 0,
156                                  "A string encoded as %s for output to the terminal contained unsafe bytes:\n%r" %
157                                  (io_encoding, repr(s)))
158     return out
159
160
161 def _unicode_escape(m, quote_newlines):
162     u = m.group(0)
163     if u == u'"' or u == u'$' or u == u'`' or u == u'\\':
164         return u'\\' + u
165     elif u == u'\n' and not quote_newlines:
166         return u
167     if len(u) == 2:
168         codepoint = (ord(u[0])-0xD800)*0x400 + ord(u[1])-0xDC00 + 0x10000
169     else:
170         codepoint = ord(u)
171     if codepoint > 0xFFFF:
172         return u'\\U%08x' % (codepoint,)
173     elif codepoint > 0xFF:
174         return u'\\u%04x' % (codepoint,)
175     else:
176         return u'\\x%02x' % (codepoint,)
177
178 def _str_escape(m, quote_newlines):
179     c = m.group(0)
180     if c == '"' or c == '$' or c == '`' or c == '\\':
181         return '\\' + c
182     elif c == '\n' and not quote_newlines:
183         return c
184     else:
185         return '\\x%02x' % (ord(c),)
186
187 MUST_DOUBLE_QUOTE_NL = re.compile(ur'[^\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
188 MUST_DOUBLE_QUOTE    = re.compile(ur'[^\n\x20-\x26\x28-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]', re.DOTALL)
189
190 # if we must double-quote, then we have to escape ", $ and `, but need not escape '
191 ESCAPABLE_UNICODE = re.compile(ur'([\uD800-\uDBFF][\uDC00-\uDFFF])|'  # valid surrogate pairs
192                                ur'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E\u00A0-\uD7FF\uE000-\uFDCF\uFDF0-\uFFFC]',
193                                re.DOTALL)
194
195 ESCAPABLE_8BIT    = re.compile( r'[^ !#\x25-\x5B\x5D-\x5F\x61-\x7E]', re.DOTALL)
196
197 def quote_output(s, quotemarks=True, quote_newlines=None, encoding=None):
198     """
199     Encode either a Unicode string or a UTF-8-encoded bytestring for representation
200     on stdout or stderr, tolerating errors. If 'quotemarks' is True, the string is
201     always quoted; otherwise, it is quoted only if necessary to avoid ambiguity or
202     control bytes in the output. (Newlines are counted as control bytes iff
203     quote_newlines is True.)
204
205     Quoting may use either single or double quotes. Within single quotes, all
206     characters stand for themselves, and ' will not appear. Within double quotes,
207     Python-compatible backslash escaping is used.
208
209     If not explicitly given, quote_newlines is True when quotemarks is True.
210     """
211     precondition(isinstance(s, (str, unicode)), s)
212     if quote_newlines is None:
213         quote_newlines = quotemarks
214
215     if isinstance(s, str):
216         try:
217             s = s.decode('utf-8')
218         except UnicodeDecodeError:
219             return 'b"%s"' % (ESCAPABLE_8BIT.sub(lambda m: _str_escape(m, quote_newlines), s),)
220
221     must_double_quote = quote_newlines and MUST_DOUBLE_QUOTE_NL or MUST_DOUBLE_QUOTE
222     if must_double_quote.search(s) is None:
223         try:
224             out = s.encode(encoding or io_encoding)
225             if quotemarks or out.startswith('"'):
226                 return "'%s'" % (out,)
227             else:
228                 return out
229         except (UnicodeDecodeError, UnicodeEncodeError):
230             pass
231
232     escaped = ESCAPABLE_UNICODE.sub(lambda m: _unicode_escape(m, quote_newlines), s)
233     return '"%s"' % (escaped.encode(encoding or io_encoding, 'backslashreplace'),)
234
235 def quote_path(path, quotemarks=True):
236     return quote_output("/".join(map(to_str, path)), quotemarks=quotemarks, quote_newlines=True)
237
238 def quote_local_unicode_path(path, quotemarks=True):
239     precondition(isinstance(path, unicode), path)
240
241     if sys.platform == "win32" and path.startswith(u"\\\\?\\"):
242         path = path[4 :]
243         if path.startswith(u"UNC\\"):
244             path = u"\\\\" + path[4 :]
245
246     return quote_output(path, quotemarks=quotemarks, quote_newlines=True)
247
248
249 def unicode_platform():
250     """
251     Does the current platform handle Unicode filenames natively?
252     """
253     return is_unicode_platform
254
255 class FilenameEncodingError(Exception):
256     """
257     Filename cannot be encoded using the current encoding of your filesystem
258     (%s). Please configure your locale correctly or rename this file.
259     """
260     pass
261
262 def listdir_unicode_fallback(path):
263     """
264     This function emulates a fallback Unicode API similar to one available
265     under Windows or MacOS X.
266
267     If badly encoded filenames are encountered, an exception is raised.
268     """
269     precondition(isinstance(path, unicode), path)
270
271     try:
272         byte_path = path.encode(filesystem_encoding)
273     except (UnicodeEncodeError, UnicodeDecodeError):
274         raise FilenameEncodingError(path)
275
276     try:
277         return [unicode(fn, filesystem_encoding) for fn in os.listdir(byte_path)]
278     except UnicodeDecodeError:
279         raise FilenameEncodingError(fn)
280
281 def listdir_unicode(path):
282     """
283     Wrapper around listdir() which provides safe access to the convenient
284     Unicode API even under platforms that don't provide one natively.
285     """
286     precondition(isinstance(path, unicode), path)
287
288     # On Windows and MacOS X, the Unicode API is used
289     # On other platforms (ie. Unix systems), the byte-level API is used
290
291     if is_unicode_platform:
292         return os.listdir(path)
293     else:
294         return listdir_unicode_fallback(path)