/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

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

Show diffs side-by-side

added added

removed removed

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