/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: Jelmer Vernooij
  • Date: 2017-06-08 23:30:31 UTC
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170608233031-3qavls2o7a1pqllj
Update imports.

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
24
 
 
25
 
from . import (
26
 
    errors,
27
 
    osutils,
28
 
    )
 
25
try:
 
26
    import urlparse
 
27
except ImportError:
 
28
    from urllib import parse as urlparse
29
29
 
30
30
from .lazy_import import lazy_import
31
31
lazy_import(globals(), """
32
32
from posixpath import split as _posix_split
 
33
 
 
34
from breezy import (
 
35
    errors,
 
36
    osutils,
 
37
    )
33
38
""")
34
39
 
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.')
 
40
from .sixish import (
 
41
    text_type,
 
42
    )
62
43
 
63
44
 
64
45
def basename(url, exclude_trailing_slash=True):
88
69
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
89
70
 
90
71
 
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='/~'):
 
72
# Private copies of quote and unquote, copied from Python's
 
73
# urllib module because urllib unconditionally imports socket, which imports
 
74
# ssl.
 
75
 
 
76
always_safe = (b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
77
               b'abcdefghijklmnopqrstuvwxyz'
 
78
               b'0123456789' b'_.-')
 
79
_safe_map = {}
 
80
for i, c in zip(range(256), bytes(bytearray(range(256)))):
 
81
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i).encode('ascii')
 
82
_safe_quoters = {}
 
83
 
 
84
 
 
85
def quote(s, safe=b'/'):
 
86
    """quote('abc def') -> 'abc%20def'
 
87
 
 
88
    Each part of a URL, e.g. the path info, the query, etc., has a
 
89
    different set of reserved characters that must be quoted.
 
90
 
 
91
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
 
92
    the following reserved characters.
 
93
 
 
94
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
 
95
                  "$" | ","
 
96
 
 
97
    Each of these characters is reserved in some component of a URL,
 
98
    but not necessarily in all of them.
 
99
 
 
100
    By default, the quote function is intended for quoting the path
 
101
    section of a URL.  Thus, it will not encode '/'.  This character
 
102
    is reserved, but in typical usage the quote function is being
 
103
    called on a path where the existing slash characters are used as
 
104
    reserved characters.
 
105
    """
 
106
    # fastpath
 
107
    if not s:
 
108
        if s is None:
 
109
            raise TypeError('None object cannot be quoted')
 
110
        return s
 
111
    cachekey = (safe, always_safe)
 
112
    try:
 
113
        (quoter, safe) = _safe_quoters[cachekey]
 
114
    except KeyError:
 
115
        safe_map = _safe_map.copy()
 
116
        safe_map.update([(c, c) for c in safe])
 
117
        quoter = safe_map.__getitem__
 
118
        safe = always_safe + safe
 
119
        _safe_quoters[cachekey] = (quoter, safe)
 
120
    if not s.rstrip(safe):
 
121
        return s
 
122
    return b''.join(map(quoter, s))
 
123
 
 
124
 
 
125
_hexdig = '0123456789ABCDEFabcdef'
 
126
_hextochr = dict((a + b, chr(int(a + b, 16)))
 
127
                 for a in _hexdig for b in _hexdig)
 
128
 
 
129
def unquote(s):
 
130
    """unquote('abc%20def') -> 'abc def'."""
 
131
    res = s.split(b'%')
 
132
    # fastpath
 
133
    if len(res) == 1:
 
134
        return s
 
135
    s = res[0]
 
136
    for item in res[1:]:
 
137
        try:
 
138
            s += _hextochr[item[:2]] + item[2:]
 
139
        except KeyError:
 
140
            s += b'%' + item
 
141
        except UnicodeDecodeError:
 
142
            s += unichr(int(item[:2], 16)) + item[2:]
 
143
    return s
 
144
 
 
145
 
 
146
def escape(relpath):
98
147
    """Escape relpath to be a valid url."""
99
 
    return quote(relpath, safe=safe)
 
148
    if isinstance(relpath, text_type):
 
149
        relpath = relpath.encode('utf-8')
 
150
    return quote(relpath, safe=b'/~')
100
151
 
101
152
 
102
153
def file_relpath(base, path):
106
157
    """
107
158
    if len(base) < MIN_ABS_FILEURL_LENGTH:
108
159
        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))
 
160
            ' exceed the platform minimum url length (which is %d)' %
 
161
            (base, MIN_ABS_FILEURL_LENGTH))
111
162
    base = osutils.normpath(local_path_from_url(base))
112
163
    path = osutils.normpath(local_path_from_url(path))
113
164
    return escape(osutils.relpath(base, path))
128
179
 
129
180
    # Find the path separating slash
130
181
    # (first slash after the ://)
131
 
    first_path_slash = path.find('/')
 
182
    first_path_slash = path.find(b'/')
132
183
    if first_path_slash == -1:
133
184
        return len(scheme), None
134
 
    return len(scheme), first_path_slash + m.start('path')
 
185
    return len(scheme), first_path_slash+m.start('path')
135
186
 
136
187
 
137
188
def is_url(url):
185
236
    We really should try to have exactly one place in the code base responsible
186
237
    for combining paths of URLs.
187
238
    """
188
 
    path = base.split('/')
189
 
    if len(path) > 1 and path[-1] == '':
190
 
        # If the path ends in a trailing /, remove it.
 
239
    path = base.split(b'/')
 
240
    if len(path) > 1 and path[-1] == b'':
 
241
        #If the path ends in a trailing /, remove it.
191
242
        path.pop()
192
243
    for arg in args:
193
 
        if arg.startswith('/'):
 
244
        if arg.startswith(b'/'):
194
245
            path = []
195
 
        for chunk in arg.split('/'):
196
 
            if chunk == '.':
 
246
        for chunk in arg.split(b'/'):
 
247
            if chunk == b'.':
197
248
                continue
198
 
            elif chunk == '..':
199
 
                if path == ['']:
200
 
                    raise InvalidURLJoin('Cannot go above root',
201
 
                                         base, args)
 
249
            elif chunk == b'..':
 
250
                if path == [b'']:
 
251
                    raise errors.InvalidURLJoin('Cannot go above root',
 
252
                            base, args)
202
253
                path.pop()
203
254
            else:
204
255
                path.append(chunk)
205
 
    if path == ['']:
206
 
        return '/'
 
256
    if path == [b'']:
 
257
        return b'/'
207
258
    else:
208
 
        return '/'.join(path)
 
259
        return b'/'.join(path)
209
260
 
210
261
 
211
262
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
212
263
def _posix_local_path_from_url(url):
213
264
    """Convert a url like file:///path/to/foo into /path/to/foo"""
214
 
    url = strip_segment_parameters(url)
215
 
    file_localhost_prefix = 'file://localhost/'
 
265
    url = split_segment_parameters_raw(url)[0]
 
266
    file_localhost_prefix = b'file://localhost/'
216
267
    if url.startswith(file_localhost_prefix):
217
268
        path = url[len(file_localhost_prefix) - 1:]
218
 
    elif not url.startswith('file:///'):
219
 
        raise InvalidURL(
 
269
    elif not url.startswith(b'file:///'):
 
270
        raise errors.InvalidURL(
220
271
            url, 'local urls must start with file:/// or file://localhost/')
221
272
    else:
222
 
        path = url[len('file://'):]
 
273
        path = url[len(b'file://'):]
223
274
    # We only strip off 2 slashes
224
275
    return unescape(path)
225
276
 
231
282
    """
232
283
    # importing directly from posixpath allows us to test this
233
284
    # on non-posix platforms
234
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
285
    return b'file://' + escape(osutils._posix_abspath(path))
235
286
 
236
287
 
237
288
def _win32_local_path_from_url(url):
238
289
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
239
290
    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)
 
291
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
 
292
                                     'UNC path urls must start with file://')
 
293
    url = split_segment_parameters_raw(url)[0]
243
294
    # We strip off all 3 slashes
244
295
    win32_url = url[len('file:'):]
245
296
    # check for UNC path: //HOST/path
246
297
    if not win32_url.startswith('///'):
247
298
        if (win32_url[2] == '/'
248
 
                or win32_url[3] in '|:'):
249
 
            raise InvalidURL(url, 'Win32 UNC path urls'
250
 
                             ' have form file://HOST/path')
 
299
            or win32_url[3] in '|:'):
 
300
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
 
301
                ' have form file://HOST/path')
251
302
        return unescape(win32_url)
252
303
 
253
304
    # allow empty paths so we can serve all roots
257
308
    # usual local path with drive letter
258
309
    if (len(win32_url) < 6
259
310
        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')
 
311
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
312
        or win32_url[4] not in  '|:'
 
313
        or win32_url[5] != '/'):
 
314
        raise errors.InvalidURL(url, 'Win32 file urls start with'
 
315
                ' file:///x:/, where x is a valid drive letter')
265
316
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
266
317
 
267
318
 
284
335
    if win32_path.startswith('//'):
285
336
        return 'file:' + escape(win32_path)
286
337
    return ('file:///' + str(win32_path[0].upper()) + ':' +
287
 
            escape(win32_path[2:]))
 
338
        escape(win32_path[2:]))
288
339
 
289
340
 
290
341
local_path_to_url = _posix_local_path_to_url
299
350
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
300
351
 
301
352
 
302
 
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
303
 
_url_hex_escapes_re = re.compile('(%[0-9a-fA-F]{2})')
 
353
_url_scheme_re = re.compile(b'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
 
354
_url_hex_escapes_re = re.compile(b'(%[0-9a-fA-F]{2})')
304
355
 
305
356
 
306
357
def _unescape_safe_chars(matchobj):
340
391
        return local_path_to_url(url)
341
392
    prefix = url[:path_start]
342
393
    path = url[path_start:]
343
 
    if not isinstance(url, str):
 
394
    if not isinstance(url, unicode):
344
395
        for c in url:
345
396
            if c not in _url_safe_characters:
346
 
                raise InvalidURL(url, 'URLs can only contain specific'
347
 
                                 ' safe characters (not %r)' % c)
 
397
                raise errors.InvalidURL(url, 'URLs can only contain specific'
 
398
                                            ' safe characters (not %r)' % c)
348
399
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
349
400
        return str(prefix + ''.join(path))
350
401
 
353
404
 
354
405
    for i in range(len(path_chars)):
355
406
        if path_chars[i] not in _url_safe_characters:
 
407
            chars = path_chars[i].encode('utf-8')
356
408
            path_chars[i] = ''.join(
357
 
                ['%%%02X' % c for c in bytearray(path_chars[i].encode('utf-8'))])
 
409
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
358
410
    path = ''.join(path_chars)
359
411
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
360
412
    return str(prefix + path)
380
432
    if base_scheme != other_scheme:
381
433
        return other
382
434
    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]
 
435
        base_drive = base[base_first_slash+1:base_first_slash+3]
 
436
        other_drive = other[other_first_slash+1:other_first_slash+3]
385
437
        if base_drive != other_drive:
386
438
            return other
387
439
 
388
 
    base_path = base[base_first_slash + 1:]
389
 
    other_path = other[other_first_slash + 1:]
 
440
    base_path = base[base_first_slash+1:]
 
441
    other_path = other[other_first_slash+1:]
390
442
 
391
443
    if base_path.endswith('/'):
392
444
        base_path = base_path[:-1]
417
469
    # Strip off the drive letter
418
470
    # path is currently /C:/foo
419
471
    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
 
472
        raise errors.InvalidURL(url_base + path,
 
473
            'win32 file:/// paths need a drive letter')
 
474
    url_base += path[0:3] # file:// + /C:
 
475
    path = path[3:] # /foo
424
476
    return url_base, path
425
477
 
426
478
 
431
483
    :param exclude_trailing_slash: Strip off a final '/' if it is part
432
484
        of the path (but not if it is part of the protocol specification)
433
485
 
434
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if
435
 
        we're at the root.
 
486
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
 
487
        the root.
436
488
    """
437
489
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
438
490
 
448
500
            return url, ''
449
501
 
450
502
    # We have a fully defined path
451
 
    url_base = url[:first_path_slash]  # http://host, file://
452
 
    path = url[first_path_slash:]  # /file/foo
 
503
    url_base = url[:first_path_slash] # http://host, file://
 
504
    path = url[first_path_slash:] # /file/foo
453
505
 
454
506
    if sys.platform == 'win32' and url.startswith('file:///'):
455
507
        # Strip off the drive letter
472
524
    """
473
525
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
474
526
    #                operates on urls not url+segments, and Transport classes
475
 
    #                should not be blindly adding slashes in the first place.
 
527
    #                should not be blindly adding slashes in the first place. 
476
528
    lurl = strip_trailing_slash(url)
477
529
    # Segments begin at first comma after last forward slash, if one exists
478
 
    segment_start = lurl.find(",", lurl.rfind("/") + 1)
 
530
    segment_start = lurl.find(b",", lurl.rfind(b"/")+1)
479
531
    if segment_start == -1:
480
532
        return (url, [])
481
 
    return (lurl[:segment_start],
482
 
            [str(s) for s in lurl[segment_start + 1:].split(",")])
 
533
    return (lurl[:segment_start], lurl[segment_start+1:].split(b","))
483
534
 
484
535
 
485
536
def split_segment_parameters(url):
491
542
    (base_url, subsegments) = split_segment_parameters_raw(url)
492
543
    parameters = {}
493
544
    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)
 
545
        (key, value) = subsegment.split("=", 1)
502
546
        parameters[key] = value
503
547
    return (base_url, parameters)
504
548
 
505
549
 
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
550
def join_segment_parameters_raw(base, *subsegments):
517
 
    """Create a new URL by adding subsegments to an existing one.
 
551
    """Create a new URL by adding subsegments to an existing one. 
518
552
 
519
553
    This adds the specified subsegments to the last path in the specified
520
554
    base URL. The subsegments should be bytestrings.
527
561
        if not isinstance(subsegment, str):
528
562
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
529
563
        if "," in subsegment:
530
 
            raise InvalidURLJoin(", exists in subsegments",
531
 
                                 base, subsegments)
 
564
            raise errors.InvalidURLJoin(", exists in subsegments",
 
565
                                        base, subsegments)
532
566
    return ",".join((base,) + subsegments)
533
567
 
534
568
 
546
580
    new_parameters.update(existing_parameters)
547
581
    for key, value in parameters.items():
548
582
        if not isinstance(key, str):
549
 
            raise TypeError("parameter key %r is not a str" % key)
 
583
            raise TypeError("parameter key %r is not a bytestring" % key)
550
584
        if not isinstance(value, str):
551
 
            raise TypeError("parameter value %r for %r is not a str" %
552
 
                            (value, key))
 
585
            raise TypeError("parameter value %r for %s is not a bytestring" %
 
586
                (key, value))
553
587
        if "=" in key:
554
 
            raise InvalidURLJoin("= exists in parameter key", url,
555
 
                                 parameters)
 
588
            raise errors.InvalidURLJoin("= exists in parameter key", url,
 
589
                parameters)
556
590
        new_parameters[key] = value
557
 
    return join_segment_parameters_raw(
558
 
        base, *["%s=%s" % item for item in sorted(new_parameters.items())])
 
591
    return join_segment_parameters_raw(base, 
 
592
        *["%s=%s" % item for item in sorted(new_parameters.items())])
559
593
 
560
594
 
561
595
def _win32_strip_local_trailing_slash(url):
587
621
        # format which does it differently.
588
622
        file:///c|/       => file:///c:/
589
623
    """
590
 
    if not url.endswith('/'):
 
624
    if not url.endswith(b'/'):
591
625
        # Nothing to do
592
626
        return url
593
 
    if sys.platform == 'win32' and url.startswith('file://'):
 
627
    if sys.platform == 'win32' and url.startswith(b'file://'):
594
628
        return _win32_strip_local_trailing_slash(url)
595
629
 
596
630
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
599
633
        # so just chop off the last character
600
634
        return url[:-1]
601
635
 
602
 
    if first_path_slash is None or first_path_slash == len(url) - 1:
 
636
    if first_path_slash is None or first_path_slash == len(url)-1:
603
637
        # Don't chop off anything if the only slash is the path
604
638
        # separating slash
605
639
        return url
619
653
    #       plain ASCII strings, or the final .decode will
620
654
    #       try to encode the UNICODE => ASCII, and then decode
621
655
    #       it into utf-8.
622
 
 
623
 
    if isinstance(url, str):
 
656
    if isinstance(url, text_type):
624
657
        try:
625
 
            url.encode("ascii")
 
658
            url = url.encode("ascii")
626
659
        except UnicodeError as e:
627
 
            raise InvalidURL(
628
 
                url, 'URL was not a plain ASCII url: %s' % (e,))
629
 
    return urlparse.unquote(url)
 
660
            raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
 
661
 
 
662
    unquoted = unquote(url)
 
663
    try:
 
664
        unicode_path = unquoted.decode('utf-8')
 
665
    except UnicodeError as e:
 
666
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
 
667
    return unicode_path
630
668
 
631
669
 
632
670
# These are characters that if escaped, should stay that way
633
671
_no_decode_chars = ';/?:@&=+$,#'
634
672
_no_decode_ords = [ord(c) for c in _no_decode_chars]
635
673
_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)
 
674
                + ['%02X' % o for o in _no_decode_ords])
 
675
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
 
676
                    + [('%02X' % o, chr(o)) for o in range(256)]))
 
677
#These entries get mapped to themselves
 
678
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
642
679
 
643
680
# These characters shouldn't be percent-encoded, and it's always safe to
644
681
# unencode them if they are.
645
682
_url_dont_escape_characters = set(
646
 
    "abcdefghijklmnopqrstuvwxyz"  # Lowercase alpha
647
 
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # Uppercase alpha
648
 
    "0123456789"  # Numbers
649
 
    "-._~"  # Unreserved characters
 
683
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
684
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
685
   "0123456789" # Numbers
 
686
   "-._~"  # Unreserved characters
650
687
)
651
688
 
652
689
# These characters should not be escaped
653
690
_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
 
691
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
692
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
693
   "0123456789" # Numbers
 
694
   "_.-!~*'()"  # Unreserved characters
 
695
   "/;?:@&=+$," # Reserved characters
 
696
   "%#"         # Extra reserved characters
660
697
)
661
698
 
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
699
def unescape_for_display(url, encoding):
706
700
    """Decode what you can for a URL, so that we get a nice looking path.
707
701
 
730
724
    # Split into sections to try to decode utf-8
731
725
    res = url.split('/')
732
726
    for i in range(1, len(res)):
733
 
        res[i] = _unescape_segment_for_display(res[i], encoding)
 
727
        escaped_chunks = res[i].split('%')
 
728
        for j in range(1, len(escaped_chunks)):
 
729
            item = escaped_chunks[j]
 
730
            try:
 
731
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
 
732
            except KeyError:
 
733
                # Put back the percent symbol
 
734
                escaped_chunks[j] = '%' + item
 
735
            except UnicodeDecodeError:
 
736
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
 
737
        unescaped = ''.join(escaped_chunks)
 
738
        try:
 
739
            decoded = unescaped.decode('utf-8')
 
740
        except UnicodeDecodeError:
 
741
            # If this path segment cannot be properly utf-8 decoded
 
742
            # after doing unescaping we will just leave it alone
 
743
            pass
 
744
        else:
 
745
            try:
 
746
                decoded.encode(encoding)
 
747
            except UnicodeEncodeError:
 
748
                # If this chunk cannot be encoded in the local
 
749
                # encoding, then we should leave it alone
 
750
                pass
 
751
            else:
 
752
                # Otherwise take the url decoded one
 
753
                res[i] = decoded
734
754
    return u'/'.join(res)
735
755
 
736
756
 
745
765
    is used without a path, e.g. c:foo-bar => foo-bar.
746
766
    If no /, path separator or : is found, the from_location is returned.
747
767
    """
748
 
    from_location = strip_segment_parameters(from_location)
749
768
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
750
769
        return os.path.basename(from_location.rstrip("/\\"))
751
770
    else:
752
771
        sep = from_location.find(":")
753
772
        if sep > 0:
754
 
            return from_location[sep + 1:]
 
773
            return from_location[sep+1:]
755
774
        else:
756
775
            return from_location
757
776
 
774
793
    old_parsed = urlparse.urlparse(old_base)
775
794
    new_parsed = urlparse.urlparse(new_base)
776
795
    if (old_parsed[:2]) != (new_parsed[:2]):
777
 
        raise InvalidRebaseURLs(old_base, new_base)
 
796
        raise errors.InvalidRebaseURLs(old_base, new_base)
778
797
    return determine_relative_path(new_parsed[2],
779
798
                                   join(old_parsed[2], url))
780
799
 
785
804
    to_segments = osutils.splitpath(to_path)
786
805
    count = -1
787
806
    for count, (from_element, to_element) in enumerate(zip(from_segments,
788
 
                                                           to_segments)):
 
807
                                                       to_segments)):
789
808
        if from_element != to_element:
790
809
            break
791
810
    else:
802
821
    """Parsed URL."""
803
822
 
804
823
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
805
 
                 port, quoted_path):
 
824
            port, quoted_path):
806
825
        self.scheme = scheme
807
826
        self.quoted_host = quoted_host
808
827
        self.host = unquote(self.quoted_host)
817
836
        else:
818
837
            self.password = None
819
838
        self.port = port
820
 
        self.quoted_path = _url_hex_escapes_re.sub(
821
 
            _unescape_safe_chars, quoted_path)
 
839
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
822
840
        self.path = unquote(self.quoted_path)
823
841
 
824
842
    def __eq__(self, other):
841
859
 
842
860
        :param url: URL as bytestring
843
861
        """
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)
 
862
        if isinstance(url, unicode):
 
863
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
864
        url = url.encode('utf-8')
856
865
        (scheme, netloc, path, params,
857
866
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
858
867
        user = password = host = port = None
865
874
 
866
875
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
867
876
            # 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
 
877
            host, port = host.rsplit(':',1)
 
878
            try:
 
879
                port = int(port)
 
880
            except ValueError:
 
881
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
882
                                        (port, url))
 
883
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
878
884
            host = host[1:-1]
879
885
 
880
886
        return cls(scheme, user, password, host, port, path)
913
919
        :param relpath: relative url string for relative part of remote path.
914
920
        :return: urlencoded string for final path.
915
921
        """
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)
 
922
        if not isinstance(relpath, str):
 
923
            raise errors.InvalidURL(relpath)
927
924
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
928
925
        if relpath.startswith('/'):
929
926
            base_parts = []
939
936
                    continue
940
937
                base_parts.pop()
941
938
            elif p == '.':
942
 
                continue  # No-op
 
939
                continue # No-op
943
940
            elif p != '':
944
941
                base_parts.append(p)
945
942
        path = '/'.join(base_parts)
954
951
        :return: `URL` instance
955
952
        """
956
953
        if offset is not None:
957
 
            relative = unescape(offset)
 
954
            relative = unescape(offset).encode('utf-8')
958
955
            path = self._combine_paths(self.path, relative)
959
956
            path = quote(path, safe="/~")
960
957
        else:
961
958
            path = self.quoted_path
962
959
        return self.__class__(self.scheme, self.quoted_user,
963
 
                              self.quoted_password, self.quoted_host, self.port,
964
 
                              path)
 
960
                self.quoted_password, self.quoted_host, self.port,
 
961
                path)
965
962
 
966
963
 
967
964
def parse_url(url):
976
973
    """
977
974
    parsed_url = URL.from_string(url)
978
975
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
979
 
            parsed_url.host, parsed_url.port, parsed_url.path)
 
976
        parsed_url.host, parsed_url.port, parsed_url.path)