/breezy/unstable

To get this branch, use:
bzr branch https://code.breezy-vcs.org/breezy/unstable

« back to all changes in this revision

Viewing changes to breezy/urlutils.py

  • Committer: Jelmer Vernooij
  • Date: 2017-05-24 01:39:33 UTC
  • mfrom: (3815.3776.6)
  • Revision ID: jelmer@jelmer.uk-20170524013933-ir4y4tqtrsiz2ka2
New upstream snapshot.

Show diffs side-by-side

added added

removed removed

Lines of Context:
22
22
import re
23
23
import sys
24
24
 
25
 
from bzrlib.lazy_import import lazy_import
 
25
try:
 
26
    import urlparse
 
27
except ImportError:
 
28
    from urllib import parse as urlparse
 
29
 
 
30
from .lazy_import import lazy_import
26
31
lazy_import(globals(), """
27
32
from posixpath import split as _posix_split
28
 
import urlparse
29
33
 
30
 
from bzrlib import (
 
34
from breezy import (
31
35
    errors,
32
36
    osutils,
33
37
    )
34
38
""")
35
39
 
 
40
from .sixish import (
 
41
    text_type,
 
42
    )
 
43
 
36
44
 
37
45
def basename(url, exclude_trailing_slash=True):
38
46
    """Return the last component of a URL.
65
73
# urllib module because urllib unconditionally imports socket, which imports
66
74
# ssl.
67
75
 
68
 
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
69
 
               'abcdefghijklmnopqrstuvwxyz'
70
 
               '0123456789' '_.-')
 
76
always_safe = (b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
77
               b'abcdefghijklmnopqrstuvwxyz'
 
78
               b'0123456789' b'_.-')
71
79
_safe_map = {}
72
 
for i, c in zip(xrange(256), str(bytearray(xrange(256)))):
73
 
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
 
80
for i, c in zip(range(256), bytes(bytearray(range(256)))):
 
81
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i).encode('ascii')
74
82
_safe_quoters = {}
75
83
 
76
84
 
77
 
def quote(s, safe='/'):
 
85
def quote(s, safe=b'/'):
78
86
    """quote('abc def') -> 'abc%20def'
79
87
 
80
88
    Each part of a URL, e.g. the path info, the query, etc., has a
111
119
        _safe_quoters[cachekey] = (quoter, safe)
112
120
    if not s.rstrip(safe):
113
121
        return s
114
 
    return ''.join(map(quoter, s))
 
122
    return b''.join(map(quoter, s))
115
123
 
116
124
 
117
125
_hexdig = '0123456789ABCDEFabcdef'
120
128
 
121
129
def unquote(s):
122
130
    """unquote('abc%20def') -> 'abc def'."""
123
 
    res = s.split('%')
 
131
    res = s.split(b'%')
124
132
    # fastpath
125
133
    if len(res) == 1:
126
134
        return s
129
137
        try:
130
138
            s += _hextochr[item[:2]] + item[2:]
131
139
        except KeyError:
132
 
            s += '%' + item
 
140
            s += b'%' + item
133
141
        except UnicodeDecodeError:
134
142
            s += unichr(int(item[:2], 16)) + item[2:]
135
143
    return s
137
145
 
138
146
def escape(relpath):
139
147
    """Escape relpath to be a valid url."""
140
 
    if isinstance(relpath, unicode):
 
148
    if isinstance(relpath, text_type):
141
149
        relpath = relpath.encode('utf-8')
142
 
    # After quoting and encoding, the path should be perfectly
143
 
    # safe as a plain ASCII string, str() just enforces this
144
 
    return str(quote(relpath, safe='/~'))
 
150
    return quote(relpath, safe=b'/~')
145
151
 
146
152
 
147
153
def file_relpath(base, path):
173
179
 
174
180
    # Find the path separating slash
175
181
    # (first slash after the ://)
176
 
    first_path_slash = path.find('/')
 
182
    first_path_slash = path.find(b'/')
177
183
    if first_path_slash == -1:
178
184
        return len(scheme), None
179
185
    return len(scheme), first_path_slash+m.start('path')
230
236
    We really should try to have exactly one place in the code base responsible
231
237
    for combining paths of URLs.
232
238
    """
233
 
    path = base.split('/')
234
 
    if len(path) > 1 and path[-1] == '':
 
239
    path = base.split(b'/')
 
240
    if len(path) > 1 and path[-1] == b'':
235
241
        #If the path ends in a trailing /, remove it.
236
242
        path.pop()
237
243
    for arg in args:
238
 
        if arg.startswith('/'):
 
244
        if arg.startswith(b'/'):
239
245
            path = []
240
 
        for chunk in arg.split('/'):
241
 
            if chunk == '.':
 
246
        for chunk in arg.split(b'/'):
 
247
            if chunk == b'.':
242
248
                continue
243
 
            elif chunk == '..':
244
 
                if path == ['']:
 
249
            elif chunk == b'..':
 
250
                if path == [b'']:
245
251
                    raise errors.InvalidURLJoin('Cannot go above root',
246
252
                            base, args)
247
253
                path.pop()
248
254
            else:
249
255
                path.append(chunk)
250
 
    if path == ['']:
251
 
        return '/'
 
256
    if path == [b'']:
 
257
        return b'/'
252
258
    else:
253
 
        return '/'.join(path)
 
259
        return b'/'.join(path)
254
260
 
255
261
 
256
262
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
257
263
def _posix_local_path_from_url(url):
258
264
    """Convert a url like file:///path/to/foo into /path/to/foo"""
259
265
    url = split_segment_parameters_raw(url)[0]
260
 
    file_localhost_prefix = 'file://localhost/'
 
266
    file_localhost_prefix = b'file://localhost/'
261
267
    if url.startswith(file_localhost_prefix):
262
268
        path = url[len(file_localhost_prefix) - 1:]
263
 
    elif not url.startswith('file:///'):
 
269
    elif not url.startswith(b'file:///'):
264
270
        raise errors.InvalidURL(
265
271
            url, 'local urls must start with file:/// or file://localhost/')
266
272
    else:
267
 
        path = url[len('file://'):]
 
273
        path = url[len(b'file://'):]
268
274
    # We only strip off 2 slashes
269
275
    return unescape(path)
270
276
 
276
282
    """
277
283
    # importing directly from posixpath allows us to test this
278
284
    # on non-posix platforms
279
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
285
    return b'file://' + escape(osutils._posix_abspath(path))
280
286
 
281
287
 
282
288
def _win32_local_path_from_url(url):
344
350
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
345
351
 
346
352
 
347
 
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
348
 
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
 
353
_url_scheme_re = re.compile(b'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
 
354
_url_hex_escapes_re = re.compile(b'(%[0-9a-fA-F]{2})')
349
355
 
350
356
 
351
357
def _unescape_safe_chars(matchobj):
521
527
    #                should not be blindly adding slashes in the first place. 
522
528
    lurl = strip_trailing_slash(url)
523
529
    # Segments begin at first comma after last forward slash, if one exists
524
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
 
530
    segment_start = lurl.find(b",", lurl.rfind(b"/")+1)
525
531
    if segment_start == -1:
526
532
        return (url, [])
527
 
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
533
    return (lurl[:segment_start], lurl[segment_start+1:].split(b","))
528
534
 
529
535
 
530
536
def split_segment_parameters(url):
552
558
    if not subsegments:
553
559
        return base
554
560
    for subsegment in subsegments:
555
 
        if type(subsegment) is not str:
 
561
        if not isinstance(subsegment, str):
556
562
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
557
563
        if "," in subsegment:
558
564
            raise errors.InvalidURLJoin(", exists in subsegments",
573
579
    new_parameters = {}
574
580
    new_parameters.update(existing_parameters)
575
581
    for key, value in parameters.iteritems():
576
 
        if type(key) is not str:
 
582
        if not isinstance(key, str):
577
583
            raise TypeError("parameter key %r is not a bytestring" % key)
578
 
        if type(value) is not str:
 
584
        if not isinstance(value, str):
579
585
            raise TypeError("parameter value %r for %s is not a bytestring" %
580
586
                (key, value))
581
587
        if "=" in key:
615
621
        # format which does it differently.
616
622
        file:///c|/       => file:///c:/
617
623
    """
618
 
    if not url.endswith('/'):
 
624
    if not url.endswith(b'/'):
619
625
        # Nothing to do
620
626
        return url
621
 
    if sys.platform == 'win32' and url.startswith('file://'):
 
627
    if sys.platform == 'win32' and url.startswith(b'file://'):
622
628
        return _win32_strip_local_trailing_slash(url)
623
629
 
624
630
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
647
653
    #       plain ASCII strings, or the final .decode will
648
654
    #       try to encode the UNICODE => ASCII, and then decode
649
655
    #       it into utf-8.
650
 
    try:
651
 
        url = str(url)
652
 
    except UnicodeError, e:
653
 
        raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
 
656
    if isinstance(url, text_type):
 
657
        try:
 
658
            url = url.encode("ascii")
 
659
        except UnicodeError as e:
 
660
            raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
654
661
 
655
662
    unquoted = unquote(url)
656
663
    try:
657
664
        unicode_path = unquoted.decode('utf-8')
658
 
    except UnicodeError, e:
 
665
    except UnicodeError as e:
659
666
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
660
667
    return unicode_path
661
668