/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 breezy/urlutils.py

  • Committer: Breezy landing bot
  • Author(s): Colin Watson
  • Date: 2020-11-16 21:47:08 UTC
  • mfrom: (7521.1.1 remove-lp-workaround)
  • Revision ID: breezy.the.bot@gmail.com-20201116214708-jos209mgxi41oy15
Remove breezy.git workaround for bazaar.launchpad.net.

Merged from https://code.launchpad.net/~cjwatson/brz/remove-lp-workaround/+merge/393710

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
 
from bzrlib.lazy_import import lazy_import
26
 
lazy_import(globals(), """
27
 
from posixpath import split as _posix_split
28
 
import urlparse
 
23
from urllib import parse as urlparse
29
24
 
30
 
from bzrlib import (
 
25
from . import (
31
26
    errors,
32
27
    osutils,
33
28
    )
 
29
 
 
30
from .lazy_import import lazy_import
 
31
lazy_import(globals(), """
 
32
from posixpath import split as _posix_split
34
33
""")
35
34
 
36
35
 
 
36
 
 
37
class InvalidURL(errors.PathError):
 
38
 
 
39
    _fmt = 'Invalid url supplied to transport: "%(path)s"%(extra)s'
 
40
 
 
41
 
 
42
class InvalidURLJoin(errors.PathError):
 
43
 
 
44
    _fmt = "Invalid URL join request: %(reason)s: %(base)r + %(join_args)r"
 
45
 
 
46
    def __init__(self, reason, base, join_args):
 
47
        self.reason = reason
 
48
        self.base = base
 
49
        self.join_args = join_args
 
50
        errors.PathError.__init__(self, base, reason)
 
51
 
 
52
 
 
53
class InvalidRebaseURLs(errors.PathError):
 
54
 
 
55
    _fmt = "URLs differ by more than path: %(from_)r and %(to)r"
 
56
 
 
57
    def __init__(self, from_, to):
 
58
        self.from_ = from_
 
59
        self.to = to
 
60
        errors.PathError.__init__(
 
61
            self, from_, 'URLs differ by more than path.')
 
62
 
 
63
 
37
64
def basename(url, exclude_trailing_slash=True):
38
65
    """Return the last component of a URL.
39
66
 
61
88
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
62
89
 
63
90
 
64
 
# Private copies of quote and unquote, copied from Python's
65
 
# urllib module because urllib unconditionally imports socket, which imports
66
 
# ssl.
67
 
 
68
 
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
69
 
               'abcdefghijklmnopqrstuvwxyz'
70
 
               '0123456789' '_.-')
71
 
_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)
74
 
_safe_quoters = {}
75
 
 
76
 
 
77
 
def quote(s, safe='/'):
78
 
    """quote('abc def') -> 'abc%20def'
79
 
 
80
 
    Each part of a URL, e.g. the path info, the query, etc., has a
81
 
    different set of reserved characters that must be quoted.
82
 
 
83
 
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
84
 
    the following reserved characters.
85
 
 
86
 
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
87
 
                  "$" | ","
88
 
 
89
 
    Each of these characters is reserved in some component of a URL,
90
 
    but not necessarily in all of them.
91
 
 
92
 
    By default, the quote function is intended for quoting the path
93
 
    section of a URL.  Thus, it will not encode '/'.  This character
94
 
    is reserved, but in typical usage the quote function is being
95
 
    called on a path where the existing slash characters are used as
96
 
    reserved characters.
97
 
    """
98
 
    # fastpath
99
 
    if not s:
100
 
        if s is None:
101
 
            raise TypeError('None object cannot be quoted')
102
 
        return s
103
 
    cachekey = (safe, always_safe)
104
 
    try:
105
 
        (quoter, safe) = _safe_quoters[cachekey]
106
 
    except KeyError:
107
 
        safe_map = _safe_map.copy()
108
 
        safe_map.update([(c, c) for c in safe])
109
 
        quoter = safe_map.__getitem__
110
 
        safe = always_safe + safe
111
 
        _safe_quoters[cachekey] = (quoter, safe)
112
 
    if not s.rstrip(safe):
113
 
        return s
114
 
    return ''.join(map(quoter, s))
115
 
 
116
 
 
117
 
_hexdig = '0123456789ABCDEFabcdef'
118
 
_hextochr = dict((a + b, chr(int(a + b, 16)))
119
 
                 for a in _hexdig for b in _hexdig)
120
 
 
121
 
def unquote(s):
122
 
    """unquote('abc%20def') -> 'abc def'."""
123
 
    res = s.split('%')
124
 
    # fastpath
125
 
    if len(res) == 1:
126
 
        return s
127
 
    s = res[0]
128
 
    for item in res[1:]:
129
 
        try:
130
 
            s += _hextochr[item[:2]] + item[2:]
131
 
        except KeyError:
132
 
            s += '%' + item
133
 
        except UnicodeDecodeError:
134
 
            s += unichr(int(item[:2], 16)) + item[2:]
135
 
    return s
136
 
 
137
 
 
138
 
def escape(relpath):
 
91
quote_from_bytes = urlparse.quote_from_bytes
 
92
quote = urlparse.quote
 
93
unquote_to_bytes = urlparse.unquote_to_bytes
 
94
unquote = urlparse.unquote
 
95
 
 
96
 
 
97
def escape(relpath, safe='/~'):
139
98
    """Escape relpath to be a valid url."""
140
 
    if isinstance(relpath, unicode):
141
 
        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='/~'))
 
99
    return quote(relpath, safe=safe)
145
100
 
146
101
 
147
102
def file_relpath(base, path):
151
106
    """
152
107
    if len(base) < MIN_ABS_FILEURL_LENGTH:
153
108
        raise ValueError('Length of base (%r) must equal or'
154
 
            ' exceed the platform minimum url length (which is %d)' %
155
 
            (base, MIN_ABS_FILEURL_LENGTH))
 
109
                         ' exceed the platform minimum url length (which is %d)' %
 
110
                         (base, MIN_ABS_FILEURL_LENGTH))
156
111
    base = osutils.normpath(local_path_from_url(base))
157
112
    path = osutils.normpath(local_path_from_url(path))
158
113
    return escape(osutils.relpath(base, path))
176
131
    first_path_slash = path.find('/')
177
132
    if first_path_slash == -1:
178
133
        return len(scheme), None
179
 
    return len(scheme), first_path_slash+m.start('path')
 
134
    return len(scheme), first_path_slash + m.start('path')
180
135
 
181
136
 
182
137
def is_url(url):
232
187
    """
233
188
    path = base.split('/')
234
189
    if len(path) > 1 and path[-1] == '':
235
 
        #If the path ends in a trailing /, remove it.
 
190
        # If the path ends in a trailing /, remove it.
236
191
        path.pop()
237
192
    for arg in args:
238
193
        if arg.startswith('/'):
242
197
                continue
243
198
            elif chunk == '..':
244
199
                if path == ['']:
245
 
                    raise errors.InvalidURLJoin('Cannot go above root',
246
 
                            base, args)
 
200
                    raise InvalidURLJoin('Cannot go above root',
 
201
                                         base, args)
247
202
                path.pop()
248
203
            else:
249
204
                path.append(chunk)
256
211
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
257
212
def _posix_local_path_from_url(url):
258
213
    """Convert a url like file:///path/to/foo into /path/to/foo"""
259
 
    url = split_segment_parameters_raw(url)[0]
 
214
    url = strip_segment_parameters(url)
260
215
    file_localhost_prefix = 'file://localhost/'
261
216
    if url.startswith(file_localhost_prefix):
262
217
        path = url[len(file_localhost_prefix) - 1:]
263
218
    elif not url.startswith('file:///'):
264
 
        raise errors.InvalidURL(
 
219
        raise InvalidURL(
265
220
            url, 'local urls must start with file:/// or file://localhost/')
266
221
    else:
267
222
        path = url[len('file://'):]
282
237
def _win32_local_path_from_url(url):
283
238
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
284
239
    if not url.startswith('file://'):
285
 
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
286
 
                                     'UNC path urls must start with file://')
287
 
    url = split_segment_parameters_raw(url)[0]
 
240
        raise InvalidURL(url, 'local urls must start with file:///, '
 
241
                         'UNC path urls must start with file://')
 
242
    url = strip_segment_parameters(url)
288
243
    # We strip off all 3 slashes
289
244
    win32_url = url[len('file:'):]
290
245
    # check for UNC path: //HOST/path
291
246
    if not win32_url.startswith('///'):
292
247
        if (win32_url[2] == '/'
293
 
            or win32_url[3] in '|:'):
294
 
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
295
 
                ' have form file://HOST/path')
 
248
                or win32_url[3] in '|:'):
 
249
            raise InvalidURL(url, 'Win32 UNC path urls'
 
250
                             ' have form file://HOST/path')
296
251
        return unescape(win32_url)
297
252
 
298
253
    # allow empty paths so we can serve all roots
302
257
    # usual local path with drive letter
303
258
    if (len(win32_url) < 6
304
259
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
305
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
306
 
        or win32_url[4] not in  '|:'
307
 
        or win32_url[5] != '/'):
308
 
        raise errors.InvalidURL(url, 'Win32 file urls start with'
309
 
                ' file:///x:/, where x is a valid drive letter')
 
260
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ') or
 
261
        win32_url[4] not in '|:'
 
262
            or win32_url[5] != '/'):
 
263
        raise InvalidURL(url, 'Win32 file urls start with'
 
264
                         ' file:///x:/, where x is a valid drive letter')
310
265
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
311
266
 
312
267
 
329
284
    if win32_path.startswith('//'):
330
285
        return 'file:' + escape(win32_path)
331
286
    return ('file:///' + str(win32_path[0].upper()) + ':' +
332
 
        escape(win32_path[2:]))
 
287
            escape(win32_path[2:]))
333
288
 
334
289
 
335
290
local_path_to_url = _posix_local_path_to_url
344
299
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
345
300
 
346
301
 
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})')
 
302
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
 
303
_url_hex_escapes_re = re.compile('(%[0-9a-fA-F]{2})')
349
304
 
350
305
 
351
306
def _unescape_safe_chars(matchobj):
385
340
        return local_path_to_url(url)
386
341
    prefix = url[:path_start]
387
342
    path = url[path_start:]
388
 
    if not isinstance(url, unicode):
 
343
    if not isinstance(url, str):
389
344
        for c in url:
390
345
            if c not in _url_safe_characters:
391
 
                raise errors.InvalidURL(url, 'URLs can only contain specific'
392
 
                                            ' safe characters (not %r)' % c)
 
346
                raise InvalidURL(url, 'URLs can only contain specific'
 
347
                                 ' safe characters (not %r)' % c)
393
348
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
394
349
        return str(prefix + ''.join(path))
395
350
 
396
351
    # We have a unicode (hybrid) url
397
352
    path_chars = list(path)
398
353
 
399
 
    for i in xrange(len(path_chars)):
 
354
    for i in range(len(path_chars)):
400
355
        if path_chars[i] not in _url_safe_characters:
401
 
            chars = path_chars[i].encode('utf-8')
402
356
            path_chars[i] = ''.join(
403
 
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
 
357
                ['%%%02X' % c for c in bytearray(path_chars[i].encode('utf-8'))])
404
358
    path = ''.join(path_chars)
405
359
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
406
360
    return str(prefix + path)
426
380
    if base_scheme != other_scheme:
427
381
        return other
428
382
    elif sys.platform == 'win32' and base_scheme == 'file://':
429
 
        base_drive = base[base_first_slash+1:base_first_slash+3]
430
 
        other_drive = other[other_first_slash+1:other_first_slash+3]
 
383
        base_drive = base[base_first_slash + 1:base_first_slash + 3]
 
384
        other_drive = other[other_first_slash + 1:other_first_slash + 3]
431
385
        if base_drive != other_drive:
432
386
            return other
433
387
 
434
 
    base_path = base[base_first_slash+1:]
435
 
    other_path = other[other_first_slash+1:]
 
388
    base_path = base[base_first_slash + 1:]
 
389
    other_path = other[other_first_slash + 1:]
436
390
 
437
391
    if base_path.endswith('/'):
438
392
        base_path = base_path[:-1]
463
417
    # Strip off the drive letter
464
418
    # path is currently /C:/foo
465
419
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
466
 
        raise errors.InvalidURL(url_base + path,
467
 
            'win32 file:/// paths need a drive letter')
468
 
    url_base += path[0:3] # file:// + /C:
469
 
    path = path[3:] # /foo
 
420
        raise InvalidURL(url_base + path,
 
421
                         'win32 file:/// paths need a drive letter')
 
422
    url_base += path[0:3]  # file:// + /C:
 
423
    path = path[3:]  # /foo
470
424
    return url_base, path
471
425
 
472
426
 
477
431
    :param exclude_trailing_slash: Strip off a final '/' if it is part
478
432
        of the path (but not if it is part of the protocol specification)
479
433
 
480
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
481
 
        the root.
 
434
    :return: (parent_url, child_dir).  child_dir may be the empty string if
 
435
        we're at the root.
482
436
    """
483
437
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
484
438
 
494
448
            return url, ''
495
449
 
496
450
    # We have a fully defined path
497
 
    url_base = url[:first_path_slash] # http://host, file://
498
 
    path = url[first_path_slash:] # /file/foo
 
451
    url_base = url[:first_path_slash]  # http://host, file://
 
452
    path = url[first_path_slash:]  # /file/foo
499
453
 
500
454
    if sys.platform == 'win32' and url.startswith('file:///'):
501
455
        # Strip off the drive letter
518
472
    """
519
473
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
520
474
    #                operates on urls not url+segments, and Transport classes
521
 
    #                should not be blindly adding slashes in the first place. 
 
475
    #                should not be blindly adding slashes in the first place.
522
476
    lurl = strip_trailing_slash(url)
523
477
    # Segments begin at first comma after last forward slash, if one exists
524
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
 
478
    segment_start = lurl.find(",", lurl.rfind("/") + 1)
525
479
    if segment_start == -1:
526
480
        return (url, [])
527
 
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
481
    return (lurl[:segment_start],
 
482
            [str(s) for s in lurl[segment_start + 1:].split(",")])
528
483
 
529
484
 
530
485
def split_segment_parameters(url):
536
491
    (base_url, subsegments) = split_segment_parameters_raw(url)
537
492
    parameters = {}
538
493
    for subsegment in subsegments:
539
 
        (key, value) = subsegment.split("=", 1)
 
494
        try:
 
495
            (key, value) = subsegment.split("=", 1)
 
496
        except ValueError:
 
497
            raise InvalidURL(url, "missing = in subsegment")
 
498
        if not isinstance(key, str):
 
499
            raise TypeError(key)
 
500
        if not isinstance(value, str):
 
501
            raise TypeError(value)
540
502
        parameters[key] = value
541
503
    return (base_url, parameters)
542
504
 
543
505
 
 
506
def strip_segment_parameters(url):
 
507
    """Strip the segment parameters from a URL.
 
508
 
 
509
    :param url: A relative or absolute URL
 
510
    :return: url
 
511
    """
 
512
    base_url, subsegments = split_segment_parameters_raw(url)
 
513
    return base_url
 
514
 
 
515
 
544
516
def join_segment_parameters_raw(base, *subsegments):
545
 
    """Create a new URL by adding subsegments to an existing one. 
 
517
    """Create a new URL by adding subsegments to an existing one.
546
518
 
547
519
    This adds the specified subsegments to the last path in the specified
548
520
    base URL. The subsegments should be bytestrings.
552
524
    if not subsegments:
553
525
        return base
554
526
    for subsegment in subsegments:
555
 
        if type(subsegment) is not str:
 
527
        if not isinstance(subsegment, str):
556
528
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
557
529
        if "," in subsegment:
558
 
            raise errors.InvalidURLJoin(", exists in subsegments",
559
 
                                        base, subsegments)
 
530
            raise InvalidURLJoin(", exists in subsegments",
 
531
                                 base, subsegments)
560
532
    return ",".join((base,) + subsegments)
561
533
 
562
534
 
572
544
    (base, existing_parameters) = split_segment_parameters(url)
573
545
    new_parameters = {}
574
546
    new_parameters.update(existing_parameters)
575
 
    for key, value in parameters.iteritems():
576
 
        if type(key) is not str:
577
 
            raise TypeError("parameter key %r is not a bytestring" % key)
578
 
        if type(value) is not str:
579
 
            raise TypeError("parameter value %r for %s is not a bytestring" %
580
 
                (key, value))
 
547
    for key, value in parameters.items():
 
548
        if not isinstance(key, str):
 
549
            raise TypeError("parameter key %r is not a str" % key)
 
550
        if not isinstance(value, str):
 
551
            raise TypeError("parameter value %r for %r is not a str" %
 
552
                            (value, key))
581
553
        if "=" in key:
582
 
            raise errors.InvalidURLJoin("= exists in parameter key", url,
583
 
                parameters)
 
554
            raise InvalidURLJoin("= exists in parameter key", url,
 
555
                                 parameters)
584
556
        new_parameters[key] = value
585
 
    return join_segment_parameters_raw(base, 
586
 
        *["%s=%s" % item for item in sorted(new_parameters.items())])
 
557
    return join_segment_parameters_raw(
 
558
        base, *["%s=%s" % item for item in sorted(new_parameters.items())])
587
559
 
588
560
 
589
561
def _win32_strip_local_trailing_slash(url):
627
599
        # so just chop off the last character
628
600
        return url[:-1]
629
601
 
630
 
    if first_path_slash is None or first_path_slash == len(url)-1:
 
602
    if first_path_slash is None or first_path_slash == len(url) - 1:
631
603
        # Don't chop off anything if the only slash is the path
632
604
        # separating slash
633
605
        return url
647
619
    #       plain ASCII strings, or the final .decode will
648
620
    #       try to encode the UNICODE => ASCII, and then decode
649
621
    #       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,))
654
622
 
655
 
    unquoted = unquote(url)
656
 
    try:
657
 
        unicode_path = unquoted.decode('utf-8')
658
 
    except UnicodeError, e:
659
 
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
660
 
    return unicode_path
 
623
    if isinstance(url, str):
 
624
        try:
 
625
            url.encode("ascii")
 
626
        except UnicodeError as e:
 
627
            raise InvalidURL(
 
628
                url, 'URL was not a plain ASCII url: %s' % (e,))
 
629
    return urlparse.unquote(url)
661
630
 
662
631
 
663
632
# These are characters that if escaped, should stay that way
664
633
_no_decode_chars = ';/?:@&=+$,#'
665
634
_no_decode_ords = [ord(c) for c in _no_decode_chars]
666
635
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
667
 
                + ['%02X' % o for o in _no_decode_ords])
668
 
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
669
 
                    + [('%02X' % o, chr(o)) for o in range(256)]))
670
 
#These entries get mapped to themselves
671
 
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
 
636
                  + ['%02X' % o for o in _no_decode_ords])
 
637
_hex_display_map = dict(([('%02x' % o, bytes([o])) for o in range(256)]
 
638
                         + [('%02X' % o, bytes([o])) for o in range(256)]))
 
639
# These entries get mapped to themselves
 
640
_hex_display_map.update((hex, b'%' + hex.encode('ascii'))
 
641
                        for hex in _no_decode_hex)
672
642
 
673
643
# These characters shouldn't be percent-encoded, and it's always safe to
674
644
# unencode them if they are.
675
645
_url_dont_escape_characters = set(
676
 
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
677
 
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
678
 
   "0123456789" # Numbers
679
 
   "-._~"  # Unreserved characters
 
646
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
 
647
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
 
648
    "0123456789"  # Numbers
 
649
    "-._~"  # Unreserved characters
680
650
)
681
651
 
682
652
# These characters should not be escaped
683
653
_url_safe_characters = set(
684
 
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
685
 
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
686
 
   "0123456789" # Numbers
687
 
   "_.-!~*'()"  # Unreserved characters
688
 
   "/;?:@&=+$," # Reserved characters
689
 
   "%#"         # Extra reserved characters
 
654
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
 
655
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
 
656
    "0123456789"  # Numbers
 
657
    "_.-!~*'()"  # Unreserved characters
 
658
    "/;?:@&=+$,"  # Reserved characters
 
659
    "%#"         # Extra reserved characters
690
660
)
691
661
 
 
662
 
 
663
def _unescape_segment_for_display(segment, encoding):
 
664
    """Unescape a segment for display.
 
665
 
 
666
    Helper for unescape_for_display
 
667
 
 
668
    :param url: A 7-bit ASCII URL
 
669
    :param encoding: The final output encoding
 
670
 
 
671
    :return: A unicode string which can be safely encoded into the
 
672
         specified encoding.
 
673
    """
 
674
    escaped_chunks = segment.split('%')
 
675
    escaped_chunks[0] = escaped_chunks[0].encode('utf-8')
 
676
    for j in range(1, len(escaped_chunks)):
 
677
        item = escaped_chunks[j]
 
678
        try:
 
679
            escaped_chunks[j] = _hex_display_map[item[:2]]
 
680
        except KeyError:
 
681
            # Put back the percent symbol
 
682
            escaped_chunks[j] = b'%' + (item[:2].encode('utf-8'))
 
683
        except UnicodeDecodeError:
 
684
            escaped_chunks[j] = chr(int(item[:2], 16)).encode('utf-8')
 
685
        escaped_chunks[j] += (item[2:].encode('utf-8'))
 
686
    unescaped = b''.join(escaped_chunks)
 
687
    try:
 
688
        decoded = unescaped.decode('utf-8')
 
689
    except UnicodeDecodeError:
 
690
        # If this path segment cannot be properly utf-8 decoded
 
691
        # after doing unescaping we will just leave it alone
 
692
        return segment
 
693
    else:
 
694
        try:
 
695
            decoded.encode(encoding)
 
696
        except UnicodeEncodeError:
 
697
            # If this chunk cannot be encoded in the local
 
698
            # encoding, then we should leave it alone
 
699
            return segment
 
700
        else:
 
701
            # Otherwise take the url decoded one
 
702
            return decoded
 
703
 
 
704
 
692
705
def unescape_for_display(url, encoding):
693
706
    """Decode what you can for a URL, so that we get a nice looking path.
694
707
 
716
729
 
717
730
    # Split into sections to try to decode utf-8
718
731
    res = url.split('/')
719
 
    for i in xrange(1, len(res)):
720
 
        escaped_chunks = res[i].split('%')
721
 
        for j in xrange(1, len(escaped_chunks)):
722
 
            item = escaped_chunks[j]
723
 
            try:
724
 
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
725
 
            except KeyError:
726
 
                # Put back the percent symbol
727
 
                escaped_chunks[j] = '%' + item
728
 
            except UnicodeDecodeError:
729
 
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
730
 
        unescaped = ''.join(escaped_chunks)
731
 
        try:
732
 
            decoded = unescaped.decode('utf-8')
733
 
        except UnicodeDecodeError:
734
 
            # If this path segment cannot be properly utf-8 decoded
735
 
            # after doing unescaping we will just leave it alone
736
 
            pass
737
 
        else:
738
 
            try:
739
 
                decoded.encode(encoding)
740
 
            except UnicodeEncodeError:
741
 
                # If this chunk cannot be encoded in the local
742
 
                # encoding, then we should leave it alone
743
 
                pass
744
 
            else:
745
 
                # Otherwise take the url decoded one
746
 
                res[i] = decoded
 
732
    for i in range(1, len(res)):
 
733
        res[i] = _unescape_segment_for_display(res[i], encoding)
747
734
    return u'/'.join(res)
748
735
 
749
736
 
758
745
    is used without a path, e.g. c:foo-bar => foo-bar.
759
746
    If no /, path separator or : is found, the from_location is returned.
760
747
    """
 
748
    from_location = strip_segment_parameters(from_location)
761
749
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
762
750
        return os.path.basename(from_location.rstrip("/\\"))
763
751
    else:
764
752
        sep = from_location.find(":")
765
753
        if sep > 0:
766
 
            return from_location[sep+1:]
 
754
            return from_location[sep + 1:]
767
755
        else:
768
756
            return from_location
769
757
 
786
774
    old_parsed = urlparse.urlparse(old_base)
787
775
    new_parsed = urlparse.urlparse(new_base)
788
776
    if (old_parsed[:2]) != (new_parsed[:2]):
789
 
        raise errors.InvalidRebaseURLs(old_base, new_base)
 
777
        raise InvalidRebaseURLs(old_base, new_base)
790
778
    return determine_relative_path(new_parsed[2],
791
779
                                   join(old_parsed[2], url))
792
780
 
797
785
    to_segments = osutils.splitpath(to_path)
798
786
    count = -1
799
787
    for count, (from_element, to_element) in enumerate(zip(from_segments,
800
 
                                                       to_segments)):
 
788
                                                           to_segments)):
801
789
        if from_element != to_element:
802
790
            break
803
791
    else:
814
802
    """Parsed URL."""
815
803
 
816
804
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
817
 
            port, quoted_path):
 
805
                 port, quoted_path):
818
806
        self.scheme = scheme
819
807
        self.quoted_host = quoted_host
820
808
        self.host = unquote(self.quoted_host)
829
817
        else:
830
818
            self.password = None
831
819
        self.port = port
832
 
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
 
820
        self.quoted_path = _url_hex_escapes_re.sub(
 
821
            _unescape_safe_chars, quoted_path)
833
822
        self.path = unquote(self.quoted_path)
834
823
 
835
824
    def __eq__(self, other):
852
841
 
853
842
        :param url: URL as bytestring
854
843
        """
855
 
        if isinstance(url, unicode):
856
 
            raise errors.InvalidURL('should be ascii:\n%r' % url)
857
 
        url = url.encode('utf-8')
 
844
        # GZ 2017-06-09: Actually validate ascii-ness
 
845
        # pad.lv/1696545: For the moment, accept both native strings and
 
846
        # unicode.
 
847
        if isinstance(url, str):
 
848
            pass
 
849
        elif isinstance(url, str):
 
850
            try:
 
851
                url = url.encode()
 
852
            except UnicodeEncodeError:
 
853
                raise InvalidURL(url)
 
854
        else:
 
855
            raise InvalidURL(url)
858
856
        (scheme, netloc, path, params,
859
857
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
860
858
        user = password = host = port = None
867
865
 
868
866
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
869
867
            # there *is* port
870
 
            host, port = host.rsplit(':',1)
871
 
            try:
872
 
                port = int(port)
873
 
            except ValueError:
874
 
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
875
 
                                        (port, url))
876
 
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
868
            host, port = host.rsplit(':', 1)
 
869
            if port:
 
870
                try:
 
871
                    port = int(port)
 
872
                except ValueError:
 
873
                    raise InvalidURL('invalid port number %s in url:\n%s' %
 
874
                                     (port, url))
 
875
            else:
 
876
                port = None
 
877
        if host != "" and host[0] == '[' and host[-1] == ']':  # IPv6
877
878
            host = host[1:-1]
878
879
 
879
880
        return cls(scheme, user, password, host, port, path)
912
913
        :param relpath: relative url string for relative part of remote path.
913
914
        :return: urlencoded string for final path.
914
915
        """
915
 
        if not isinstance(relpath, str):
916
 
            raise errors.InvalidURL(relpath)
 
916
        # pad.lv/1696545: For the moment, accept both native strings and
 
917
        # unicode.
 
918
        if isinstance(relpath, str):
 
919
            pass
 
920
        elif isinstance(relpath, str):
 
921
            try:
 
922
                relpath = relpath.encode()
 
923
            except UnicodeEncodeError:
 
924
                raise InvalidURL(relpath)
 
925
        else:
 
926
            raise InvalidURL(relpath)
917
927
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
918
928
        if relpath.startswith('/'):
919
929
            base_parts = []
929
939
                    continue
930
940
                base_parts.pop()
931
941
            elif p == '.':
932
 
                continue # No-op
 
942
                continue  # No-op
933
943
            elif p != '':
934
944
                base_parts.append(p)
935
945
        path = '/'.join(base_parts)
944
954
        :return: `URL` instance
945
955
        """
946
956
        if offset is not None:
947
 
            relative = unescape(offset).encode('utf-8')
 
957
            relative = unescape(offset)
948
958
            path = self._combine_paths(self.path, relative)
949
959
            path = quote(path, safe="/~")
950
960
        else:
951
961
            path = self.quoted_path
952
962
        return self.__class__(self.scheme, self.quoted_user,
953
 
                self.quoted_password, self.quoted_host, self.port,
954
 
                path)
 
963
                              self.quoted_password, self.quoted_host, self.port,
 
964
                              path)
955
965
 
956
966
 
957
967
def parse_url(url):
966
976
    """
967
977
    parsed_url = URL.from_string(url)
968
978
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
969
 
        parsed_url.host, parsed_url.port, parsed_url.path)
 
979
            parsed_url.host, parsed_url.port, parsed_url.path)