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

  • Committer: Jelmer Vernooij
  • Date: 2017-05-21 12:41:27 UTC
  • mto: This revision was merged to the branch mainline in revision 6623.
  • Revision ID: jelmer@jelmer.uk-20170521124127-iv8etg0vwymyai6y
s/bzr/brz/ in apport config.

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
 
19
21
import os
20
22
import re
21
23
import sys
22
24
 
23
 
from urllib import parse as urlparse
 
25
from brzlib.lazy_import import lazy_import
 
26
lazy_import(globals(), """
 
27
from posixpath import split as _posix_split
 
28
import urlparse
24
29
 
25
 
from . import (
 
30
from brzlib import (
26
31
    errors,
27
32
    osutils,
28
33
    )
29
 
 
30
 
from .lazy_import import lazy_import
31
 
lazy_import(globals(), """
32
 
from posixpath import split as _posix_split
33
34
""")
34
35
 
35
36
 
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
 
 
64
37
def basename(url, exclude_trailing_slash=True):
65
38
    """Return the last component of a URL.
66
39
 
88
61
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
89
62
 
90
63
 
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='/~'):
 
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):
98
139
    """Escape relpath to be a valid url."""
99
 
    return quote(relpath, safe=safe)
 
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='/~'))
100
145
 
101
146
 
102
147
def file_relpath(base, path):
106
151
    """
107
152
    if len(base) < MIN_ABS_FILEURL_LENGTH:
108
153
        raise ValueError('Length of base (%r) must equal or'
109
 
                         ' exceed the platform minimum url length (which is %d)' %
110
 
                         (base, MIN_ABS_FILEURL_LENGTH))
 
154
            ' exceed the platform minimum url length (which is %d)' %
 
155
            (base, MIN_ABS_FILEURL_LENGTH))
111
156
    base = osutils.normpath(local_path_from_url(base))
112
157
    path = osutils.normpath(local_path_from_url(path))
113
158
    return escape(osutils.relpath(base, path))
131
176
    first_path_slash = path.find('/')
132
177
    if first_path_slash == -1:
133
178
        return len(scheme), None
134
 
    return len(scheme), first_path_slash + m.start('path')
 
179
    return len(scheme), first_path_slash+m.start('path')
135
180
 
136
181
 
137
182
def is_url(url):
187
232
    """
188
233
    path = base.split('/')
189
234
    if len(path) > 1 and path[-1] == '':
190
 
        # If the path ends in a trailing /, remove it.
 
235
        #If the path ends in a trailing /, remove it.
191
236
        path.pop()
192
237
    for arg in args:
193
238
        if arg.startswith('/'):
197
242
                continue
198
243
            elif chunk == '..':
199
244
                if path == ['']:
200
 
                    raise InvalidURLJoin('Cannot go above root',
201
 
                                         base, args)
 
245
                    raise errors.InvalidURLJoin('Cannot go above root',
 
246
                            base, args)
202
247
                path.pop()
203
248
            else:
204
249
                path.append(chunk)
211
256
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
212
257
def _posix_local_path_from_url(url):
213
258
    """Convert a url like file:///path/to/foo into /path/to/foo"""
214
 
    url = strip_segment_parameters(url)
 
259
    url = split_segment_parameters_raw(url)[0]
215
260
    file_localhost_prefix = 'file://localhost/'
216
261
    if url.startswith(file_localhost_prefix):
217
262
        path = url[len(file_localhost_prefix) - 1:]
218
263
    elif not url.startswith('file:///'):
219
 
        raise InvalidURL(
 
264
        raise errors.InvalidURL(
220
265
            url, 'local urls must start with file:/// or file://localhost/')
221
266
    else:
222
267
        path = url[len('file://'):]
237
282
def _win32_local_path_from_url(url):
238
283
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
239
284
    if not url.startswith('file://'):
240
 
        raise InvalidURL(url, 'local urls must start with file:///, '
241
 
                         'UNC path urls must start with file://')
242
 
    url = strip_segment_parameters(url)
 
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]
243
288
    # We strip off all 3 slashes
244
289
    win32_url = url[len('file:'):]
245
290
    # check for UNC path: //HOST/path
246
291
    if not win32_url.startswith('///'):
247
292
        if (win32_url[2] == '/'
248
 
                or win32_url[3] in '|:'):
249
 
            raise InvalidURL(url, 'Win32 UNC path urls'
250
 
                             ' have form file://HOST/path')
 
293
            or win32_url[3] in '|:'):
 
294
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
 
295
                ' have form file://HOST/path')
251
296
        return unescape(win32_url)
252
297
 
253
298
    # allow empty paths so we can serve all roots
257
302
    # usual local path with drive letter
258
303
    if (len(win32_url) < 6
259
304
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
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')
 
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')
265
310
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
266
311
 
267
312
 
284
329
    if win32_path.startswith('//'):
285
330
        return 'file:' + escape(win32_path)
286
331
    return ('file:///' + str(win32_path[0].upper()) + ':' +
287
 
            escape(win32_path[2:]))
 
332
        escape(win32_path[2:]))
288
333
 
289
334
 
290
335
local_path_to_url = _posix_local_path_to_url
299
344
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
300
345
 
301
346
 
302
 
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
303
 
_url_hex_escapes_re = re.compile('(%[0-9a-fA-F]{2})')
 
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})')
304
349
 
305
350
 
306
351
def _unescape_safe_chars(matchobj):
340
385
        return local_path_to_url(url)
341
386
    prefix = url[:path_start]
342
387
    path = url[path_start:]
343
 
    if not isinstance(url, str):
 
388
    if not isinstance(url, unicode):
344
389
        for c in url:
345
390
            if c not in _url_safe_characters:
346
 
                raise InvalidURL(url, 'URLs can only contain specific'
347
 
                                 ' safe characters (not %r)' % c)
 
391
                raise errors.InvalidURL(url, 'URLs can only contain specific'
 
392
                                            ' safe characters (not %r)' % c)
348
393
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
349
394
        return str(prefix + ''.join(path))
350
395
 
351
396
    # We have a unicode (hybrid) url
352
397
    path_chars = list(path)
353
398
 
354
 
    for i in range(len(path_chars)):
 
399
    for i in xrange(len(path_chars)):
355
400
        if path_chars[i] not in _url_safe_characters:
 
401
            chars = path_chars[i].encode('utf-8')
356
402
            path_chars[i] = ''.join(
357
 
                ['%%%02X' % c for c in bytearray(path_chars[i].encode('utf-8'))])
 
403
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
358
404
    path = ''.join(path_chars)
359
405
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
360
406
    return str(prefix + path)
380
426
    if base_scheme != other_scheme:
381
427
        return other
382
428
    elif sys.platform == 'win32' and base_scheme == 'file://':
383
 
        base_drive = base[base_first_slash + 1:base_first_slash + 3]
384
 
        other_drive = other[other_first_slash + 1:other_first_slash + 3]
 
429
        base_drive = base[base_first_slash+1:base_first_slash+3]
 
430
        other_drive = other[other_first_slash+1:other_first_slash+3]
385
431
        if base_drive != other_drive:
386
432
            return other
387
433
 
388
 
    base_path = base[base_first_slash + 1:]
389
 
    other_path = other[other_first_slash + 1:]
 
434
    base_path = base[base_first_slash+1:]
 
435
    other_path = other[other_first_slash+1:]
390
436
 
391
437
    if base_path.endswith('/'):
392
438
        base_path = base_path[:-1]
417
463
    # Strip off the drive letter
418
464
    # path is currently /C:/foo
419
465
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
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
 
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
424
470
    return url_base, path
425
471
 
426
472
 
431
477
    :param exclude_trailing_slash: Strip off a final '/' if it is part
432
478
        of the path (but not if it is part of the protocol specification)
433
479
 
434
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if
435
 
        we're at the root.
 
480
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
 
481
        the root.
436
482
    """
437
483
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
438
484
 
448
494
            return url, ''
449
495
 
450
496
    # We have a fully defined path
451
 
    url_base = url[:first_path_slash]  # http://host, file://
452
 
    path = url[first_path_slash:]  # /file/foo
 
497
    url_base = url[:first_path_slash] # http://host, file://
 
498
    path = url[first_path_slash:] # /file/foo
453
499
 
454
500
    if sys.platform == 'win32' and url.startswith('file:///'):
455
501
        # Strip off the drive letter
472
518
    """
473
519
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
474
520
    #                operates on urls not url+segments, and Transport classes
475
 
    #                should not be blindly adding slashes in the first place.
 
521
    #                should not be blindly adding slashes in the first place. 
476
522
    lurl = strip_trailing_slash(url)
477
523
    # Segments begin at first comma after last forward slash, if one exists
478
 
    segment_start = lurl.find(",", lurl.rfind("/") + 1)
 
524
    segment_start = lurl.find(",", lurl.rfind("/")+1)
479
525
    if segment_start == -1:
480
526
        return (url, [])
481
 
    return (lurl[:segment_start],
482
 
            [str(s) for s in lurl[segment_start + 1:].split(",")])
 
527
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
483
528
 
484
529
 
485
530
def split_segment_parameters(url):
491
536
    (base_url, subsegments) = split_segment_parameters_raw(url)
492
537
    parameters = {}
493
538
    for subsegment in subsegments:
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)
 
539
        (key, value) = subsegment.split("=", 1)
502
540
        parameters[key] = value
503
541
    return (base_url, parameters)
504
542
 
505
543
 
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
 
 
516
544
def join_segment_parameters_raw(base, *subsegments):
517
 
    """Create a new URL by adding subsegments to an existing one.
 
545
    """Create a new URL by adding subsegments to an existing one. 
518
546
 
519
547
    This adds the specified subsegments to the last path in the specified
520
548
    base URL. The subsegments should be bytestrings.
524
552
    if not subsegments:
525
553
        return base
526
554
    for subsegment in subsegments:
527
 
        if not isinstance(subsegment, str):
 
555
        if type(subsegment) is not str:
528
556
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
529
557
        if "," in subsegment:
530
 
            raise InvalidURLJoin(", exists in subsegments",
531
 
                                 base, subsegments)
 
558
            raise errors.InvalidURLJoin(", exists in subsegments",
 
559
                                        base, subsegments)
532
560
    return ",".join((base,) + subsegments)
533
561
 
534
562
 
544
572
    (base, existing_parameters) = split_segment_parameters(url)
545
573
    new_parameters = {}
546
574
    new_parameters.update(existing_parameters)
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))
 
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))
553
581
        if "=" in key:
554
 
            raise InvalidURLJoin("= exists in parameter key", url,
555
 
                                 parameters)
 
582
            raise errors.InvalidURLJoin("= exists in parameter key", url,
 
583
                parameters)
556
584
        new_parameters[key] = value
557
 
    return join_segment_parameters_raw(
558
 
        base, *["%s=%s" % item for item in sorted(new_parameters.items())])
 
585
    return join_segment_parameters_raw(base, 
 
586
        *["%s=%s" % item for item in sorted(new_parameters.items())])
559
587
 
560
588
 
561
589
def _win32_strip_local_trailing_slash(url):
599
627
        # so just chop off the last character
600
628
        return url[:-1]
601
629
 
602
 
    if first_path_slash is None or first_path_slash == len(url) - 1:
 
630
    if first_path_slash is None or first_path_slash == len(url)-1:
603
631
        # Don't chop off anything if the only slash is the path
604
632
        # separating slash
605
633
        return url
619
647
    #       plain ASCII strings, or the final .decode will
620
648
    #       try to encode the UNICODE => ASCII, and then decode
621
649
    #       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,))
622
654
 
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)
 
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
630
661
 
631
662
 
632
663
# These are characters that if escaped, should stay that way
633
664
_no_decode_chars = ';/?:@&=+$,#'
634
665
_no_decode_ords = [ord(c) for c in _no_decode_chars]
635
666
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
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)
 
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)
642
672
 
643
673
# These characters shouldn't be percent-encoded, and it's always safe to
644
674
# unencode them if they are.
645
675
_url_dont_escape_characters = set(
646
 
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
647
 
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
648
 
    "0123456789"  # Numbers
649
 
    "-._~"  # Unreserved characters
 
676
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
677
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
678
   "0123456789" # Numbers
 
679
   "-._~"  # Unreserved characters
650
680
)
651
681
 
652
682
# These characters should not be escaped
653
683
_url_safe_characters = set(
654
 
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
655
 
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
656
 
    "0123456789"  # Numbers
657
 
    "_.-!~*'()"  # Unreserved characters
658
 
    "/;?:@&=+$,"  # Reserved characters
659
 
    "%#"         # Extra reserved characters
 
684
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
685
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
686
   "0123456789" # Numbers
 
687
   "_.-!~*'()"  # Unreserved characters
 
688
   "/;?:@&=+$," # Reserved characters
 
689
   "%#"         # Extra reserved characters
660
690
)
661
691
 
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
 
 
705
692
def unescape_for_display(url, encoding):
706
693
    """Decode what you can for a URL, so that we get a nice looking path.
707
694
 
729
716
 
730
717
    # Split into sections to try to decode utf-8
731
718
    res = url.split('/')
732
 
    for i in range(1, len(res)):
733
 
        res[i] = _unescape_segment_for_display(res[i], encoding)
 
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
734
747
    return u'/'.join(res)
735
748
 
736
749
 
745
758
    is used without a path, e.g. c:foo-bar => foo-bar.
746
759
    If no /, path separator or : is found, the from_location is returned.
747
760
    """
748
 
    from_location = strip_segment_parameters(from_location)
749
761
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
750
762
        return os.path.basename(from_location.rstrip("/\\"))
751
763
    else:
752
764
        sep = from_location.find(":")
753
765
        if sep > 0:
754
 
            return from_location[sep + 1:]
 
766
            return from_location[sep+1:]
755
767
        else:
756
768
            return from_location
757
769
 
774
786
    old_parsed = urlparse.urlparse(old_base)
775
787
    new_parsed = urlparse.urlparse(new_base)
776
788
    if (old_parsed[:2]) != (new_parsed[:2]):
777
 
        raise InvalidRebaseURLs(old_base, new_base)
 
789
        raise errors.InvalidRebaseURLs(old_base, new_base)
778
790
    return determine_relative_path(new_parsed[2],
779
791
                                   join(old_parsed[2], url))
780
792
 
785
797
    to_segments = osutils.splitpath(to_path)
786
798
    count = -1
787
799
    for count, (from_element, to_element) in enumerate(zip(from_segments,
788
 
                                                           to_segments)):
 
800
                                                       to_segments)):
789
801
        if from_element != to_element:
790
802
            break
791
803
    else:
802
814
    """Parsed URL."""
803
815
 
804
816
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
805
 
                 port, quoted_path):
 
817
            port, quoted_path):
806
818
        self.scheme = scheme
807
819
        self.quoted_host = quoted_host
808
820
        self.host = unquote(self.quoted_host)
817
829
        else:
818
830
            self.password = None
819
831
        self.port = port
820
 
        self.quoted_path = _url_hex_escapes_re.sub(
821
 
            _unescape_safe_chars, quoted_path)
 
832
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
822
833
        self.path = unquote(self.quoted_path)
823
834
 
824
835
    def __eq__(self, other):
841
852
 
842
853
        :param url: URL as bytestring
843
854
        """
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)
 
855
        if isinstance(url, unicode):
 
856
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
857
        url = url.encode('utf-8')
856
858
        (scheme, netloc, path, params,
857
859
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
858
860
        user = password = host = port = None
865
867
 
866
868
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
867
869
            # there *is* port
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
 
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
878
877
            host = host[1:-1]
879
878
 
880
879
        return cls(scheme, user, password, host, port, path)
913
912
        :param relpath: relative url string for relative part of remote path.
914
913
        :return: urlencoded string for final path.
915
914
        """
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)
 
915
        if not isinstance(relpath, str):
 
916
            raise errors.InvalidURL(relpath)
927
917
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
928
918
        if relpath.startswith('/'):
929
919
            base_parts = []
939
929
                    continue
940
930
                base_parts.pop()
941
931
            elif p == '.':
942
 
                continue  # No-op
 
932
                continue # No-op
943
933
            elif p != '':
944
934
                base_parts.append(p)
945
935
        path = '/'.join(base_parts)
954
944
        :return: `URL` instance
955
945
        """
956
946
        if offset is not None:
957
 
            relative = unescape(offset)
 
947
            relative = unescape(offset).encode('utf-8')
958
948
            path = self._combine_paths(self.path, relative)
959
949
            path = quote(path, safe="/~")
960
950
        else:
961
951
            path = self.quoted_path
962
952
        return self.__class__(self.scheme, self.quoted_user,
963
 
                              self.quoted_password, self.quoted_host, self.port,
964
 
                              path)
 
953
                self.quoted_password, self.quoted_host, self.port,
 
954
                path)
965
955
 
966
956
 
967
957
def parse_url(url):
976
966
    """
977
967
    parsed_url = URL.from_string(url)
978
968
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
979
 
            parsed_url.host, parsed_url.port, parsed_url.path)
 
969
        parsed_url.host, parsed_url.port, parsed_url.path)