/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
29
 
 
30
 
from .lazy_import import lazy_import
 
23
from bzrlib.lazy_import import lazy_import
31
24
lazy_import(globals(), """
32
 
from posixpath import split as _posix_split
 
25
from posixpath import split as _posix_split, normpath as _posix_normpath
 
26
import urllib
 
27
import urlparse
33
28
 
34
 
from breezy import (
 
29
from bzrlib import (
35
30
    errors,
36
31
    osutils,
37
32
    )
38
33
""")
39
34
 
40
 
from .sixish import (
41
 
    PY3,
42
 
    text_type,
43
 
    )
44
 
 
45
35
 
46
36
def basename(url, exclude_trailing_slash=True):
47
37
    """Return the last component of a URL.
70
60
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
71
61
 
72
62
 
73
 
# Private copies of quote and unquote, copied from Python's
74
 
# urllib module because urllib unconditionally imports socket, which imports
75
 
# ssl.
76
 
 
77
 
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
78
 
               'abcdefghijklmnopqrstuvwxyz'
79
 
               '0123456789' '_.-')
80
 
_safe_map = {}
81
 
for i, c in zip(range(256), ''.join(map(chr, range(256)))):
82
 
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
83
 
_safe_quoters = {}
84
 
 
85
 
 
86
 
def quote(s, safe='/'):
87
 
    """quote('abc def') -> 'abc%20def'
88
 
 
89
 
    Each part of a URL, e.g. the path info, the query, etc., has a
90
 
    different set of reserved characters that must be quoted.
91
 
 
92
 
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
93
 
    the following reserved characters.
94
 
 
95
 
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
96
 
                  "$" | ","
97
 
 
98
 
    Each of these characters is reserved in some component of a URL,
99
 
    but not necessarily in all of them.
100
 
 
101
 
    By default, the quote function is intended for quoting the path
102
 
    section of a URL.  Thus, it will not encode '/'.  This character
103
 
    is reserved, but in typical usage the quote function is being
104
 
    called on a path where the existing slash characters are used as
105
 
    reserved characters.
106
 
    """
107
 
    # fastpath
108
 
    if not s:
109
 
        if s is None:
110
 
            raise TypeError('None object cannot be quoted')
111
 
        return s
112
 
    cachekey = (safe, always_safe)
113
 
    try:
114
 
        (quoter, safe) = _safe_quoters[cachekey]
115
 
    except KeyError:
116
 
        safe_map = _safe_map.copy()
117
 
        safe_map.update([(c, c) for c in safe])
118
 
        quoter = safe_map.__getitem__
119
 
        safe = always_safe + safe
120
 
        _safe_quoters[cachekey] = (quoter, safe)
121
 
    if not s.rstrip(safe):
122
 
        return s
123
 
    return ''.join(map(quoter, s))
124
 
 
125
 
 
126
 
unquote = urlparse.unquote
127
 
 
128
 
 
129
63
def escape(relpath):
130
64
    """Escape relpath to be a valid url."""
131
 
    if not isinstance(relpath, str):
 
65
    if isinstance(relpath, unicode):
132
66
        relpath = relpath.encode('utf-8')
133
 
    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='/~'))
134
70
 
135
71
 
136
72
def file_relpath(base, path):
142
78
        raise ValueError('Length of base (%r) must equal or'
143
79
            ' exceed the platform minimum url length (which is %d)' %
144
80
            (base, MIN_ABS_FILEURL_LENGTH))
145
 
    base = osutils.normpath(local_path_from_url(base))
146
 
    path = osutils.normpath(local_path_from_url(path))
 
81
    base = local_path_from_url(base)
 
82
    path = local_path_from_url(path)
147
83
    return escape(osutils.relpath(base, path))
148
84
 
149
85
 
165
101
    first_path_slash = path.find('/')
166
102
    if first_path_slash == -1:
167
103
        return len(scheme), None
168
 
    return len(scheme), first_path_slash+m.start('path')
169
 
 
170
 
 
171
 
def is_url(url):
172
 
    """Tests whether a URL is in actual fact a URL."""
173
 
    return _url_scheme_re.match(url) is not None
 
104
    return len(scheme), first_path_slash+len(scheme)+3
174
105
 
175
106
 
176
107
def join(base, *args):
187
118
    """
188
119
    if not args:
189
120
        return base
190
 
    scheme_end, path_start = _find_scheme_and_separator(base)
191
 
    if scheme_end is None and path_start is None:
192
 
        path_start = 0
193
 
    elif path_start is None:
194
 
        path_start = len(base)
195
 
    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
 
196
146
    for arg in args:
197
 
        arg_scheme_end, arg_path_start = _find_scheme_and_separator(arg)
198
 
        if arg_scheme_end is None and arg_path_start is None:
199
 
            arg_path_start = 0
200
 
        elif arg_path_start is None:
201
 
            arg_path_start = len(arg)
202
 
        if arg_scheme_end is not None:
203
 
            base = arg
204
 
            path = arg[arg_path_start:]
205
 
            scheme_end = arg_scheme_end
206
 
            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 = []
207
166
        else:
 
167
            path = '/'.join(path)
208
168
            path = joinpath(path, arg)
209
 
    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)
210
182
 
211
183
 
212
184
def joinpath(base, *args):
245
217
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
246
218
def _posix_local_path_from_url(url):
247
219
    """Convert a url like file:///path/to/foo into /path/to/foo"""
248
 
    url = split_segment_parameters_raw(url)[0]
249
220
    file_localhost_prefix = 'file://localhost/'
250
221
    if url.startswith(file_localhost_prefix):
251
222
        path = url[len(file_localhost_prefix) - 1:]
265
236
    """
266
237
    # importing directly from posixpath allows us to test this
267
238
    # on non-posix platforms
268
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
239
    return 'file://' + escape(_posix_normpath(
 
240
        osutils._posix_abspath(path)))
269
241
 
270
242
 
271
243
def _win32_local_path_from_url(url):
273
245
    if not url.startswith('file://'):
274
246
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
275
247
                                     'UNC path urls must start with file://')
276
 
    url = split_segment_parameters_raw(url)[0]
277
248
    # We strip off all 3 slashes
278
249
    win32_url = url[len('file:'):]
279
250
    # check for UNC path: //HOST/path
289
260
        return '/'
290
261
 
291
262
    # usual local path with drive letter
292
 
    if (len(win32_url) < 6
293
 
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
294
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
263
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
264
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
295
265
        or win32_url[4] not in  '|:'
296
266
        or win32_url[5] != '/'):
297
267
        raise errors.InvalidURL(url, 'Win32 file urls start with'
308
278
    # on non-win32 platform
309
279
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
310
280
    #       which actually strips trailing space characters.
311
 
    #       The worst part is that on linux ntpath.abspath has different
 
281
    #       The worst part is that under linux ntpath.abspath has different
312
282
    #       semantics, since 'nt' is not an available module.
313
283
    if path == '/':
314
284
        return 'file:///'
333
303
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
334
304
 
335
305
 
336
 
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
337
 
_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})')
338
308
 
339
309
 
340
310
def _unescape_safe_chars(matchobj):
369
339
    :param url: Either a hybrid URL or a local path
370
340
    :return: A normalized URL which only includes 7-bit ASCII characters.
371
341
    """
372
 
    scheme_end, path_start = _find_scheme_and_separator(url)
373
 
    if scheme_end is None:
 
342
    m = _url_scheme_re.match(url)
 
343
    if not m:
374
344
        return local_path_to_url(url)
375
 
    prefix = url[:path_start]
376
 
    path = url[path_start:]
377
 
    if not isinstance(url, text_type):
 
345
    scheme = m.group('scheme')
 
346
    path = m.group('path')
 
347
    if not isinstance(url, unicode):
378
348
        for c in url:
379
349
            if c not in _url_safe_characters:
380
350
                raise errors.InvalidURL(url, 'URLs can only contain specific'
381
351
                                            ' safe characters (not %r)' % c)
382
352
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
383
 
        return str(prefix + ''.join(path))
 
353
        return str(scheme + '://' + ''.join(path))
384
354
 
385
355
    # We have a unicode (hybrid) url
386
356
    path_chars = list(path)
387
357
 
388
 
    for i in range(len(path_chars)):
 
358
    for i in xrange(len(path_chars)):
389
359
        if path_chars[i] not in _url_safe_characters:
390
360
            chars = path_chars[i].encode('utf-8')
391
361
            path_chars[i] = ''.join(
392
362
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
393
363
    path = ''.join(path_chars)
394
364
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
395
 
    return str(prefix + path)
 
365
    return str(scheme + '://' + path)
396
366
 
397
367
 
398
368
def relative_url(base, other):
451
421
    """On win32 the drive letter needs to be added to the url base."""
452
422
    # Strip off the drive letter
453
423
    # path is currently /C:/foo
454
 
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
 
424
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
455
425
        raise errors.InvalidURL(url_base + path,
456
426
            'win32 file:/// paths need a drive letter')
457
427
    url_base += path[0:3] # file:// + /C:
499
469
    return url_base + head, tail
500
470
 
501
471
 
502
 
def split_segment_parameters_raw(url):
503
 
    """Split the subsegment of the last segment of a URL.
504
 
 
505
 
    :param url: A relative or absolute URL
506
 
    :return: (url, subsegments)
507
 
    """
508
 
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
509
 
    #                operates on urls not url+segments, and Transport classes
510
 
    #                should not be blindly adding slashes in the first place. 
511
 
    lurl = strip_trailing_slash(url)
512
 
    # Segments begin at first comma after last forward slash, if one exists
513
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
514
 
    if segment_start == -1:
515
 
        return (url, [])
516
 
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
517
 
 
518
 
 
519
 
def split_segment_parameters(url):
520
 
    """Split the segment parameters of the last segment of a URL.
521
 
 
522
 
    :param url: A relative or absolute URL
523
 
    :return: (url, segment_parameters)
524
 
    """
525
 
    (base_url, subsegments) = split_segment_parameters_raw(url)
526
 
    parameters = {}
527
 
    for subsegment in subsegments:
528
 
        (key, value) = subsegment.split("=", 1)
529
 
        parameters[key] = value
530
 
    return (base_url, parameters)
531
 
 
532
 
 
533
 
def join_segment_parameters_raw(base, *subsegments):
534
 
    """Create a new URL by adding subsegments to an existing one. 
535
 
 
536
 
    This adds the specified subsegments to the last path in the specified
537
 
    base URL. The subsegments should be bytestrings.
538
 
 
539
 
    :note: You probably want to use join_segment_parameters instead.
540
 
    """
541
 
    if not subsegments:
542
 
        return base
543
 
    for subsegment in subsegments:
544
 
        if not isinstance(subsegment, str):
545
 
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
546
 
        if "," in subsegment:
547
 
            raise errors.InvalidURLJoin(", exists in subsegments",
548
 
                                        base, subsegments)
549
 
    return ",".join((base,) + subsegments)
550
 
 
551
 
 
552
 
def join_segment_parameters(url, parameters):
553
 
    """Create a new URL by adding segment parameters to an existing one.
554
 
 
555
 
    The parameters of the last segment in the URL will be updated; if a
556
 
    parameter with the same key already exists it will be overwritten.
557
 
 
558
 
    :param url: A URL, as string
559
 
    :param parameters: Dictionary of parameters, keys and values as bytestrings
560
 
    """
561
 
    (base, existing_parameters) = split_segment_parameters(url)
562
 
    new_parameters = {}
563
 
    new_parameters.update(existing_parameters)
564
 
    for key, value in parameters.items():
565
 
        if not isinstance(key, str):
566
 
            raise TypeError("parameter key %r is not a bytestring" % key)
567
 
        if not isinstance(value, str):
568
 
            raise TypeError("parameter value %r for %s is not a bytestring" %
569
 
                (key, value))
570
 
        if "=" in key:
571
 
            raise errors.InvalidURLJoin("= exists in parameter key", url,
572
 
                parameters)
573
 
        new_parameters[key] = value
574
 
    return join_segment_parameters_raw(base, 
575
 
        *["%s=%s" % item for item in sorted(new_parameters.items())])
576
 
 
577
 
 
578
472
def _win32_strip_local_trailing_slash(url):
579
473
    """Strip slashes after the drive letter"""
580
474
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
630
524
    This returns a Unicode path from a URL
631
525
    """
632
526
    # jam 20060427 URLs are supposed to be ASCII only strings
633
 
    #       If they are passed in as unicode, unquote
 
527
    #       If they are passed in as unicode, urllib.unquote
634
528
    #       will return a UNICODE string, which actually contains
635
529
    #       utf-8 bytes. So we have to ensure that they are
636
530
    #       plain ASCII strings, or the final .decode will
637
531
    #       try to encode the UNICODE => ASCII, and then decode
638
532
    #       it into utf-8.
639
 
    if isinstance(url, text_type):
640
 
        try:
641
 
            url = url.encode("ascii")
642
 
        except UnicodeError as e:
643
 
            raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
644
 
    if PY3:
645
 
        unquoted = urlparse.unquote_to_bytes(url)
646
 
    else:
647
 
        unquoted = unquote(url)
 
533
    try:
 
534
        url = str(url)
 
535
    except UnicodeError, e:
 
536
        raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
 
537
 
 
538
    unquoted = urllib.unquote(url)
648
539
    try:
649
540
        unicode_path = unquoted.decode('utf-8')
650
 
    except UnicodeError as e:
 
541
    except UnicodeError, e:
651
542
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
652
543
    return unicode_path
653
544
 
708
599
 
709
600
    # Split into sections to try to decode utf-8
710
601
    res = url.split('/')
711
 
    for i in range(1, len(res)):
 
602
    for i in xrange(1, len(res)):
712
603
        escaped_chunks = res[i].split('%')
713
 
        for j in range(1, len(escaped_chunks)):
 
604
        for j in xrange(1, len(escaped_chunks)):
714
605
            item = escaped_chunks[j]
715
606
            try:
716
607
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
802
693
    return osutils.pathjoin(*segments)
803
694
 
804
695
 
805
 
class URL(object):
806
 
    """Parsed URL."""
807
 
 
808
 
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
809
 
            port, quoted_path):
810
 
        self.scheme = scheme
811
 
        self.quoted_host = quoted_host
812
 
        self.host = unquote(self.quoted_host)
813
 
        self.quoted_user = quoted_user
814
 
        if self.quoted_user is not None:
815
 
            self.user = unquote(self.quoted_user)
816
 
        else:
817
 
            self.user = None
818
 
        self.quoted_password = quoted_password
819
 
        if self.quoted_password is not None:
820
 
            self.password = unquote(self.quoted_password)
821
 
        else:
822
 
            self.password = None
823
 
        self.port = port
824
 
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
825
 
        self.path = unquote(self.quoted_path)
826
 
 
827
 
    def __eq__(self, other):
828
 
        return (isinstance(other, self.__class__) and
829
 
                self.scheme == other.scheme and
830
 
                self.host == other.host and
831
 
                self.user == other.user and
832
 
                self.password == other.password and
833
 
                self.path == other.path)
834
 
 
835
 
    def __repr__(self):
836
 
        return "<%s(%r, %r, %r, %r, %r, %r)>" % (
837
 
            self.__class__.__name__,
838
 
            self.scheme, self.quoted_user, self.quoted_password,
839
 
            self.quoted_host, self.port, self.quoted_path)
840
 
 
841
 
    @classmethod
842
 
    def from_string(cls, url):
843
 
        """Create a URL object from a string.
844
 
 
845
 
        :param url: URL as bytestring
846
 
        """
847
 
        # GZ 2017-06-09: Actually validate ascii-ness
848
 
        if not isinstance(url, str):
849
 
            raise errors.InvalidURL('should be ascii:\n%r' % url)
850
 
        (scheme, netloc, path, params,
851
 
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
852
 
        user = password = host = port = None
853
 
        if '@' in netloc:
854
 
            user, host = netloc.rsplit('@', 1)
855
 
            if ':' in user:
856
 
                user, password = user.split(':', 1)
857
 
        else:
858
 
            host = netloc
859
 
 
860
 
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
861
 
            # there *is* port
862
 
            host, port = host.rsplit(':',1)
863
 
            try:
864
 
                port = int(port)
865
 
            except ValueError:
866
 
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
867
 
                                        (port, url))
868
 
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
869
 
            host = host[1:-1]
870
 
 
871
 
        return cls(scheme, user, password, host, port, path)
872
 
 
873
 
    def __str__(self):
874
 
        netloc = self.quoted_host
875
 
        if ":" in netloc:
876
 
            netloc = "[%s]" % netloc
877
 
        if self.quoted_user is not None:
878
 
            # Note that we don't put the password back even if we
879
 
            # have one so that it doesn't get accidentally
880
 
            # exposed.
881
 
            netloc = '%s@%s' % (self.quoted_user, netloc)
882
 
        if self.port is not None:
883
 
            netloc = '%s:%d' % (netloc, self.port)
884
 
        return urlparse.urlunparse(
885
 
            (self.scheme, netloc, self.quoted_path, None, None, None))
886
 
 
887
 
    @staticmethod
888
 
    def _combine_paths(base_path, relpath):
889
 
        """Transform a Transport-relative path to a remote absolute path.
890
 
 
891
 
        This does not handle substitution of ~ but does handle '..' and '.'
892
 
        components.
893
 
 
894
 
        Examples::
895
 
 
896
 
            t._combine_paths('/home/sarah', 'project/foo')
897
 
                => '/home/sarah/project/foo'
898
 
            t._combine_paths('/home/sarah', '../../etc')
899
 
                => '/etc'
900
 
            t._combine_paths('/home/sarah', '/etc')
901
 
                => '/etc'
902
 
 
903
 
        :param base_path: base path
904
 
        :param relpath: relative url string for relative part of remote path.
905
 
        :return: urlencoded string for final path.
906
 
        """
907
 
        if not isinstance(relpath, str):
908
 
            raise errors.InvalidURL(relpath)
909
 
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
910
 
        if relpath.startswith('/'):
911
 
            base_parts = []
912
 
        else:
913
 
            base_parts = base_path.split('/')
914
 
        if len(base_parts) > 0 and base_parts[-1] == '':
915
 
            base_parts = base_parts[:-1]
916
 
        for p in relpath.split('/'):
917
 
            if p == '..':
918
 
                if len(base_parts) == 0:
919
 
                    # In most filesystems, a request for the parent
920
 
                    # of root, just returns root.
921
 
                    continue
922
 
                base_parts.pop()
923
 
            elif p == '.':
924
 
                continue # No-op
925
 
            elif p != '':
926
 
                base_parts.append(p)
927
 
        path = '/'.join(base_parts)
928
 
        if not path.startswith('/'):
929
 
            path = '/' + path
930
 
        return path
931
 
 
932
 
    def clone(self, offset=None):
933
 
        """Return a new URL for a path relative to this URL.
934
 
 
935
 
        :param offset: A relative path, already urlencoded
936
 
        :return: `URL` instance
937
 
        """
938
 
        if offset is not None:
939
 
            relative = unescape(offset).encode('utf-8')
940
 
            path = self._combine_paths(self.path, relative)
941
 
            path = quote(path, safe="/~")
942
 
        else:
943
 
            path = self.quoted_path
944
 
        return self.__class__(self.scheme, self.quoted_user,
945
 
                self.quoted_password, self.quoted_host, self.port,
946
 
                path)
947
 
 
948
696
 
949
697
def parse_url(url):
950
698
    """Extract the server address, the credentials and the path from the url.
953
701
    chars.
954
702
 
955
703
    :param url: an quoted url
 
704
 
956
705
    :return: (scheme, user, password, host, port, path) tuple, all fields
957
706
        are unquoted.
958
707
    """
959
 
    parsed_url = URL.from_string(url)
960
 
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
961
 
        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)