~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

  • Committer: Robert J. Tanner
  • Date: 2009-04-29 05:53:21 UTC
  • mfrom: (4311 +trunk)
  • mto: This revision was merged to the branch mainline in revision 4312.
  • Revision ID: tanner@real-time.com-20090429055321-v2s5l1mgki9f6cgn
[merge] 1.14 back to trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Bazaar -- distributed version control
2
 
#
3
 
# Copyright (C) 2006 by Canonical Ltd
 
1
# Copyright (C) 2006, 2008 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
 
21
19
import os
22
 
from posixpath import split as _posix_split, normpath as _posix_normpath
23
20
import re
24
21
import sys
 
22
 
 
23
from bzrlib.lazy_import import lazy_import
 
24
lazy_import(globals(), """
 
25
from posixpath import split as _posix_split, normpath as _posix_normpath
25
26
import urllib
 
27
import urlparse
26
28
 
27
 
import bzrlib.errors as errors
28
 
import bzrlib.osutils
 
29
from bzrlib import (
 
30
    errors,
 
31
    osutils,
 
32
    )
 
33
""")
29
34
 
30
35
 
31
36
def basename(url, exclude_trailing_slash=True):
61
66
        relpath = relpath.encode('utf-8')
62
67
    # After quoting and encoding, the path should be perfectly
63
68
    # safe as a plain ASCII string, str() just enforces this
64
 
    return str(urllib.quote(relpath))
 
69
    return str(urllib.quote(relpath, safe='/~'))
65
70
 
66
71
 
67
72
def file_relpath(base, path):
68
73
    """Compute just the relative sub-portion of a url
69
 
    
 
74
 
70
75
    This assumes that both paths are already fully specified file:// URLs.
71
76
    """
72
 
    assert len(base) >= MIN_ABS_FILEURL_LENGTH, ('Length of base must be equal or'
73
 
        ' exceed the platform minimum url length (which is %d)' % 
74
 
        MIN_ABS_FILEURL_LENGTH)
75
 
 
 
77
    if len(base) < MIN_ABS_FILEURL_LENGTH:
 
78
        raise ValueError('Length of base must be equal or'
 
79
            ' exceed the platform minimum url length (which is %d)' %
 
80
            MIN_ABS_FILEURL_LENGTH)
76
81
    base = local_path_from_url(base)
77
82
    path = local_path_from_url(path)
78
 
    return escape(bzrlib.osutils.relpath(base, path))
 
83
    return escape(osutils.relpath(base, path))
79
84
 
80
85
 
81
86
def _find_scheme_and_separator(url):
111
116
        join('http://foo', 'bar') => 'http://foo/bar'
112
117
        join('http://foo', 'bar', '../baz') => 'http://foo/baz'
113
118
    """
114
 
    m = _url_scheme_re.match(base)
 
119
    if not args:
 
120
        return base
 
121
    match = _url_scheme_re.match(base)
115
122
    scheme = None
116
 
    if m:
117
 
        scheme = m.group('scheme')
118
 
        path = m.group('path').split('/')
 
123
    if match:
 
124
        scheme = match.group('scheme')
 
125
        path = match.group('path').split('/')
119
126
        if path[-1:] == ['']:
120
127
            # Strip off a trailing slash
121
128
            # This helps both when we are at the root, and when
124
131
    else:
125
132
        path = base.split('/')
126
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
 
127
146
    for arg in args:
128
 
        m = _url_scheme_re.match(arg)
129
 
        if m:
 
147
        match = _url_scheme_re.match(arg)
 
148
        if match:
130
149
            # Absolute URL
131
 
            scheme = m.group('scheme')
132
 
            path = m.group('path').split('/')
 
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 = []
133
166
        else:
134
 
            for chunk in arg.split('/'):
135
 
                if chunk == '.':
136
 
                    continue
137
 
                elif chunk == '..':
138
 
                    if len(path) >= 2:
139
 
                        # Don't pop off the host portion
140
 
                        path.pop()
141
 
                    else:
142
 
                        raise errors.InvalidURLJoin('Cannot go above root',
143
 
                                base, args)
144
 
                else:
145
 
                    path.append(chunk)
 
167
            path = '/'.join(path)
 
168
            path = joinpath(path, arg)
 
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
146
178
 
147
179
    if scheme is None:
148
180
        return '/'.join(path)
149
181
    return scheme + '://' + '/'.join(path)
150
182
 
151
183
 
 
184
def joinpath(base, *args):
 
185
    """Join URL path segments to a URL path segment.
 
186
 
 
187
    This is somewhat like osutils.joinpath, but intended for URLs.
 
188
 
 
189
    XXX: this duplicates some normalisation logic, and also duplicates a lot of
 
190
    path handling logic that already exists in some Transport implementations.
 
191
    We really should try to have exactly one place in the code base responsible
 
192
    for combining paths of URLs.
 
193
    """
 
194
    path = base.split('/')
 
195
    if len(path) > 1 and path[-1] == '':
 
196
        #If the path ends in a trailing /, remove it.
 
197
        path.pop()
 
198
    for arg in args:
 
199
        if arg.startswith('/'):
 
200
            path = []
 
201
        for chunk in arg.split('/'):
 
202
            if chunk == '.':
 
203
                continue
 
204
            elif chunk == '..':
 
205
                if path == ['']:
 
206
                    raise errors.InvalidURLJoin('Cannot go above root',
 
207
                            base, args)
 
208
                path.pop()
 
209
            else:
 
210
                path.append(chunk)
 
211
    if path == ['']:
 
212
        return '/'
 
213
    else:
 
214
        return '/'.join(path)
 
215
 
 
216
 
152
217
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
153
218
def _posix_local_path_from_url(url):
154
219
    """Convert a url like file:///path/to/foo into /path/to/foo"""
163
228
 
164
229
    This also handles transforming escaping unicode characters, etc.
165
230
    """
166
 
    # importing directly from posixpath allows us to test this 
 
231
    # importing directly from posixpath allows us to test this
167
232
    # on non-posix platforms
168
233
    return 'file://' + escape(_posix_normpath(
169
 
        bzrlib.osutils._posix_abspath(path)))
 
234
        osutils._posix_abspath(path)))
170
235
 
171
236
 
172
237
def _win32_local_path_from_url(url):
173
238
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
174
 
    if not url.startswith('file:///'):
175
 
        raise errors.InvalidURL(url, 'local urls must start with file:///')
 
239
    if not url.startswith('file://'):
 
240
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
 
241
                                     'UNC path urls must start with file://')
176
242
    # We strip off all 3 slashes
177
 
    win32_url = url[len('file:///'):]
178
 
    if (win32_url[0] not in ('abcdefghijklmnopqrstuvwxyz'
 
243
    win32_url = url[len('file:'):]
 
244
    # check for UNC path: //HOST/path
 
245
    if not win32_url.startswith('///'):
 
246
        if (win32_url[2] == '/'
 
247
            or win32_url[3] in '|:'):
 
248
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
 
249
                ' have form file://HOST/path')
 
250
        return unescape(win32_url)
 
251
 
 
252
    # allow empty paths so we can serve all roots
 
253
    if win32_url == '///':
 
254
        return '/'
 
255
 
 
256
    # usual local path with drive letter
 
257
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
179
258
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
180
 
        or win32_url[1] not in  '|:'
181
 
        or win32_url[2] != '/'):
 
259
        or win32_url[4] not in  '|:'
 
260
        or win32_url[5] != '/'):
182
261
        raise errors.InvalidURL(url, 'Win32 file urls start with'
183
262
                ' file:///x:/, where x is a valid drive letter')
184
 
    return win32_url[0].upper() + u':' + unescape(win32_url[2:])
 
263
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
185
264
 
186
265
 
187
266
def _win32_local_path_to_url(path):
189
268
 
190
269
    This also handles transforming escaping unicode characters, etc.
191
270
    """
192
 
    # importing directly from ntpath allows us to test this 
 
271
    # importing directly from ntpath allows us to test this
193
272
    # on non-win32 platform
194
273
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
195
274
    #       which actually strips trailing space characters.
196
275
    #       The worst part is that under linux ntpath.abspath has different
197
276
    #       semantics, since 'nt' is not an available module.
198
 
    win32_path = bzrlib.osutils._nt_normpath(
199
 
        bzrlib.osutils._win32_abspath(path)).replace('\\', '/')
200
 
    return 'file:///' + win32_path[0].upper() + ':' + escape(win32_path[2:])
 
277
    if path == '/':
 
278
        return 'file:///'
 
279
 
 
280
    win32_path = osutils._win32_abspath(path)
 
281
    # check for UNC path \\HOST\path
 
282
    if win32_path.startswith('//'):
 
283
        return 'file:' + escape(win32_path)
 
284
    return ('file:///' + str(win32_path[0].upper()) + ':' +
 
285
        escape(win32_path[2:]))
201
286
 
202
287
 
203
288
local_path_to_url = _posix_local_path_to_url
213
298
 
214
299
 
215
300
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,})://(?P<path>.*)$')
 
301
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
 
302
 
 
303
 
 
304
def _unescape_safe_chars(matchobj):
 
305
    """re.sub callback to convert hex-escapes to plain characters (if safe).
 
306
 
 
307
    e.g. '%7E' will be converted to '~'.
 
308
    """
 
309
    hex_digits = matchobj.group(0)[1:]
 
310
    char = chr(int(hex_digits, 16))
 
311
    if char in _url_dont_escape_characters:
 
312
        return char
 
313
    else:
 
314
        return matchobj.group(0).upper()
216
315
 
217
316
 
218
317
def normalize_url(url):
219
318
    """Make sure that a path string is in fully normalized URL form.
220
 
    
221
 
    This handles URLs which have unicode characters, spaces, 
 
319
 
 
320
    This handles URLs which have unicode characters, spaces,
222
321
    special characters, etc.
223
322
 
224
323
    It has two basic modes of operation, depending on whether the
237
336
    m = _url_scheme_re.match(url)
238
337
    if not m:
239
338
        return local_path_to_url(url)
 
339
    scheme = m.group('scheme')
 
340
    path = m.group('path')
240
341
    if not isinstance(url, unicode):
241
342
        for c in url:
242
343
            if c not in _url_safe_characters:
243
344
                raise errors.InvalidURL(url, 'URLs can only contain specific'
244
345
                                            ' safe characters (not %r)' % c)
245
 
        return url
 
346
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
 
347
        return str(scheme + '://' + ''.join(path))
 
348
 
246
349
    # We have a unicode (hybrid) url
247
 
    scheme = m.group('scheme')
248
 
    path = list(m.group('path'))
 
350
    path_chars = list(path)
249
351
 
250
 
    for i in xrange(len(path)):
251
 
        if path[i] not in _url_safe_characters:
252
 
            chars = path[i].encode('utf-8')
253
 
            path[i] = ''.join(['%%%02X' % ord(c) for c in path[i].encode('utf-8')])
254
 
    return scheme + '://' + ''.join(path)
 
352
    for i in xrange(len(path_chars)):
 
353
        if path_chars[i] not in _url_safe_characters:
 
354
            chars = path_chars[i].encode('utf-8')
 
355
            path_chars[i] = ''.join(
 
356
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
 
357
    path = ''.join(path_chars)
 
358
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
 
359
    return str(scheme + '://' + path)
255
360
 
256
361
 
257
362
def relative_url(base, other):
263
368
    dummy, base_first_slash = _find_scheme_and_separator(base)
264
369
    if base_first_slash is None:
265
370
        return other
266
 
    
 
371
 
267
372
    dummy, other_first_slash = _find_scheme_and_separator(other)
268
373
    if other_first_slash is None:
269
374
        return other
273
378
    other_scheme = other[:other_first_slash]
274
379
    if base_scheme != other_scheme:
275
380
        return other
 
381
    elif sys.platform == 'win32' and base_scheme == 'file://':
 
382
        base_drive = base[base_first_slash+1:base_first_slash+3]
 
383
        other_drive = other[other_first_slash+1:other_first_slash+3]
 
384
        if base_drive != other_drive:
 
385
            return other
276
386
 
277
387
    base_path = base[base_first_slash+1:]
278
388
    other_path = other[other_first_slash+1:]
306
416
    # Strip off the drive letter
307
417
    # path is currently /C:/foo
308
418
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
309
 
        raise errors.InvalidURL(url_base + path, 
 
419
        raise errors.InvalidURL(url_base + path,
310
420
            'win32 file:/// paths need a drive letter')
311
421
    url_base += path[0:3] # file:// + /C:
312
422
    path = path[3:] # /foo
320
430
    :param exclude_trailing_slash: Strip off a final '/' if it is part
321
431
        of the path (but not if it is part of the protocol specification)
322
432
 
323
 
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at 
 
433
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
324
434
        the root.
325
435
    """
326
436
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
385
495
    if not url.endswith('/'):
386
496
        # Nothing to do
387
497
        return url
388
 
    if sys.platform == 'win32' and url.startswith('file:///'):
 
498
    if sys.platform == 'win32' and url.startswith('file://'):
389
499
        return _win32_strip_local_trailing_slash(url)
390
500
 
391
501
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
430
540
# These are characters that if escaped, should stay that way
431
541
_no_decode_chars = ';/?:@&=+$,#'
432
542
_no_decode_ords = [ord(c) for c in _no_decode_chars]
433
 
_no_decode_hex = (['%02x' % o for o in _no_decode_ords] 
 
543
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
434
544
                + ['%02X' % o for o in _no_decode_ords])
435
545
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
436
546
                    + [('%02X' % o, chr(o)) for o in range(256)]))
437
547
#These entries get mapped to themselves
438
548
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
439
549
 
 
550
# These characters shouldn't be percent-encoded, and it's always safe to
 
551
# unencode them if they are.
 
552
_url_dont_escape_characters = set(
 
553
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
554
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
555
   "0123456789" # Numbers
 
556
   "-._~"  # Unreserved characters
 
557
)
 
558
 
440
559
# These characters should not be escaped
441
 
_url_safe_characters = set('abcdefghijklmnopqrstuvwxyz'
442
 
                        'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
443
 
                        '0123456789' '_.-/'
444
 
                        ';?:@&=+$,%#')
445
 
 
 
560
_url_safe_characters = set(
 
561
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
562
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
563
   "0123456789" # Numbers
 
564
   "_.-!~*'()"  # Unreserved characters
 
565
   "/;?:@&=+$," # Reserved characters
 
566
   "%#"         # Extra reserved characters
 
567
)
446
568
 
447
569
def unescape_for_display(url, encoding):
448
570
    """Decode what you can for a URL, so that we get a nice looking path.
450
572
    This will turn file:// urls into local paths, and try to decode
451
573
    any portions of a http:// style url that it can.
452
574
 
453
 
    Any sections of the URL which can't be represented in the encoding or 
 
575
    Any sections of the URL which can't be represented in the encoding or
454
576
    need to stay as escapes are left alone.
455
577
 
456
578
    :param url: A 7-bit ASCII URL
457
579
    :param encoding: The final output encoding
458
580
 
459
 
    :return: A unicode string which can be safely encoded into the 
 
581
    :return: A unicode string which can be safely encoded into the
460
582
         specified encoding.
461
583
    """
462
 
    assert encoding is not None, 'you cannot specify None for the display encoding.'
 
584
    if encoding is None:
 
585
        raise ValueError('you cannot specify None for the display encoding')
463
586
    if url.startswith('file://'):
464
587
        try:
465
588
            path = local_path_from_url(url)
499
622
                # Otherwise take the url decoded one
500
623
                res[i] = decoded
501
624
    return u'/'.join(res)
 
625
 
 
626
 
 
627
def derive_to_location(from_location):
 
628
    """Derive a TO_LOCATION given a FROM_LOCATION.
 
629
 
 
630
    The normal case is a FROM_LOCATION of http://foo/bar => bar.
 
631
    The Right Thing for some logical destinations may differ though
 
632
    because no / may be present at all. In that case, the result is
 
633
    the full name without the scheme indicator, e.g. lp:foo-bar => foo-bar.
 
634
    This latter case also applies when a Windows drive
 
635
    is used without a path, e.g. c:foo-bar => foo-bar.
 
636
    If no /, path separator or : is found, the from_location is returned.
 
637
    """
 
638
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
 
639
        return os.path.basename(from_location.rstrip("/\\"))
 
640
    else:
 
641
        sep = from_location.find(":")
 
642
        if sep > 0:
 
643
            return from_location[sep+1:]
 
644
        else:
 
645
            return from_location
 
646
 
 
647
 
 
648
def _is_absolute(url):
 
649
    return (osutils.pathjoin('/foo', url) == url)
 
650
 
 
651
 
 
652
def rebase_url(url, old_base, new_base):
 
653
    """Convert a relative path from an old base URL to a new base URL.
 
654
 
 
655
    The result will be a relative path.
 
656
    Absolute paths and full URLs are returned unaltered.
 
657
    """
 
658
    scheme, separator = _find_scheme_and_separator(url)
 
659
    if scheme is not None:
 
660
        return url
 
661
    if _is_absolute(url):
 
662
        return url
 
663
    old_parsed = urlparse.urlparse(old_base)
 
664
    new_parsed = urlparse.urlparse(new_base)
 
665
    if (old_parsed[:2]) != (new_parsed[:2]):
 
666
        raise errors.InvalidRebaseURLs(old_base, new_base)
 
667
    return determine_relative_path(new_parsed[2],
 
668
                                   join(old_parsed[2], url))
 
669
 
 
670
 
 
671
def determine_relative_path(from_path, to_path):
 
672
    """Determine a relative path from from_path to to_path."""
 
673
    from_segments = osutils.splitpath(from_path)
 
674
    to_segments = osutils.splitpath(to_path)
 
675
    count = -1
 
676
    for count, (from_element, to_element) in enumerate(zip(from_segments,
 
677
                                                       to_segments)):
 
678
        if from_element != to_element:
 
679
            break
 
680
    else:
 
681
        count += 1
 
682
    unique_from = from_segments[count:]
 
683
    unique_to = to_segments[count:]
 
684
    segments = (['..'] * len(unique_from) + unique_to)
 
685
    if len(segments) == 0:
 
686
        return '.'
 
687
    return osutils.pathjoin(*segments)
 
688
 
 
689
 
 
690
 
 
691
def parse_url(url):
 
692
    """Extract the server address, the credentials and the path from the url.
 
693
 
 
694
    user, password, host and path should be quoted if they contain reserved
 
695
    chars.
 
696
 
 
697
    :param url: an quoted url
 
698
 
 
699
    :return: (scheme, user, password, host, port, path) tuple, all fields
 
700
        are unquoted.
 
701
    """
 
702
    if isinstance(url, unicode):
 
703
        raise errors.InvalidURL('should be ascii:\n%r' % url)
 
704
    url = url.encode('utf-8')
 
705
    (scheme, netloc, path, params,
 
706
     query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
707
    user = password = host = port = None
 
708
    if '@' in netloc:
 
709
        user, host = netloc.rsplit('@', 1)
 
710
        if ':' in user:
 
711
            user, password = user.split(':', 1)
 
712
            password = urllib.unquote(password)
 
713
        user = urllib.unquote(user)
 
714
    else:
 
715
        host = netloc
 
716
 
 
717
    if ':' in host and not (host[0] == '[' and host[-1] == ']'): #there *is* port
 
718
        host, port = host.rsplit(':',1)
 
719
        try:
 
720
            port = int(port)
 
721
        except ValueError:
 
722
            raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
723
                                    (port, url))
 
724
    if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
725
        host = host[1:-1]
 
726
 
 
727
    host = urllib.unquote(host)
 
728
    path = urllib.unquote(path)
 
729
 
 
730
    return (scheme, user, password, host, port, path)