~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

  • Committer: Martin Pool
  • Date: 2010-07-16 15:20:17 UTC
  • mfrom: (5346.3.1 pathnotchild)
  • mto: This revision was merged to the branch mainline in revision 5351.
  • Revision ID: mbp@canonical.com-20100716152017-t4c73h9y1uoih7fb
PathNotChild should not give a traceback.

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
77
75
    This assumes that both paths are already fully specified file:// URLs.
78
76
    """
79
77
    if len(base) < MIN_ABS_FILEURL_LENGTH:
80
 
        raise ValueError('Length of base must be equal or'
 
78
        raise ValueError('Length of base (%r) must equal or'
81
79
            ' exceed the platform minimum url length (which is %d)' %
82
 
            MIN_ABS_FILEURL_LENGTH)
 
80
            (base, MIN_ABS_FILEURL_LENGTH))
83
81
    base = local_path_from_url(base)
84
82
    path = local_path_from_url(path)
85
83
    return escape(osutils.relpath(base, path))
103
101
    first_path_slash = path.find('/')
104
102
    if first_path_slash == -1:
105
103
        return len(scheme), None
106
 
    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
107
110
 
108
111
 
109
112
def join(base, *args):
120
123
    """
121
124
    if not args:
122
125
        return base
123
 
    match = _url_scheme_re.match(base)
124
 
    scheme = None
125
 
    if match:
126
 
        scheme = match.group('scheme')
127
 
        path = match.group('path').split('/')
128
 
        if path[-1:] == ['']:
129
 
            # Strip off a trailing slash
130
 
            # This helps both when we are at the root, and when
131
 
            # 'base' has an extra slash at the end
132
 
            path = path[:-1]
133
 
    else:
134
 
        path = base.split('/')
135
 
 
136
 
    if scheme is not None and len(path) >= 1:
137
 
        host = path[:1]
138
 
        # the path should be represented as an abs path.
139
 
        # we know this must be absolute because of the presence of a URL scheme.
140
 
        remove_root = True
141
 
        path = [''] + path[1:]
142
 
    else:
143
 
        # create an empty host, but dont alter the path - this might be a
144
 
        # relative url fragment.
145
 
        host = []
146
 
        remove_root = False
147
 
 
 
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:]
148
132
    for arg in args:
149
 
        match = _url_scheme_re.match(arg)
150
 
        if match:
151
 
            # Absolute URL
152
 
            scheme = match.group('scheme')
153
 
            # this skips .. normalisation, making http://host/../../..
154
 
            # be rather strange.
155
 
            path = match.group('path').split('/')
156
 
            # set the host and path according to new absolute URL, discarding
157
 
            # any previous values.
158
 
            # XXX: duplicates mess from earlier in this function.  This URL
159
 
            # manipulation code needs some cleaning up.
160
 
            if scheme is not None and len(path) >= 1:
161
 
                host = path[:1]
162
 
                path = path[1:]
163
 
                # url scheme implies absolute path.
164
 
                path = [''] + path
165
 
            else:
166
 
                # no url scheme we take the path as is.
167
 
                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
168
143
        else:
169
 
            path = '/'.join(path)
170
144
            path = joinpath(path, arg)
171
 
            path = path.split('/')
172
 
    if remove_root and path[0:1] == ['']:
173
 
        del path[0]
174
 
    if host:
175
 
        # Remove the leading slash from the path, so long as it isn't also the
176
 
        # trailing slash, which we want to keep if present.
177
 
        if path and path[0] == '' and len(path) > 1:
178
 
            del path[0]
179
 
        path = host + path
180
 
 
181
 
    if scheme is None:
182
 
        return '/'.join(path)
183
 
    return scheme + '://' + '/'.join(path)
 
145
    return base[:path_start] + path
184
146
 
185
147
 
186
148
def joinpath(base, *args):
219
181
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
220
182
def _posix_local_path_from_url(url):
221
183
    """Convert a url like file:///path/to/foo into /path/to/foo"""
222
 
    if not url.startswith('file:///'):
223
 
        raise errors.InvalidURL(url, 'local urls must start with file:///')
 
184
    file_localhost_prefix = 'file://localhost/'
 
185
    if url.startswith(file_localhost_prefix):
 
186
        path = url[len(file_localhost_prefix) - 1:]
 
187
    elif not url.startswith('file:///'):
 
188
        raise errors.InvalidURL(
 
189
            url, 'local urls must start with file:/// or file://localhost/')
 
190
    else:
 
191
        path = url[len('file://'):]
224
192
    # We only strip off 2 slashes
225
 
    return unescape(url[len('file://'):])
 
193
    return unescape(path)
226
194
 
227
195
 
228
196
def _posix_local_path_to_url(path):
274
242
    # on non-win32 platform
275
243
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
276
244
    #       which actually strips trailing space characters.
277
 
    #       The worst part is that under linux ntpath.abspath has different
 
245
    #       The worst part is that on linux ntpath.abspath has different
278
246
    #       semantics, since 'nt' is not an available module.
279
247
    if path == '/':
280
248
        return 'file:///'
299
267
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
300
268
 
301
269
 
302
 
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,})://(?P<path>.*)$')
 
270
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
303
271
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
304
272
 
305
273
 
335
303
    :param url: Either a hybrid URL or a local path
336
304
    :return: A normalized URL which only includes 7-bit ASCII characters.
337
305
    """
338
 
    m = _url_scheme_re.match(url)
339
 
    if not m:
 
306
    scheme_end, path_start = _find_scheme_and_separator(url)
 
307
    if scheme_end is None:
340
308
        return local_path_to_url(url)
341
 
    scheme = m.group('scheme')
342
 
    path = m.group('path')
 
309
    prefix = url[:path_start]
 
310
    path = url[path_start:]
343
311
    if not isinstance(url, unicode):
344
312
        for c in url:
345
313
            if c not in _url_safe_characters:
346
314
                raise errors.InvalidURL(url, 'URLs can only contain specific'
347
315
                                            ' safe characters (not %r)' % c)
348
316
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
349
 
        return str(scheme + '://' + ''.join(path))
 
317
        return str(prefix + ''.join(path))
350
318
 
351
319
    # We have a unicode (hybrid) url
352
320
    path_chars = list(path)
358
326
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
359
327
    path = ''.join(path_chars)
360
328
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
361
 
    return str(scheme + '://' + path)
 
329
    return str(prefix + path)
362
330
 
363
331
 
364
332
def relative_url(base, other):
465
433
    return url_base + head, tail
466
434
 
467
435
 
 
436
def split_segment_parameters_raw(url):
 
437
    """Split the subsegment of the last segment of a URL.
 
438
 
 
439
    :param url: A relative or absolute URL
 
440
    :return: (url, subsegments)
 
441
    """
 
442
    (parent_url, child_dir) = split(url)
 
443
    subsegments = child_dir.split(",")
 
444
    if len(subsegments) == 1:
 
445
        return (url, [])
 
446
    return (join(parent_url, subsegments[0]), subsegments[1:])
 
447
 
 
448
 
 
449
def split_segment_parameters(url):
 
450
    """Split the segment parameters of the last segment of a URL.
 
451
 
 
452
    :param url: A relative or absolute URL
 
453
    :return: (url, segment_parameters)
 
454
    """
 
455
    (base_url, subsegments) = split_segment_parameters_raw(url)
 
456
    parameters = {}
 
457
    for subsegment in subsegments:
 
458
        (key, value) = subsegment.split("=", 1)
 
459
        parameters[key] = value
 
460
    return (base_url, parameters)
 
461
 
 
462
 
 
463
def join_segment_parameters_raw(base, *subsegments):
 
464
    """Create a new URL by adding subsegments to an existing one. 
 
465
 
 
466
    This adds the specified subsegments to the last path in the specified
 
467
    base URL. The subsegments should be bytestrings.
 
468
 
 
469
    :note: You probably want to use join_segment_parameters instead.
 
470
    """
 
471
    if not subsegments:
 
472
        return base
 
473
    for subsegment in subsegments:
 
474
        if type(subsegment) is not str:
 
475
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
 
476
        if "," in subsegment:
 
477
            raise errors.InvalidURLJoin(", exists in subsegments",
 
478
                                        base, subsegments)
 
479
    return ",".join((base,) + subsegments)
 
480
 
 
481
 
 
482
def join_segment_parameters(url, parameters):
 
483
    """Create a new URL by adding segment parameters to an existing one.
 
484
 
 
485
    The parameters of the last segment in the URL will be updated; if a
 
486
    parameter with the same key already exists it will be overwritten.
 
487
 
 
488
    :param url: A URL, as string
 
489
    :param parameters: Dictionary of parameters, keys and values as bytestrings
 
490
    """
 
491
    (base, existing_parameters) = split_segment_parameters(url)
 
492
    new_parameters = {}
 
493
    new_parameters.update(existing_parameters)
 
494
    for key, value in parameters.iteritems():
 
495
        if type(key) is not str:
 
496
            raise TypeError("parameter key %r is not a bytestring" % key)
 
497
        if type(value) is not str:
 
498
            raise TypeError("parameter value %r for %s is not a bytestring" %
 
499
                (key, value))
 
500
        if "=" in key:
 
501
            raise errors.InvalidURLJoin("= exists in parameter key", url,
 
502
                parameters)
 
503
        new_parameters[key] = value
 
504
    return join_segment_parameters_raw(base, 
 
505
        *["%s=%s" % item for item in sorted(new_parameters.items())])
 
506
 
 
507
 
468
508
def _win32_strip_local_trailing_slash(url):
469
509
    """Strip slashes after the drive letter"""
470
510
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
687
727
    if len(segments) == 0:
688
728
        return '.'
689
729
    return osutils.pathjoin(*segments)
 
730
 
 
731
 
 
732
 
 
733
def parse_url(url):
 
734
    """Extract the server address, the credentials and the path from the url.
 
735
 
 
736
    user, password, host and path should be quoted if they contain reserved
 
737
    chars.
 
738
 
 
739
    :param url: an quoted url
 
740
 
 
741
    :return: (scheme, user, password, host, port, path) tuple, all fields
 
742
        are unquoted.
 
743
    """
 
744
    if isinstance(url, unicode):
 
745
        raise errors.InvalidURL('should be ascii:\n%r' % url)
 
746
    url = url.encode('utf-8')
 
747
    (scheme, netloc, path, params,
 
748
     query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
749
    user = password = host = port = None
 
750
    if '@' in netloc:
 
751
        user, host = netloc.rsplit('@', 1)
 
752
        if ':' in user:
 
753
            user, password = user.split(':', 1)
 
754
            password = urllib.unquote(password)
 
755
        user = urllib.unquote(user)
 
756
    else:
 
757
        host = netloc
 
758
 
 
759
    if ':' in host and not (host[0] == '[' and host[-1] == ']'): #there *is* port
 
760
        host, port = host.rsplit(':',1)
 
761
        try:
 
762
            port = int(port)
 
763
        except ValueError:
 
764
            raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
765
                                    (port, url))
 
766
    if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
767
        host = host[1:-1]
 
768
 
 
769
    host = urllib.unquote(host)
 
770
    path = urllib.unquote(path)
 
771
 
 
772
    return (scheme, user, password, host, port, path)