/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:
22
22
 
23
23
from bzrlib.lazy_import import lazy_import
24
24
lazy_import(globals(), """
25
 
from posixpath import split as _posix_split
 
25
from posixpath import split as _posix_split, normpath as _posix_normpath
26
26
import urllib
27
27
import urlparse
28
28
 
78
78
        raise ValueError('Length of base (%r) must equal or'
79
79
            ' exceed the platform minimum url length (which is %d)' %
80
80
            (base, MIN_ABS_FILEURL_LENGTH))
81
 
    base = osutils.normpath(local_path_from_url(base))
82
 
    path = osutils.normpath(local_path_from_url(path))
 
81
    base = local_path_from_url(base)
 
82
    path = local_path_from_url(path)
83
83
    return escape(osutils.relpath(base, path))
84
84
 
85
85
 
101
101
    first_path_slash = path.find('/')
102
102
    if first_path_slash == -1:
103
103
        return len(scheme), None
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
 
104
    return len(scheme), first_path_slash+len(scheme)+3
110
105
 
111
106
 
112
107
def join(base, *args):
123
118
    """
124
119
    if not args:
125
120
        return base
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:]
 
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
 
132
146
    for arg in args:
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
 
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 = []
143
166
        else:
 
167
            path = '/'.join(path)
144
168
            path = joinpath(path, arg)
145
 
    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)
146
182
 
147
183
 
148
184
def joinpath(base, *args):
181
217
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
182
218
def _posix_local_path_from_url(url):
183
219
    """Convert a url like file:///path/to/foo into /path/to/foo"""
184
 
    url = split_segment_parameters_raw(url)[0]
185
220
    file_localhost_prefix = 'file://localhost/'
186
221
    if url.startswith(file_localhost_prefix):
187
222
        path = url[len(file_localhost_prefix) - 1:]
201
236
    """
202
237
    # importing directly from posixpath allows us to test this
203
238
    # on non-posix platforms
204
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
239
    return 'file://' + escape(_posix_normpath(
 
240
        osutils._posix_abspath(path)))
205
241
 
206
242
 
207
243
def _win32_local_path_from_url(url):
209
245
    if not url.startswith('file://'):
210
246
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
211
247
                                     'UNC path urls must start with file://')
212
 
    url = split_segment_parameters_raw(url)[0]
213
248
    # We strip off all 3 slashes
214
249
    win32_url = url[len('file:'):]
215
250
    # check for UNC path: //HOST/path
225
260
        return '/'
226
261
 
227
262
    # usual local path with drive letter
228
 
    if (len(win32_url) < 6
229
 
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
230
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
263
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
264
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
231
265
        or win32_url[4] not in  '|:'
232
266
        or win32_url[5] != '/'):
233
267
        raise errors.InvalidURL(url, 'Win32 file urls start with'
244
278
    # on non-win32 platform
245
279
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
246
280
    #       which actually strips trailing space characters.
247
 
    #       The worst part is that on linux ntpath.abspath has different
 
281
    #       The worst part is that under linux ntpath.abspath has different
248
282
    #       semantics, since 'nt' is not an available module.
249
283
    if path == '/':
250
284
        return 'file:///'
269
303
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
270
304
 
271
305
 
272
 
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
 
306
_url_scheme_re = re.compile(r'^(?P<scheme>[^:/]{2,})://(?P<path>.*)$')
273
307
_url_hex_escapes_re = re.compile(r'(%[0-9a-fA-F]{2})')
274
308
 
275
309
 
305
339
    :param url: Either a hybrid URL or a local path
306
340
    :return: A normalized URL which only includes 7-bit ASCII characters.
307
341
    """
308
 
    scheme_end, path_start = _find_scheme_and_separator(url)
309
 
    if scheme_end is None:
 
342
    m = _url_scheme_re.match(url)
 
343
    if not m:
310
344
        return local_path_to_url(url)
311
 
    prefix = url[:path_start]
312
 
    path = url[path_start:]
 
345
    scheme = m.group('scheme')
 
346
    path = m.group('path')
313
347
    if not isinstance(url, unicode):
314
348
        for c in url:
315
349
            if c not in _url_safe_characters:
316
350
                raise errors.InvalidURL(url, 'URLs can only contain specific'
317
351
                                            ' safe characters (not %r)' % c)
318
352
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
319
 
        return str(prefix + ''.join(path))
 
353
        return str(scheme + '://' + ''.join(path))
320
354
 
321
355
    # We have a unicode (hybrid) url
322
356
    path_chars = list(path)
328
362
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
329
363
    path = ''.join(path_chars)
330
364
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
331
 
    return str(prefix + path)
 
365
    return str(scheme + '://' + path)
332
366
 
333
367
 
334
368
def relative_url(base, other):
387
421
    """On win32 the drive letter needs to be added to the url base."""
388
422
    # Strip off the drive letter
389
423
    # path is currently /C:/foo
390
 
    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] != '/':
391
425
        raise errors.InvalidURL(url_base + path,
392
426
            'win32 file:/// paths need a drive letter')
393
427
    url_base += path[0:3] # file:// + /C:
435
469
    return url_base + head, tail
436
470
 
437
471
 
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
 
 
514
472
def _win32_strip_local_trailing_slash(url):
515
473
    """Strip slashes after the drive letter"""
516
474
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
735
693
    return osutils.pathjoin(*segments)
736
694
 
737
695
 
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
696
 
882
697
def parse_url(url):
883
698
    """Extract the server address, the credentials and the path from the url.
886
701
    chars.
887
702
 
888
703
    :param url: an quoted url
 
704
 
889
705
    :return: (scheme, user, password, host, port, path) tuple, all fields
890
706
        are unquoted.
891
707
    """
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)
 
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)