~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

  • Committer: Martin Packman
  • Date: 2011-12-23 19:38:22 UTC
  • mto: This revision was merged to the branch mainline in revision 6405.
  • Revision ID: martin.packman@canonical.com-20111223193822-hesheea4o8aqwexv
Accept and document passing the medium rather than transport for smart connections

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Bazaar -- distributed version control
2
 
#
3
 
# Copyright (C) 2006 Canonical Ltd
 
1
# Copyright (C) 2006-2010 Canonical Ltd
4
2
#
5
3
# This program is free software; you can redistribute it and/or modify
6
4
# it under the terms of the GNU General Public License as published by
14
12
#
15
13
# You should have received a copy of the GNU General Public License
16
14
# along with this program; if not, write to the Free Software
17
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
16
 
19
17
"""A collection of function for handling URL operations."""
20
18
 
24
22
 
25
23
from bzrlib.lazy_import import lazy_import
26
24
lazy_import(globals(), """
27
 
from posixpath import split as _posix_split, normpath as _posix_normpath
 
25
from posixpath import split as _posix_split
28
26
import urllib
 
27
import urlparse
29
28
 
30
29
from bzrlib import (
31
30
    errors,
67
66
        relpath = relpath.encode('utf-8')
68
67
    # After quoting and encoding, the path should be perfectly
69
68
    # safe as a plain ASCII string, str() just enforces this
70
 
    return str(urllib.quote(relpath))
 
69
    return str(urllib.quote(relpath, safe='/~'))
71
70
 
72
71
 
73
72
def file_relpath(base, path):
74
73
    """Compute just the relative sub-portion of a url
75
 
    
 
74
 
76
75
    This assumes that both paths are already fully specified file:// URLs.
77
76
    """
78
 
    assert len(base) >= MIN_ABS_FILEURL_LENGTH, ('Length of base must be equal or'
79
 
        ' exceed the platform minimum url length (which is %d)' % 
80
 
        MIN_ABS_FILEURL_LENGTH)
81
 
 
82
 
    base = local_path_from_url(base)
83
 
    path = local_path_from_url(path)
 
77
    if len(base) < MIN_ABS_FILEURL_LENGTH:
 
78
        raise ValueError('Length of base (%r) must equal or'
 
79
            ' exceed the platform minimum url length (which is %d)' %
 
80
            (base, MIN_ABS_FILEURL_LENGTH))
 
81
    base = osutils.normpath(local_path_from_url(base))
 
82
    path = osutils.normpath(local_path_from_url(path))
84
83
    return escape(osutils.relpath(base, path))
85
84
 
86
85
 
102
101
    first_path_slash = path.find('/')
103
102
    if first_path_slash == -1:
104
103
        return len(scheme), None
105
 
    return len(scheme), first_path_slash+len(scheme)+3
 
104
    return len(scheme), first_path_slash+m.start('path')
 
105
 
 
106
 
 
107
def is_url(url):
 
108
    """Tests whether a URL is in actual fact a URL."""
 
109
    return _url_scheme_re.match(url) is not None
106
110
 
107
111
 
108
112
def join(base, *args):
119
123
    """
120
124
    if not args:
121
125
        return base
122
 
    match = _url_scheme_re.match(base)
123
 
    scheme = None
124
 
    if match:
125
 
        scheme = match.group('scheme')
126
 
        path = match.group('path').split('/')
127
 
        if path[-1:] == ['']:
128
 
            # Strip off a trailing slash
129
 
            # This helps both when we are at the root, and when
130
 
            # 'base' has an extra slash at the end
131
 
            path = path[:-1]
132
 
    else:
133
 
        path = base.split('/')
134
 
 
135
 
    if scheme is not None and len(path) >= 1:
136
 
        host = path[:1]
137
 
        # the path should be represented as an abs path.
138
 
        # we know this must be absolute because of the presence of a URL scheme.
139
 
        remove_root = True
140
 
        path = [''] + path[1:]
141
 
    else:
142
 
        # create an empty host, but dont alter the path - this might be a
143
 
        # relative url fragment.
144
 
        host = []
145
 
        remove_root = False
146
 
 
 
126
    scheme_end, path_start = _find_scheme_and_separator(base)
 
127
    if scheme_end is None and path_start is None:
 
128
        path_start = 0
 
129
    elif path_start is None:
 
130
        path_start = len(base)
 
131
    path = base[path_start:]
147
132
    for arg in args:
148
 
        match = _url_scheme_re.match(arg)
149
 
        if match:
150
 
            # Absolute URL
151
 
            scheme = match.group('scheme')
152
 
            # this skips .. normalisation, making http://host/../../..
153
 
            # be rather strange.
154
 
            path = match.group('path').split('/')
155
 
            # set the host and path according to new absolute URL, discarding
156
 
            # any previous values.
157
 
            # XXX: duplicates mess from earlier in this function.  This URL
158
 
            # manipulation code needs some cleaning up.
159
 
            if scheme is not None and len(path) >= 1:
160
 
                host = path[:1]
161
 
                path = path[1:]
162
 
                # url scheme implies absolute path.
163
 
                path = [''] + path
164
 
            else:
165
 
                # no url scheme we take the path as is.
166
 
                host = []
 
133
        arg_scheme_end, arg_path_start = _find_scheme_and_separator(arg)
 
134
        if arg_scheme_end is None and arg_path_start is None:
 
135
            arg_path_start = 0
 
136
        elif arg_path_start is None:
 
137
            arg_path_start = len(arg)
 
138
        if arg_scheme_end is not None:
 
139
            base = arg
 
140
            path = arg[arg_path_start:]
 
141
            scheme_end = arg_scheme_end
 
142
            path_start = arg_path_start
167
143
        else:
168
 
            path = '/'.join(path)
169
144
            path = joinpath(path, arg)
170
 
            path = path.split('/')
171
 
    if remove_root and path[0:1] == ['']:
172
 
        del path[0]
173
 
    if host:
174
 
        # Remove the leading slash from the path, so long as it isn't also the
175
 
        # trailing slash, which we want to keep if present.
176
 
        if path and path[0] == '' and len(path) > 1:
177
 
            del path[0]
178
 
        path = host + path
179
 
 
180
 
    if scheme is None:
181
 
        return '/'.join(path)
182
 
    return scheme + '://' + '/'.join(path)
 
145
    return base[:path_start] + path
183
146
 
184
147
 
185
148
def joinpath(base, *args):
186
149
    """Join URL path segments to a URL path segment.
187
 
    
 
150
 
188
151
    This is somewhat like osutils.joinpath, but intended for URLs.
189
152
 
190
153
    XXX: this duplicates some normalisation logic, and also duplicates a lot of
218
181
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
219
182
def _posix_local_path_from_url(url):
220
183
    """Convert a url like file:///path/to/foo into /path/to/foo"""
221
 
    if not url.startswith('file:///'):
222
 
        raise errors.InvalidURL(url, 'local urls must start with file:///')
 
184
    url = split_segment_parameters_raw(url)[0]
 
185
    file_localhost_prefix = 'file://localhost/'
 
186
    if url.startswith(file_localhost_prefix):
 
187
        path = url[len(file_localhost_prefix) - 1:]
 
188
    elif not url.startswith('file:///'):
 
189
        raise errors.InvalidURL(
 
190
            url, 'local urls must start with file:/// or file://localhost/')
 
191
    else:
 
192
        path = url[len('file://'):]
223
193
    # We only strip off 2 slashes
224
 
    return unescape(url[len('file://'):])
 
194
    return unescape(path)
225
195
 
226
196
 
227
197
def _posix_local_path_to_url(path):
229
199
 
230
200
    This also handles transforming escaping unicode characters, etc.
231
201
    """
232
 
    # importing directly from posixpath allows us to test this 
 
202
    # importing directly from posixpath allows us to test this
233
203
    # on non-posix platforms
234
 
    return 'file://' + escape(_posix_normpath(
235
 
        osutils._posix_abspath(path)))
 
204
    return 'file://' + escape(osutils._posix_abspath(path))
236
205
 
237
206
 
238
207
def _win32_local_path_from_url(url):
240
209
    if not url.startswith('file://'):
241
210
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
242
211
                                     'UNC path urls must start with file://')
 
212
    url = split_segment_parameters_raw(url)[0]
243
213
    # We strip off all 3 slashes
244
214
    win32_url = url[len('file:'):]
245
215
    # check for UNC path: //HOST/path
249
219
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
250
220
                ' have form file://HOST/path')
251
221
        return unescape(win32_url)
 
222
 
 
223
    # allow empty paths so we can serve all roots
 
224
    if win32_url == '///':
 
225
        return '/'
 
226
 
252
227
    # usual local path with drive letter
253
 
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
254
 
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
228
    if (len(win32_url) < 6
 
229
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
230
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
255
231
        or win32_url[4] not in  '|:'
256
232
        or win32_url[5] != '/'):
257
233
        raise errors.InvalidURL(url, 'Win32 file urls start with'
264
240
 
265
241
    This also handles transforming escaping unicode characters, etc.
266
242
    """
267
 
    # importing directly from ntpath allows us to test this 
 
243
    # importing directly from ntpath allows us to test this
268
244
    # on non-win32 platform
269
245
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
270
246
    #       which actually strips trailing space characters.
271
 
    #       The worst part is that under linux ntpath.abspath has different
 
247
    #       The worst part is that on linux ntpath.abspath has different
272
248
    #       semantics, since 'nt' is not an available module.
 
249
    if path == '/':
 
250
        return 'file:///'
 
251
 
273
252
    win32_path = osutils._win32_abspath(path)
274
253
    # check for UNC path \\HOST\path
275
254
    if win32_path.startswith('//'):
276
255
        return 'file:' + escape(win32_path)
277
 
    return 'file:///' + win32_path[0].upper() + ':' + escape(win32_path[2:])
 
256
    return ('file:///' + str(win32_path[0].upper()) + ':' +
 
257
        escape(win32_path[2:]))
278
258
 
279
259
 
280
260
local_path_to_url = _posix_local_path_to_url
289
269
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
290
270
 
291
271
 
292
 
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,})://(?P<path>.*)$')
 
272
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
293
273
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
294
274
 
295
275
 
296
276
def _unescape_safe_chars(matchobj):
297
277
    """re.sub callback to convert hex-escapes to plain characters (if safe).
298
 
    
 
278
 
299
279
    e.g. '%7E' will be converted to '~'.
300
280
    """
301
281
    hex_digits = matchobj.group(0)[1:]
308
288
 
309
289
def normalize_url(url):
310
290
    """Make sure that a path string is in fully normalized URL form.
311
 
    
 
291
 
312
292
    This handles URLs which have unicode characters, spaces,
313
293
    special characters, etc.
314
294
 
325
305
    :param url: Either a hybrid URL or a local path
326
306
    :return: A normalized URL which only includes 7-bit ASCII characters.
327
307
    """
328
 
    m = _url_scheme_re.match(url)
329
 
    if not m:
 
308
    scheme_end, path_start = _find_scheme_and_separator(url)
 
309
    if scheme_end is None:
330
310
        return local_path_to_url(url)
331
 
    scheme = m.group('scheme')
332
 
    path = m.group('path')
 
311
    prefix = url[:path_start]
 
312
    path = url[path_start:]
333
313
    if not isinstance(url, unicode):
334
314
        for c in url:
335
315
            if c not in _url_safe_characters:
336
316
                raise errors.InvalidURL(url, 'URLs can only contain specific'
337
317
                                            ' safe characters (not %r)' % c)
338
318
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
339
 
        return str(scheme + '://' + ''.join(path))
 
319
        return str(prefix + ''.join(path))
340
320
 
341
321
    # We have a unicode (hybrid) url
342
322
    path_chars = list(path)
348
328
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
349
329
    path = ''.join(path_chars)
350
330
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
351
 
    return str(scheme + '://' + path)
 
331
    return str(prefix + path)
352
332
 
353
333
 
354
334
def relative_url(base, other):
360
340
    dummy, base_first_slash = _find_scheme_and_separator(base)
361
341
    if base_first_slash is None:
362
342
        return other
363
 
    
 
343
 
364
344
    dummy, other_first_slash = _find_scheme_and_separator(other)
365
345
    if other_first_slash is None:
366
346
        return other
370
350
    other_scheme = other[:other_first_slash]
371
351
    if base_scheme != other_scheme:
372
352
        return other
 
353
    elif sys.platform == 'win32' and base_scheme == 'file://':
 
354
        base_drive = base[base_first_slash+1:base_first_slash+3]
 
355
        other_drive = other[other_first_slash+1:other_first_slash+3]
 
356
        if base_drive != other_drive:
 
357
            return other
373
358
 
374
359
    base_path = base[base_first_slash+1:]
375
360
    other_path = other[other_first_slash+1:]
402
387
    """On win32 the drive letter needs to be added to the url base."""
403
388
    # Strip off the drive letter
404
389
    # path is currently /C:/foo
405
 
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
406
 
        raise errors.InvalidURL(url_base + path, 
 
390
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
 
391
        raise errors.InvalidURL(url_base + path,
407
392
            'win32 file:/// paths need a drive letter')
408
393
    url_base += path[0:3] # file:// + /C:
409
394
    path = path[3:] # /foo
417
402
    :param exclude_trailing_slash: Strip off a final '/' if it is part
418
403
        of the path (but not if it is part of the protocol specification)
419
404
 
420
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at 
 
405
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
421
406
        the root.
422
407
    """
423
408
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
450
435
    return url_base + head, tail
451
436
 
452
437
 
 
438
def split_segment_parameters_raw(url):
 
439
    """Split the subsegment of the last segment of a URL.
 
440
 
 
441
    :param url: A relative or absolute URL
 
442
    :return: (url, subsegments)
 
443
    """
 
444
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
 
445
    #                operates on urls not url+segments, and Transport classes
 
446
    #                should not be blindly adding slashes in the first place. 
 
447
    lurl = strip_trailing_slash(url)
 
448
    # Segments begin at first comma after last forward slash, if one exists
 
449
    segment_start = lurl.find(",", lurl.rfind("/")+1)
 
450
    if segment_start == -1:
 
451
        return (url, [])
 
452
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
453
 
 
454
 
 
455
def split_segment_parameters(url):
 
456
    """Split the segment parameters of the last segment of a URL.
 
457
 
 
458
    :param url: A relative or absolute URL
 
459
    :return: (url, segment_parameters)
 
460
    """
 
461
    (base_url, subsegments) = split_segment_parameters_raw(url)
 
462
    parameters = {}
 
463
    for subsegment in subsegments:
 
464
        (key, value) = subsegment.split("=", 1)
 
465
        parameters[key] = value
 
466
    return (base_url, parameters)
 
467
 
 
468
 
 
469
def join_segment_parameters_raw(base, *subsegments):
 
470
    """Create a new URL by adding subsegments to an existing one. 
 
471
 
 
472
    This adds the specified subsegments to the last path in the specified
 
473
    base URL. The subsegments should be bytestrings.
 
474
 
 
475
    :note: You probably want to use join_segment_parameters instead.
 
476
    """
 
477
    if not subsegments:
 
478
        return base
 
479
    for subsegment in subsegments:
 
480
        if type(subsegment) is not str:
 
481
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
 
482
        if "," in subsegment:
 
483
            raise errors.InvalidURLJoin(", exists in subsegments",
 
484
                                        base, subsegments)
 
485
    return ",".join((base,) + subsegments)
 
486
 
 
487
 
 
488
def join_segment_parameters(url, parameters):
 
489
    """Create a new URL by adding segment parameters to an existing one.
 
490
 
 
491
    The parameters of the last segment in the URL will be updated; if a
 
492
    parameter with the same key already exists it will be overwritten.
 
493
 
 
494
    :param url: A URL, as string
 
495
    :param parameters: Dictionary of parameters, keys and values as bytestrings
 
496
    """
 
497
    (base, existing_parameters) = split_segment_parameters(url)
 
498
    new_parameters = {}
 
499
    new_parameters.update(existing_parameters)
 
500
    for key, value in parameters.iteritems():
 
501
        if type(key) is not str:
 
502
            raise TypeError("parameter key %r is not a bytestring" % key)
 
503
        if type(value) is not str:
 
504
            raise TypeError("parameter value %r for %s is not a bytestring" %
 
505
                (key, value))
 
506
        if "=" in key:
 
507
            raise errors.InvalidURLJoin("= exists in parameter key", url,
 
508
                parameters)
 
509
        new_parameters[key] = value
 
510
    return join_segment_parameters_raw(base, 
 
511
        *["%s=%s" % item for item in sorted(new_parameters.items())])
 
512
 
 
513
 
453
514
def _win32_strip_local_trailing_slash(url):
454
515
    """Strip slashes after the drive letter"""
455
516
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
527
588
# These are characters that if escaped, should stay that way
528
589
_no_decode_chars = ';/?:@&=+$,#'
529
590
_no_decode_ords = [ord(c) for c in _no_decode_chars]
530
 
_no_decode_hex = (['%02x' % o for o in _no_decode_ords] 
 
591
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
531
592
                + ['%02X' % o for o in _no_decode_ords])
532
593
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
533
594
                    + [('%02X' % o, chr(o)) for o in range(256)]))
559
620
    This will turn file:// urls into local paths, and try to decode
560
621
    any portions of a http:// style url that it can.
561
622
 
562
 
    Any sections of the URL which can't be represented in the encoding or 
 
623
    Any sections of the URL which can't be represented in the encoding or
563
624
    need to stay as escapes are left alone.
564
625
 
565
626
    :param url: A 7-bit ASCII URL
566
627
    :param encoding: The final output encoding
567
628
 
568
 
    :return: A unicode string which can be safely encoded into the 
 
629
    :return: A unicode string which can be safely encoded into the
569
630
         specified encoding.
570
631
    """
571
 
    assert encoding is not None, 'you cannot specify None for the display encoding.'
 
632
    if encoding is None:
 
633
        raise ValueError('you cannot specify None for the display encoding')
572
634
    if url.startswith('file://'):
573
635
        try:
574
636
            path = local_path_from_url(url)
629
691
            return from_location[sep+1:]
630
692
        else:
631
693
            return from_location
 
694
 
 
695
 
 
696
def _is_absolute(url):
 
697
    return (osutils.pathjoin('/foo', url) == url)
 
698
 
 
699
 
 
700
def rebase_url(url, old_base, new_base):
 
701
    """Convert a relative path from an old base URL to a new base URL.
 
702
 
 
703
    The result will be a relative path.
 
704
    Absolute paths and full URLs are returned unaltered.
 
705
    """
 
706
    scheme, separator = _find_scheme_and_separator(url)
 
707
    if scheme is not None:
 
708
        return url
 
709
    if _is_absolute(url):
 
710
        return url
 
711
    old_parsed = urlparse.urlparse(old_base)
 
712
    new_parsed = urlparse.urlparse(new_base)
 
713
    if (old_parsed[:2]) != (new_parsed[:2]):
 
714
        raise errors.InvalidRebaseURLs(old_base, new_base)
 
715
    return determine_relative_path(new_parsed[2],
 
716
                                   join(old_parsed[2], url))
 
717
 
 
718
 
 
719
def determine_relative_path(from_path, to_path):
 
720
    """Determine a relative path from from_path to to_path."""
 
721
    from_segments = osutils.splitpath(from_path)
 
722
    to_segments = osutils.splitpath(to_path)
 
723
    count = -1
 
724
    for count, (from_element, to_element) in enumerate(zip(from_segments,
 
725
                                                       to_segments)):
 
726
        if from_element != to_element:
 
727
            break
 
728
    else:
 
729
        count += 1
 
730
    unique_from = from_segments[count:]
 
731
    unique_to = to_segments[count:]
 
732
    segments = (['..'] * len(unique_from) + unique_to)
 
733
    if len(segments) == 0:
 
734
        return '.'
 
735
    return osutils.pathjoin(*segments)
 
736
 
 
737
 
 
738
class URL(object):
 
739
    """Parsed URL."""
 
740
 
 
741
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
 
742
            port, quoted_path):
 
743
        self.scheme = scheme
 
744
        self.quoted_host = quoted_host
 
745
        self.host = urllib.unquote(self.quoted_host)
 
746
        self.quoted_user = quoted_user
 
747
        if self.quoted_user is not None:
 
748
            self.user = urllib.unquote(self.quoted_user)
 
749
        else:
 
750
            self.user = None
 
751
        self.quoted_password = quoted_password
 
752
        if self.quoted_password is not None:
 
753
            self.password = urllib.unquote(self.quoted_password)
 
754
        else:
 
755
            self.password = None
 
756
        self.port = port
 
757
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
 
758
        self.path = urllib.unquote(self.quoted_path)
 
759
 
 
760
    def __eq__(self, other):
 
761
        return (isinstance(other, self.__class__) and
 
762
                self.scheme == other.scheme and
 
763
                self.host == other.host and
 
764
                self.user == other.user and
 
765
                self.password == other.password and
 
766
                self.path == other.path)
 
767
 
 
768
    def __repr__(self):
 
769
        return "<%s(%r, %r, %r, %r, %r, %r)>" % (
 
770
            self.__class__.__name__,
 
771
            self.scheme, self.quoted_user, self.quoted_password,
 
772
            self.quoted_host, self.port, self.quoted_path)
 
773
 
 
774
    @classmethod
 
775
    def from_string(cls, url):
 
776
        """Create a URL object from a string.
 
777
 
 
778
        :param url: URL as bytestring
 
779
        """
 
780
        if isinstance(url, unicode):
 
781
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
782
        url = url.encode('utf-8')
 
783
        (scheme, netloc, path, params,
 
784
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
785
        user = password = host = port = None
 
786
        if '@' in netloc:
 
787
            user, host = netloc.rsplit('@', 1)
 
788
            if ':' in user:
 
789
                user, password = user.split(':', 1)
 
790
        else:
 
791
            host = netloc
 
792
 
 
793
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
 
794
            # there *is* port
 
795
            host, port = host.rsplit(':',1)
 
796
            try:
 
797
                port = int(port)
 
798
            except ValueError:
 
799
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
800
                                        (port, url))
 
801
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
802
            host = host[1:-1]
 
803
 
 
804
        return cls(scheme, user, password, host, port, path)
 
805
 
 
806
    def __str__(self):
 
807
        netloc = self.quoted_host
 
808
        if ":" in netloc:
 
809
            netloc = "[%s]" % netloc
 
810
        if self.quoted_user is not None:
 
811
            # Note that we don't put the password back even if we
 
812
            # have one so that it doesn't get accidentally
 
813
            # exposed.
 
814
            netloc = '%s@%s' % (self.quoted_user, netloc)
 
815
        if self.port is not None:
 
816
            netloc = '%s:%d' % (netloc, self.port)
 
817
        return urlparse.urlunparse(
 
818
            (self.scheme, netloc, self.quoted_path, None, None, None))
 
819
 
 
820
    @staticmethod
 
821
    def _combine_paths(base_path, relpath):
 
822
        """Transform a Transport-relative path to a remote absolute path.
 
823
 
 
824
        This does not handle substitution of ~ but does handle '..' and '.'
 
825
        components.
 
826
 
 
827
        Examples::
 
828
 
 
829
            t._combine_paths('/home/sarah', 'project/foo')
 
830
                => '/home/sarah/project/foo'
 
831
            t._combine_paths('/home/sarah', '../../etc')
 
832
                => '/etc'
 
833
            t._combine_paths('/home/sarah', '/etc')
 
834
                => '/etc'
 
835
 
 
836
        :param base_path: base path
 
837
        :param relpath: relative url string for relative part of remote path.
 
838
        :return: urlencoded string for final path.
 
839
        """
 
840
        if not isinstance(relpath, str):
 
841
            raise errors.InvalidURL(relpath)
 
842
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
 
843
        if relpath.startswith('/'):
 
844
            base_parts = []
 
845
        else:
 
846
            base_parts = base_path.split('/')
 
847
        if len(base_parts) > 0 and base_parts[-1] == '':
 
848
            base_parts = base_parts[:-1]
 
849
        for p in relpath.split('/'):
 
850
            if p == '..':
 
851
                if len(base_parts) == 0:
 
852
                    # In most filesystems, a request for the parent
 
853
                    # of root, just returns root.
 
854
                    continue
 
855
                base_parts.pop()
 
856
            elif p == '.':
 
857
                continue # No-op
 
858
            elif p != '':
 
859
                base_parts.append(p)
 
860
        path = '/'.join(base_parts)
 
861
        if not path.startswith('/'):
 
862
            path = '/' + path
 
863
        return path
 
864
 
 
865
    def clone(self, offset=None):
 
866
        """Return a new URL for a path relative to this URL.
 
867
 
 
868
        :param offset: A relative path, already urlencoded
 
869
        :return: `URL` instance
 
870
        """
 
871
        if offset is not None:
 
872
            relative = unescape(offset).encode('utf-8')
 
873
            path = self._combine_paths(self.path, relative)
 
874
            path = urllib.quote(path, safe="/~")
 
875
        else:
 
876
            path = self.quoted_path
 
877
        return self.__class__(self.scheme, self.quoted_user,
 
878
                self.quoted_password, self.quoted_host, self.port,
 
879
                path)
 
880
 
 
881
 
 
882
def parse_url(url):
 
883
    """Extract the server address, the credentials and the path from the url.
 
884
 
 
885
    user, password, host and path should be quoted if they contain reserved
 
886
    chars.
 
887
 
 
888
    :param url: an quoted url
 
889
    :return: (scheme, user, password, host, port, path) tuple, all fields
 
890
        are unquoted.
 
891
    """
 
892
    parsed_url = URL.from_string(url)
 
893
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
 
894
        parsed_url.host, parsed_url.port, parsed_url.path)