/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""A collection of function for handling URL operations."""
18
18
 
19
 
from __future__ import absolute_import
20
 
 
21
19
import os
22
20
import re
23
21
import sys
24
22
 
25
 
try:
26
 
    import urlparse
27
 
except ImportError:
28
 
    from urllib import parse as urlparse
 
23
from bzrlib.lazy_import import lazy_import
 
24
lazy_import(globals(), """
 
25
from posixpath import split as _posix_split, normpath as _posix_normpath
 
26
import urllib
 
27
import urlparse
29
28
 
30
 
from . import (
 
29
from bzrlib import (
31
30
    errors,
32
31
    osutils,
33
32
    )
34
 
 
35
 
from .lazy_import import lazy_import
36
 
lazy_import(globals(), """
37
 
from posixpath import split as _posix_split
38
33
""")
39
34
 
40
 
from .sixish import (
41
 
    int2byte,
42
 
    PY3,
43
 
    text_type,
44
 
    )
45
 
 
46
 
 
47
 
class InvalidURL(errors.PathError):
48
 
 
49
 
    _fmt = 'Invalid url supplied to transport: "%(path)s"%(extra)s'
50
 
 
51
 
 
52
 
class InvalidURLJoin(errors.PathError):
53
 
 
54
 
    _fmt = "Invalid URL join request: %(reason)s: %(base)r + %(join_args)r"
55
 
 
56
 
    def __init__(self, reason, base, join_args):
57
 
        self.reason = reason
58
 
        self.base = base
59
 
        self.join_args = join_args
60
 
        errors.PathError.__init__(self, base, reason)
61
 
 
62
 
 
63
 
class InvalidRebaseURLs(errors.PathError):
64
 
 
65
 
    _fmt = "URLs differ by more than path: %(from_)r and %(to)r"
66
 
 
67
 
    def __init__(self, from_, to):
68
 
        self.from_ = from_
69
 
        self.to = to
70
 
        errors.PathError.__init__(self, from_, 'URLs differ by more than path.')
71
 
 
72
35
 
73
36
def basename(url, exclude_trailing_slash=True):
74
37
    """Return the last component of a URL.
97
60
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
98
61
 
99
62
 
100
 
if PY3:
101
 
    quote_from_bytes = urlparse.quote_from_bytes
102
 
    quote = urlparse.quote
103
 
    unquote_to_bytes = urlparse.unquote_to_bytes
104
 
else:
105
 
    # Private copies of quote and unquote, copied from Python's
106
 
    # urllib module because urllib unconditionally imports socket, which imports
107
 
    # ssl.
108
 
 
109
 
    always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
110
 
                   'abcdefghijklmnopqrstuvwxyz'
111
 
                   '0123456789' '_.-')
112
 
    _safe_map = {}
113
 
    for i, c in zip(range(256), ''.join(map(chr, range(256)))):
114
 
        _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
115
 
    _safe_quoters = {}
116
 
 
117
 
    def quote_from_bytes(s, safe='/'):
118
 
        """quote('abc def') -> 'abc%20def'
119
 
 
120
 
        Each part of a URL, e.g. the path info, the query, etc., has a
121
 
        different set of reserved characters that must be quoted.
122
 
 
123
 
        RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
124
 
        the following reserved characters.
125
 
 
126
 
        reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
127
 
                      "$" | ","
128
 
 
129
 
        Each of these characters is reserved in some component of a URL,
130
 
        but not necessarily in all of them.
131
 
 
132
 
        By default, the quote function is intended for quoting the path
133
 
        section of a URL.  Thus, it will not encode '/'.  This character
134
 
        is reserved, but in typical usage the quote function is being
135
 
        called on a path where the existing slash characters are used as
136
 
        reserved characters.
137
 
        """
138
 
        # fastpath
139
 
        if not s:
140
 
            if s is None:
141
 
                raise TypeError('None object cannot be quoted')
142
 
            return s
143
 
        cachekey = (safe, always_safe)
144
 
        try:
145
 
            (quoter, safe) = _safe_quoters[cachekey]
146
 
        except KeyError:
147
 
            safe_map = _safe_map.copy()
148
 
            safe_map.update([(c, c) for c in safe])
149
 
            quoter = safe_map.__getitem__
150
 
            safe = always_safe + safe
151
 
            _safe_quoters[cachekey] = (quoter, safe)
152
 
        if not s.rstrip(safe):
153
 
            return s
154
 
        return ''.join(map(quoter, s))
155
 
 
156
 
    quote = quote_from_bytes
157
 
    unquote_to_bytes = urlparse.unquote
158
 
 
159
 
 
160
 
unquote = urlparse.unquote
161
 
 
162
 
 
163
63
def escape(relpath):
164
64
    """Escape relpath to be a valid url."""
165
 
    if not isinstance(relpath, str) and sys.version_info[0] == 2:
 
65
    if isinstance(relpath, unicode):
166
66
        relpath = relpath.encode('utf-8')
167
 
    return quote(relpath, safe='/~')
 
67
    # After quoting and encoding, the path should be perfectly
 
68
    # safe as a plain ASCII string, str() just enforces this
 
69
    return str(urllib.quote(relpath, safe='/~'))
168
70
 
169
71
 
170
72
def file_relpath(base, path):
176
78
        raise ValueError('Length of base (%r) must equal or'
177
79
            ' exceed the platform minimum url length (which is %d)' %
178
80
            (base, MIN_ABS_FILEURL_LENGTH))
179
 
    base = osutils.normpath(local_path_from_url(base))
180
 
    path = osutils.normpath(local_path_from_url(path))
 
81
    base = local_path_from_url(base)
 
82
    path = local_path_from_url(path)
181
83
    return escape(osutils.relpath(base, path))
182
84
 
183
85
 
199
101
    first_path_slash = path.find('/')
200
102
    if first_path_slash == -1:
201
103
        return len(scheme), None
202
 
    return len(scheme), first_path_slash+m.start('path')
203
 
 
204
 
 
205
 
def is_url(url):
206
 
    """Tests whether a URL is in actual fact a URL."""
207
 
    return _url_scheme_re.match(url) is not None
 
104
    return len(scheme), first_path_slash+len(scheme)+3
208
105
 
209
106
 
210
107
def join(base, *args):
221
118
    """
222
119
    if not args:
223
120
        return base
224
 
    scheme_end, path_start = _find_scheme_and_separator(base)
225
 
    if scheme_end is None and path_start is None:
226
 
        path_start = 0
227
 
    elif path_start is None:
228
 
        path_start = len(base)
229
 
    path = base[path_start:]
 
121
    match = _url_scheme_re.match(base)
 
122
    scheme = None
 
123
    if match:
 
124
        scheme = match.group('scheme')
 
125
        path = match.group('path').split('/')
 
126
        if path[-1:] == ['']:
 
127
            # Strip off a trailing slash
 
128
            # This helps both when we are at the root, and when
 
129
            # 'base' has an extra slash at the end
 
130
            path = path[:-1]
 
131
    else:
 
132
        path = base.split('/')
 
133
 
 
134
    if scheme is not None and len(path) >= 1:
 
135
        host = path[:1]
 
136
        # the path should be represented as an abs path.
 
137
        # we know this must be absolute because of the presence of a URL scheme.
 
138
        remove_root = True
 
139
        path = [''] + path[1:]
 
140
    else:
 
141
        # create an empty host, but dont alter the path - this might be a
 
142
        # relative url fragment.
 
143
        host = []
 
144
        remove_root = False
 
145
 
230
146
    for arg in args:
231
 
        arg_scheme_end, arg_path_start = _find_scheme_and_separator(arg)
232
 
        if arg_scheme_end is None and arg_path_start is None:
233
 
            arg_path_start = 0
234
 
        elif arg_path_start is None:
235
 
            arg_path_start = len(arg)
236
 
        if arg_scheme_end is not None:
237
 
            base = arg
238
 
            path = arg[arg_path_start:]
239
 
            scheme_end = arg_scheme_end
240
 
            path_start = arg_path_start
 
147
        match = _url_scheme_re.match(arg)
 
148
        if match:
 
149
            # Absolute URL
 
150
            scheme = match.group('scheme')
 
151
            # this skips .. normalisation, making http://host/../../..
 
152
            # be rather strange.
 
153
            path = match.group('path').split('/')
 
154
            # set the host and path according to new absolute URL, discarding
 
155
            # any previous values.
 
156
            # XXX: duplicates mess from earlier in this function.  This URL
 
157
            # manipulation code needs some cleaning up.
 
158
            if scheme is not None and len(path) >= 1:
 
159
                host = path[:1]
 
160
                path = path[1:]
 
161
                # url scheme implies absolute path.
 
162
                path = [''] + path
 
163
            else:
 
164
                # no url scheme we take the path as is.
 
165
                host = []
241
166
        else:
 
167
            path = '/'.join(path)
242
168
            path = joinpath(path, arg)
243
 
    return base[:path_start] + path
 
169
            path = path.split('/')
 
170
    if remove_root and path[0:1] == ['']:
 
171
        del path[0]
 
172
    if host:
 
173
        # Remove the leading slash from the path, so long as it isn't also the
 
174
        # trailing slash, which we want to keep if present.
 
175
        if path and path[0] == '' and len(path) > 1:
 
176
            del path[0]
 
177
        path = host + path
 
178
 
 
179
    if scheme is None:
 
180
        return '/'.join(path)
 
181
    return scheme + '://' + '/'.join(path)
244
182
 
245
183
 
246
184
def joinpath(base, *args):
265
203
                continue
266
204
            elif chunk == '..':
267
205
                if path == ['']:
268
 
                    raise InvalidURLJoin('Cannot go above root',
 
206
                    raise errors.InvalidURLJoin('Cannot go above root',
269
207
                            base, args)
270
208
                path.pop()
271
209
            else:
279
217
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
280
218
def _posix_local_path_from_url(url):
281
219
    """Convert a url like file:///path/to/foo into /path/to/foo"""
282
 
    url = split_segment_parameters_raw(url)[0]
283
220
    file_localhost_prefix = 'file://localhost/'
284
221
    if url.startswith(file_localhost_prefix):
285
222
        path = url[len(file_localhost_prefix) - 1:]
286
223
    elif not url.startswith('file:///'):
287
 
        raise InvalidURL(
 
224
        raise errors.InvalidURL(
288
225
            url, 'local urls must start with file:/// or file://localhost/')
289
226
    else:
290
227
        path = url[len('file://'):]
299
236
    """
300
237
    # importing directly from posixpath allows us to test this
301
238
    # on non-posix platforms
302
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
239
    return 'file://' + escape(_posix_normpath(
 
240
        osutils._posix_abspath(path)))
303
241
 
304
242
 
305
243
def _win32_local_path_from_url(url):
306
244
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
307
245
    if not url.startswith('file://'):
308
 
        raise InvalidURL(url, 'local urls must start with file:///, '
 
246
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
309
247
                                     'UNC path urls must start with file://')
310
 
    url = split_segment_parameters_raw(url)[0]
311
248
    # We strip off all 3 slashes
312
249
    win32_url = url[len('file:'):]
313
250
    # check for UNC path: //HOST/path
314
251
    if not win32_url.startswith('///'):
315
252
        if (win32_url[2] == '/'
316
253
            or win32_url[3] in '|:'):
317
 
            raise InvalidURL(url, 'Win32 UNC path urls'
 
254
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
318
255
                ' have form file://HOST/path')
319
256
        return unescape(win32_url)
320
257
 
323
260
        return '/'
324
261
 
325
262
    # usual local path with drive letter
326
 
    if (len(win32_url) < 6
327
 
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
328
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
263
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
264
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
329
265
        or win32_url[4] not in  '|:'
330
266
        or win32_url[5] != '/'):
331
 
        raise InvalidURL(url, 'Win32 file urls start with'
 
267
        raise errors.InvalidURL(url, 'Win32 file urls start with'
332
268
                ' file:///x:/, where x is a valid drive letter')
333
269
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
334
270
 
342
278
    # on non-win32 platform
343
279
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
344
280
    #       which actually strips trailing space characters.
345
 
    #       The worst part is that on linux ntpath.abspath has different
 
281
    #       The worst part is that under linux ntpath.abspath has different
346
282
    #       semantics, since 'nt' is not an available module.
347
283
    if path == '/':
348
284
        return 'file:///'
367
303
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
368
304
 
369
305
 
370
 
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
371
 
_url_hex_escapes_re = re.compile('(%[0-9a-fA-F]{2})')
 
306
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,})://(?P<path>.*)$')
 
307
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
372
308
 
373
309
 
374
310
def _unescape_safe_chars(matchobj):
403
339
    :param url: Either a hybrid URL or a local path
404
340
    :return: A normalized URL which only includes 7-bit ASCII characters.
405
341
    """
406
 
    scheme_end, path_start = _find_scheme_and_separator(url)
407
 
    if scheme_end is None:
 
342
    m = _url_scheme_re.match(url)
 
343
    if not m:
408
344
        return local_path_to_url(url)
409
 
    prefix = url[:path_start]
410
 
    path = url[path_start:]
411
 
    if not isinstance(url, text_type):
 
345
    scheme = m.group('scheme')
 
346
    path = m.group('path')
 
347
    if not isinstance(url, unicode):
412
348
        for c in url:
413
349
            if c not in _url_safe_characters:
414
 
                raise InvalidURL(url, 'URLs can only contain specific'
 
350
                raise errors.InvalidURL(url, 'URLs can only contain specific'
415
351
                                            ' safe characters (not %r)' % c)
416
352
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
417
 
        return str(prefix + ''.join(path))
 
353
        return str(scheme + '://' + ''.join(path))
418
354
 
419
355
    # We have a unicode (hybrid) url
420
356
    path_chars = list(path)
421
357
 
422
 
    for i in range(len(path_chars)):
 
358
    for i in xrange(len(path_chars)):
423
359
        if path_chars[i] not in _url_safe_characters:
424
360
            chars = path_chars[i].encode('utf-8')
425
361
            path_chars[i] = ''.join(
426
 
                ['%%%02X' % c for c in bytearray(path_chars[i].encode('utf-8'))])
 
362
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
427
363
    path = ''.join(path_chars)
428
364
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
429
 
    return str(prefix + path)
 
365
    return str(scheme + '://' + path)
430
366
 
431
367
 
432
368
def relative_url(base, other):
485
421
    """On win32 the drive letter needs to be added to the url base."""
486
422
    # Strip off the drive letter
487
423
    # path is currently /C:/foo
488
 
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
489
 
        raise InvalidURL(url_base + path,
 
424
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
 
425
        raise errors.InvalidURL(url_base + path,
490
426
            'win32 file:/// paths need a drive letter')
491
427
    url_base += path[0:3] # file:// + /C:
492
428
    path = path[3:] # /foo
533
469
    return url_base + head, tail
534
470
 
535
471
 
536
 
def split_segment_parameters_raw(url):
537
 
    """Split the subsegment of the last segment of a URL.
538
 
 
539
 
    :param url: A relative or absolute URL
540
 
    :return: (url, subsegments)
541
 
    """
542
 
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
543
 
    #                operates on urls not url+segments, and Transport classes
544
 
    #                should not be blindly adding slashes in the first place. 
545
 
    lurl = strip_trailing_slash(url)
546
 
    # Segments begin at first comma after last forward slash, if one exists
547
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
548
 
    if segment_start == -1:
549
 
        return (url, [])
550
 
    return (lurl[:segment_start], [str(s) for s in lurl[segment_start+1:].split(",")])
551
 
 
552
 
 
553
 
def split_segment_parameters(url):
554
 
    """Split the segment parameters of the last segment of a URL.
555
 
 
556
 
    :param url: A relative or absolute URL
557
 
    :return: (url, segment_parameters)
558
 
    """
559
 
    (base_url, subsegments) = split_segment_parameters_raw(url)
560
 
    parameters = {}
561
 
    for subsegment in subsegments:
562
 
        (key, value) = subsegment.split("=", 1)
563
 
        if not isinstance(key, str):
564
 
            raise TypeError(key)
565
 
        if not isinstance(value, str):
566
 
            raise TypeError(value)
567
 
        parameters[key] = value
568
 
    return (base_url, parameters)
569
 
 
570
 
 
571
 
def join_segment_parameters_raw(base, *subsegments):
572
 
    """Create a new URL by adding subsegments to an existing one. 
573
 
 
574
 
    This adds the specified subsegments to the last path in the specified
575
 
    base URL. The subsegments should be bytestrings.
576
 
 
577
 
    :note: You probably want to use join_segment_parameters instead.
578
 
    """
579
 
    if not subsegments:
580
 
        return base
581
 
    for subsegment in subsegments:
582
 
        if not isinstance(subsegment, str):
583
 
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
584
 
        if "," in subsegment:
585
 
            raise InvalidURLJoin(", exists in subsegments",
586
 
                                        base, subsegments)
587
 
    return ",".join((base,) + subsegments)
588
 
 
589
 
 
590
 
def join_segment_parameters(url, parameters):
591
 
    """Create a new URL by adding segment parameters to an existing one.
592
 
 
593
 
    The parameters of the last segment in the URL will be updated; if a
594
 
    parameter with the same key already exists it will be overwritten.
595
 
 
596
 
    :param url: A URL, as string
597
 
    :param parameters: Dictionary of parameters, keys and values as bytestrings
598
 
    """
599
 
    (base, existing_parameters) = split_segment_parameters(url)
600
 
    new_parameters = {}
601
 
    new_parameters.update(existing_parameters)
602
 
    for key, value in parameters.items():
603
 
        if not isinstance(key, str):
604
 
            raise TypeError("parameter key %r is not a str" % key)
605
 
        if not isinstance(value, str):
606
 
            raise TypeError("parameter value %r for %r is not a str" %
607
 
                (value, key))
608
 
        if "=" in key:
609
 
            raise InvalidURLJoin("= exists in parameter key", url,
610
 
                parameters)
611
 
        new_parameters[key] = value
612
 
    return join_segment_parameters_raw(base,
613
 
        *["%s=%s" % item for item in sorted(new_parameters.items())])
614
 
 
615
 
 
616
472
def _win32_strip_local_trailing_slash(url):
617
473
    """Strip slashes after the drive letter"""
618
474
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
668
524
    This returns a Unicode path from a URL
669
525
    """
670
526
    # jam 20060427 URLs are supposed to be ASCII only strings
671
 
    #       If they are passed in as unicode, unquote
 
527
    #       If they are passed in as unicode, urllib.unquote
672
528
    #       will return a UNICODE string, which actually contains
673
529
    #       utf-8 bytes. So we have to ensure that they are
674
530
    #       plain ASCII strings, or the final .decode will
675
531
    #       try to encode the UNICODE => ASCII, and then decode
676
532
    #       it into utf-8.
 
533
    try:
 
534
        url = str(url)
 
535
    except UnicodeError, e:
 
536
        raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
677
537
 
678
 
    if PY3:
679
 
        if isinstance(url, text_type):
680
 
            try:
681
 
                url.encode("ascii")
682
 
            except UnicodeError as e:
683
 
                raise InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
684
 
        return urlparse.unquote(url)
685
 
    else:
686
 
        if isinstance(url, text_type):
687
 
            try:
688
 
                url = url.encode("ascii")
689
 
            except UnicodeError as e:
690
 
                raise InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
691
 
        unquoted = unquote(url)
692
 
        try:
693
 
            unicode_path = unquoted.decode('utf-8')
694
 
        except UnicodeError as e:
695
 
            raise InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
696
 
        return unicode_path
 
538
    unquoted = urllib.unquote(url)
 
539
    try:
 
540
        unicode_path = unquoted.decode('utf-8')
 
541
    except UnicodeError, e:
 
542
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
 
543
    return unicode_path
697
544
 
698
545
 
699
546
# These are characters that if escaped, should stay that way
701
548
_no_decode_ords = [ord(c) for c in _no_decode_chars]
702
549
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
703
550
                + ['%02X' % o for o in _no_decode_ords])
704
 
_hex_display_map = dict(([('%02x' % o, int2byte(o)) for o in range(256)]
705
 
                    + [('%02X' % o, int2byte(o)) for o in range(256)]))
 
551
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
 
552
                    + [('%02X' % o, chr(o)) for o in range(256)]))
706
553
#These entries get mapped to themselves
707
 
_hex_display_map.update((hex, b'%'+hex.encode('ascii')) for hex in _no_decode_hex)
 
554
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
708
555
 
709
556
# These characters shouldn't be percent-encoded, and it's always safe to
710
557
# unencode them if they are.
725
572
   "%#"         # Extra reserved characters
726
573
)
727
574
 
728
 
 
729
 
def _unescape_segment_for_display(segment, encoding):
730
 
    """Unescape a segment for display.
731
 
 
732
 
    Helper for unescape_for_display
733
 
 
734
 
    :param url: A 7-bit ASCII URL
735
 
    :param encoding: The final output encoding
736
 
 
737
 
    :return: A unicode string which can be safely encoded into the
738
 
         specified encoding.
739
 
    """
740
 
    escaped_chunks = segment.split('%')
741
 
    escaped_chunks[0] = escaped_chunks[0].encode('utf-8')
742
 
    for j in range(1, len(escaped_chunks)):
743
 
        item = escaped_chunks[j]
744
 
        try:
745
 
            escaped_chunks[j] = _hex_display_map[item[:2]]
746
 
        except KeyError:
747
 
            # Put back the percent symbol
748
 
            escaped_chunks[j] = b'%' + (item[:2].encode('utf-8') if PY3 else item[:2])
749
 
        except UnicodeDecodeError:
750
 
            escaped_chunks[j] = unichr(int(item[:2], 16)).encode('utf-8')
751
 
        escaped_chunks[j] +=  (item[2:].encode('utf-8') if PY3 else item[2:])
752
 
    unescaped = b''.join(escaped_chunks)
753
 
    try:
754
 
        decoded = unescaped.decode('utf-8')
755
 
    except UnicodeDecodeError:
756
 
        # If this path segment cannot be properly utf-8 decoded
757
 
        # after doing unescaping we will just leave it alone
758
 
        return segment
759
 
    else:
760
 
        try:
761
 
            decoded.encode(encoding)
762
 
        except UnicodeEncodeError:
763
 
            # If this chunk cannot be encoded in the local
764
 
            # encoding, then we should leave it alone
765
 
            return segment
766
 
        else:
767
 
            # Otherwise take the url decoded one
768
 
            return decoded
769
 
 
770
 
 
771
575
def unescape_for_display(url, encoding):
772
576
    """Decode what you can for a URL, so that we get a nice looking path.
773
577
 
795
599
 
796
600
    # Split into sections to try to decode utf-8
797
601
    res = url.split('/')
798
 
    for i in range(1, len(res)):
799
 
        res[i] = _unescape_segment_for_display(res[i], encoding)
 
602
    for i in xrange(1, len(res)):
 
603
        escaped_chunks = res[i].split('%')
 
604
        for j in xrange(1, len(escaped_chunks)):
 
605
            item = escaped_chunks[j]
 
606
            try:
 
607
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
 
608
            except KeyError:
 
609
                # Put back the percent symbol
 
610
                escaped_chunks[j] = '%' + item
 
611
            except UnicodeDecodeError:
 
612
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
 
613
        unescaped = ''.join(escaped_chunks)
 
614
        try:
 
615
            decoded = unescaped.decode('utf-8')
 
616
        except UnicodeDecodeError:
 
617
            # If this path segment cannot be properly utf-8 decoded
 
618
            # after doing unescaping we will just leave it alone
 
619
            pass
 
620
        else:
 
621
            try:
 
622
                decoded.encode(encoding)
 
623
            except UnicodeEncodeError:
 
624
                # If this chunk cannot be encoded in the local
 
625
                # encoding, then we should leave it alone
 
626
                pass
 
627
            else:
 
628
                # Otherwise take the url decoded one
 
629
                res[i] = decoded
800
630
    return u'/'.join(res)
801
631
 
802
632
 
811
641
    is used without a path, e.g. c:foo-bar => foo-bar.
812
642
    If no /, path separator or : is found, the from_location is returned.
813
643
    """
814
 
    from_location, unused_params = split_segment_parameters(from_location)
815
644
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
816
645
        return os.path.basename(from_location.rstrip("/\\"))
817
646
    else:
840
669
    old_parsed = urlparse.urlparse(old_base)
841
670
    new_parsed = urlparse.urlparse(new_base)
842
671
    if (old_parsed[:2]) != (new_parsed[:2]):
843
 
        raise InvalidRebaseURLs(old_base, new_base)
 
672
        raise errors.InvalidRebaseURLs(old_base, new_base)
844
673
    return determine_relative_path(new_parsed[2],
845
674
                                   join(old_parsed[2], url))
846
675
 
864
693
    return osutils.pathjoin(*segments)
865
694
 
866
695
 
867
 
class URL(object):
868
 
    """Parsed URL."""
869
 
 
870
 
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
871
 
            port, quoted_path):
872
 
        self.scheme = scheme
873
 
        self.quoted_host = quoted_host
874
 
        self.host = unquote(self.quoted_host)
875
 
        self.quoted_user = quoted_user
876
 
        if self.quoted_user is not None:
877
 
            self.user = unquote(self.quoted_user)
878
 
        else:
879
 
            self.user = None
880
 
        self.quoted_password = quoted_password
881
 
        if self.quoted_password is not None:
882
 
            self.password = unquote(self.quoted_password)
883
 
        else:
884
 
            self.password = None
885
 
        self.port = port
886
 
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
887
 
        self.path = unquote(self.quoted_path)
888
 
 
889
 
    def __eq__(self, other):
890
 
        return (isinstance(other, self.__class__) and
891
 
                self.scheme == other.scheme and
892
 
                self.host == other.host and
893
 
                self.user == other.user and
894
 
                self.password == other.password and
895
 
                self.path == other.path)
896
 
 
897
 
    def __repr__(self):
898
 
        return "<%s(%r, %r, %r, %r, %r, %r)>" % (
899
 
            self.__class__.__name__,
900
 
            self.scheme, self.quoted_user, self.quoted_password,
901
 
            self.quoted_host, self.port, self.quoted_path)
902
 
 
903
 
    @classmethod
904
 
    def from_string(cls, url):
905
 
        """Create a URL object from a string.
906
 
 
907
 
        :param url: URL as bytestring
908
 
        """
909
 
        # GZ 2017-06-09: Actually validate ascii-ness
910
 
        # pad.lv/1696545: For the moment, accept both native strings and unicode.
911
 
        if isinstance(url, str):
912
 
            pass
913
 
        elif isinstance(url, text_type):
914
 
            try:
915
 
                url = url.encode()
916
 
            except UnicodeEncodeError:
917
 
                raise InvalidURL(url)
918
 
        else:
919
 
            raise InvalidURL(url)
920
 
        (scheme, netloc, path, params,
921
 
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
922
 
        user = password = host = port = None
923
 
        if '@' in netloc:
924
 
            user, host = netloc.rsplit('@', 1)
925
 
            if ':' in user:
926
 
                user, password = user.split(':', 1)
927
 
        else:
928
 
            host = netloc
929
 
 
930
 
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
931
 
            # there *is* port
932
 
            host, port = host.rsplit(':', 1)
933
 
            if port:
934
 
                try:
935
 
                    port = int(port)
936
 
                except ValueError:
937
 
                    raise InvalidURL('invalid port number %s in url:\n%s' %
938
 
                                     (port, url))
939
 
            else:
940
 
                port = None
941
 
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
942
 
            host = host[1:-1]
943
 
 
944
 
        return cls(scheme, user, password, host, port, path)
945
 
 
946
 
    def __str__(self):
947
 
        netloc = self.quoted_host
948
 
        if ":" in netloc:
949
 
            netloc = "[%s]" % netloc
950
 
        if self.quoted_user is not None:
951
 
            # Note that we don't put the password back even if we
952
 
            # have one so that it doesn't get accidentally
953
 
            # exposed.
954
 
            netloc = '%s@%s' % (self.quoted_user, netloc)
955
 
        if self.port is not None:
956
 
            netloc = '%s:%d' % (netloc, self.port)
957
 
        return urlparse.urlunparse(
958
 
            (self.scheme, netloc, self.quoted_path, None, None, None))
959
 
 
960
 
    @staticmethod
961
 
    def _combine_paths(base_path, relpath):
962
 
        """Transform a Transport-relative path to a remote absolute path.
963
 
 
964
 
        This does not handle substitution of ~ but does handle '..' and '.'
965
 
        components.
966
 
 
967
 
        Examples::
968
 
 
969
 
            t._combine_paths('/home/sarah', 'project/foo')
970
 
                => '/home/sarah/project/foo'
971
 
            t._combine_paths('/home/sarah', '../../etc')
972
 
                => '/etc'
973
 
            t._combine_paths('/home/sarah', '/etc')
974
 
                => '/etc'
975
 
 
976
 
        :param base_path: base path
977
 
        :param relpath: relative url string for relative part of remote path.
978
 
        :return: urlencoded string for final path.
979
 
        """
980
 
        # pad.lv/1696545: For the moment, accept both native strings and unicode.
981
 
        if isinstance(relpath, str):
982
 
            pass
983
 
        elif isinstance(relpath, text_type):
984
 
            try:
985
 
                relpath = relpath.encode()
986
 
            except UnicodeEncodeError:
987
 
                raise InvalidURL(relpath)
988
 
        else:
989
 
            raise InvalidURL(relpath)
990
 
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
991
 
        if relpath.startswith('/'):
992
 
            base_parts = []
993
 
        else:
994
 
            base_parts = base_path.split('/')
995
 
        if len(base_parts) > 0 and base_parts[-1] == '':
996
 
            base_parts = base_parts[:-1]
997
 
        for p in relpath.split('/'):
998
 
            if p == '..':
999
 
                if len(base_parts) == 0:
1000
 
                    # In most filesystems, a request for the parent
1001
 
                    # of root, just returns root.
1002
 
                    continue
1003
 
                base_parts.pop()
1004
 
            elif p == '.':
1005
 
                continue # No-op
1006
 
            elif p != '':
1007
 
                base_parts.append(p)
1008
 
        path = '/'.join(base_parts)
1009
 
        if not path.startswith('/'):
1010
 
            path = '/' + path
1011
 
        return path
1012
 
 
1013
 
    def clone(self, offset=None):
1014
 
        """Return a new URL for a path relative to this URL.
1015
 
 
1016
 
        :param offset: A relative path, already urlencoded
1017
 
        :return: `URL` instance
1018
 
        """
1019
 
        if offset is not None:
1020
 
            relative = unescape(offset)
1021
 
            if sys.version_info[0] == 2:
1022
 
                relative = relative.encode('utf-8')
1023
 
            path = self._combine_paths(self.path, relative)
1024
 
            path = quote(path, safe="/~")
1025
 
        else:
1026
 
            path = self.quoted_path
1027
 
        return self.__class__(self.scheme, self.quoted_user,
1028
 
                self.quoted_password, self.quoted_host, self.port,
1029
 
                path)
1030
 
 
1031
696
 
1032
697
def parse_url(url):
1033
698
    """Extract the server address, the credentials and the path from the url.
1036
701
    chars.
1037
702
 
1038
703
    :param url: an quoted url
 
704
 
1039
705
    :return: (scheme, user, password, host, port, path) tuple, all fields
1040
706
        are unquoted.
1041
707
    """
1042
 
    parsed_url = URL.from_string(url)
1043
 
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
1044
 
        parsed_url.host, parsed_url.port, parsed_url.path)
 
708
    if isinstance(url, unicode):
 
709
        raise errors.InvalidURL('should be ascii:\n%r' % url)
 
710
    url = url.encode('utf-8')
 
711
    (scheme, netloc, path, params,
 
712
     query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
713
    user = password = host = port = None
 
714
    if '@' in netloc:
 
715
        user, host = netloc.rsplit('@', 1)
 
716
        if ':' in user:
 
717
            user, password = user.split(':', 1)
 
718
            password = urllib.unquote(password)
 
719
        user = urllib.unquote(user)
 
720
    else:
 
721
        host = netloc
 
722
 
 
723
    if ':' in host and not (host[0] == '[' and host[-1] == ']'): #there *is* port
 
724
        host, port = host.rsplit(':',1)
 
725
        try:
 
726
            port = int(port)
 
727
        except ValueError:
 
728
            raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
729
                                    (port, url))
 
730
    if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
731
        host = host[1:-1]
 
732
 
 
733
    host = urllib.unquote(host)
 
734
    path = urllib.unquote(path)
 
735
 
 
736
    return (scheme, user, password, host, port, path)