~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

  • Committer: Andrew Bennetts
  • Date: 2010-10-08 08:15:14 UTC
  • mto: This revision was merged to the branch mainline in revision 5498.
  • Revision ID: andrew.bennetts@canonical.com-20101008081514-dviqzrdfwyzsqbz2
Split NEWS into per-release doc/en/release-notes/bzr-*.txt

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
23
from bzrlib.lazy_import import lazy_import
26
24
lazy_import(globals(), """
27
 
from posixpath import split as _posix_split
 
25
from posixpath import split as _posix_split, normpath as _posix_normpath
 
26
import urllib
28
27
import urlparse
29
28
 
30
29
from bzrlib import (
61
60
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
62
61
 
63
62
 
64
 
# Private copies of quote and unquote, copied from Python's
65
 
# urllib module because urllib unconditionally imports socket, which imports
66
 
# ssl.
67
 
 
68
 
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
69
 
               'abcdefghijklmnopqrstuvwxyz'
70
 
               '0123456789' '_.-')
71
 
_safe_map = {}
72
 
for i, c in zip(xrange(256), str(bytearray(xrange(256)))):
73
 
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
74
 
_safe_quoters = {}
75
 
 
76
 
 
77
 
def quote(s, safe='/'):
78
 
    """quote('abc def') -> 'abc%20def'
79
 
 
80
 
    Each part of a URL, e.g. the path info, the query, etc., has a
81
 
    different set of reserved characters that must be quoted.
82
 
 
83
 
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
84
 
    the following reserved characters.
85
 
 
86
 
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
87
 
                  "$" | ","
88
 
 
89
 
    Each of these characters is reserved in some component of a URL,
90
 
    but not necessarily in all of them.
91
 
 
92
 
    By default, the quote function is intended for quoting the path
93
 
    section of a URL.  Thus, it will not encode '/'.  This character
94
 
    is reserved, but in typical usage the quote function is being
95
 
    called on a path where the existing slash characters are used as
96
 
    reserved characters.
97
 
    """
98
 
    # fastpath
99
 
    if not s:
100
 
        if s is None:
101
 
            raise TypeError('None object cannot be quoted')
102
 
        return s
103
 
    cachekey = (safe, always_safe)
104
 
    try:
105
 
        (quoter, safe) = _safe_quoters[cachekey]
106
 
    except KeyError:
107
 
        safe_map = _safe_map.copy()
108
 
        safe_map.update([(c, c) for c in safe])
109
 
        quoter = safe_map.__getitem__
110
 
        safe = always_safe + safe
111
 
        _safe_quoters[cachekey] = (quoter, safe)
112
 
    if not s.rstrip(safe):
113
 
        return s
114
 
    return ''.join(map(quoter, s))
115
 
 
116
 
 
117
 
_hexdig = '0123456789ABCDEFabcdef'
118
 
_hextochr = dict((a + b, chr(int(a + b, 16)))
119
 
                 for a in _hexdig for b in _hexdig)
120
 
 
121
 
def unquote(s):
122
 
    """unquote('abc%20def') -> 'abc def'."""
123
 
    res = s.split('%')
124
 
    # fastpath
125
 
    if len(res) == 1:
126
 
        return s
127
 
    s = res[0]
128
 
    for item in res[1:]:
129
 
        try:
130
 
            s += _hextochr[item[:2]] + item[2:]
131
 
        except KeyError:
132
 
            s += '%' + item
133
 
        except UnicodeDecodeError:
134
 
            s += unichr(int(item[:2], 16)) + item[2:]
135
 
    return s
136
 
 
137
 
 
138
63
def escape(relpath):
139
64
    """Escape relpath to be a valid url."""
140
65
    if isinstance(relpath, unicode):
141
66
        relpath = relpath.encode('utf-8')
142
67
    # After quoting and encoding, the path should be perfectly
143
68
    # safe as a plain ASCII string, str() just enforces this
144
 
    return str(quote(relpath, safe='/~'))
 
69
    return str(urllib.quote(relpath, safe='/~'))
145
70
 
146
71
 
147
72
def file_relpath(base, path):
153
78
        raise ValueError('Length of base (%r) must equal or'
154
79
            ' exceed the platform minimum url length (which is %d)' %
155
80
            (base, MIN_ABS_FILEURL_LENGTH))
156
 
    base = osutils.normpath(local_path_from_url(base))
157
 
    path = osutils.normpath(local_path_from_url(path))
 
81
    base = local_path_from_url(base)
 
82
    path = local_path_from_url(path)
158
83
    return escape(osutils.relpath(base, path))
159
84
 
160
85
 
256
181
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
257
182
def _posix_local_path_from_url(url):
258
183
    """Convert a url like file:///path/to/foo into /path/to/foo"""
259
 
    url = split_segment_parameters_raw(url)[0]
260
184
    file_localhost_prefix = 'file://localhost/'
261
185
    if url.startswith(file_localhost_prefix):
262
186
        path = url[len(file_localhost_prefix) - 1:]
276
200
    """
277
201
    # importing directly from posixpath allows us to test this
278
202
    # on non-posix platforms
279
 
    return 'file://' + escape(osutils._posix_abspath(path))
 
203
    return 'file://' + escape(_posix_normpath(
 
204
        osutils._posix_abspath(path)))
280
205
 
281
206
 
282
207
def _win32_local_path_from_url(url):
284
209
    if not url.startswith('file://'):
285
210
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
286
211
                                     'UNC path urls must start with file://')
287
 
    url = split_segment_parameters_raw(url)[0]
288
212
    # We strip off all 3 slashes
289
213
    win32_url = url[len('file:'):]
290
214
    # check for UNC path: //HOST/path
300
224
        return '/'
301
225
 
302
226
    # usual local path with drive letter
303
 
    if (len(win32_url) < 6
304
 
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
305
 
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
227
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
228
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
306
229
        or win32_url[4] not in  '|:'
307
230
        or win32_url[5] != '/'):
308
231
        raise errors.InvalidURL(url, 'Win32 file urls start with'
462
385
    """On win32 the drive letter needs to be added to the url base."""
463
386
    # Strip off the drive letter
464
387
    # path is currently /C:/foo
465
 
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
 
388
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
466
389
        raise errors.InvalidURL(url_base + path,
467
390
            'win32 file:/// paths need a drive letter')
468
391
    url_base += path[0:3] # file:// + /C:
516
439
    :param url: A relative or absolute URL
517
440
    :return: (url, subsegments)
518
441
    """
519
 
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
520
 
    #                operates on urls not url+segments, and Transport classes
521
 
    #                should not be blindly adding slashes in the first place. 
522
 
    lurl = strip_trailing_slash(url)
523
 
    # Segments begin at first comma after last forward slash, if one exists
524
 
    segment_start = lurl.find(",", lurl.rfind("/")+1)
525
 
    if segment_start == -1:
 
442
    (parent_url, child_dir) = split(url)
 
443
    subsegments = child_dir.split(",")
 
444
    if len(subsegments) == 1:
526
445
        return (url, [])
527
 
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
446
    return (join(parent_url, subsegments[0]), subsegments[1:])
528
447
 
529
448
 
530
449
def split_segment_parameters(url):
641
560
    This returns a Unicode path from a URL
642
561
    """
643
562
    # jam 20060427 URLs are supposed to be ASCII only strings
644
 
    #       If they are passed in as unicode, unquote
 
563
    #       If they are passed in as unicode, urllib.unquote
645
564
    #       will return a UNICODE string, which actually contains
646
565
    #       utf-8 bytes. So we have to ensure that they are
647
566
    #       plain ASCII strings, or the final .decode will
652
571
    except UnicodeError, e:
653
572
        raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
654
573
 
655
 
    unquoted = unquote(url)
 
574
    unquoted = urllib.unquote(url)
656
575
    try:
657
576
        unicode_path = unquoted.decode('utf-8')
658
577
    except UnicodeError, e:
810
729
    return osutils.pathjoin(*segments)
811
730
 
812
731
 
813
 
class URL(object):
814
 
    """Parsed URL."""
815
 
 
816
 
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
817
 
            port, quoted_path):
818
 
        self.scheme = scheme
819
 
        self.quoted_host = quoted_host
820
 
        self.host = unquote(self.quoted_host)
821
 
        self.quoted_user = quoted_user
822
 
        if self.quoted_user is not None:
823
 
            self.user = unquote(self.quoted_user)
824
 
        else:
825
 
            self.user = None
826
 
        self.quoted_password = quoted_password
827
 
        if self.quoted_password is not None:
828
 
            self.password = unquote(self.quoted_password)
829
 
        else:
830
 
            self.password = None
831
 
        self.port = port
832
 
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
833
 
        self.path = unquote(self.quoted_path)
834
 
 
835
 
    def __eq__(self, other):
836
 
        return (isinstance(other, self.__class__) and
837
 
                self.scheme == other.scheme and
838
 
                self.host == other.host and
839
 
                self.user == other.user and
840
 
                self.password == other.password and
841
 
                self.path == other.path)
842
 
 
843
 
    def __repr__(self):
844
 
        return "<%s(%r, %r, %r, %r, %r, %r)>" % (
845
 
            self.__class__.__name__,
846
 
            self.scheme, self.quoted_user, self.quoted_password,
847
 
            self.quoted_host, self.port, self.quoted_path)
848
 
 
849
 
    @classmethod
850
 
    def from_string(cls, url):
851
 
        """Create a URL object from a string.
852
 
 
853
 
        :param url: URL as bytestring
854
 
        """
855
 
        if isinstance(url, unicode):
856
 
            raise errors.InvalidURL('should be ascii:\n%r' % url)
857
 
        url = url.encode('utf-8')
858
 
        (scheme, netloc, path, params,
859
 
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
860
 
        user = password = host = port = None
861
 
        if '@' in netloc:
862
 
            user, host = netloc.rsplit('@', 1)
863
 
            if ':' in user:
864
 
                user, password = user.split(':', 1)
865
 
        else:
866
 
            host = netloc
867
 
 
868
 
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
869
 
            # there *is* port
870
 
            host, port = host.rsplit(':',1)
871
 
            try:
872
 
                port = int(port)
873
 
            except ValueError:
874
 
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
875
 
                                        (port, url))
876
 
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
877
 
            host = host[1:-1]
878
 
 
879
 
        return cls(scheme, user, password, host, port, path)
880
 
 
881
 
    def __str__(self):
882
 
        netloc = self.quoted_host
883
 
        if ":" in netloc:
884
 
            netloc = "[%s]" % netloc
885
 
        if self.quoted_user is not None:
886
 
            # Note that we don't put the password back even if we
887
 
            # have one so that it doesn't get accidentally
888
 
            # exposed.
889
 
            netloc = '%s@%s' % (self.quoted_user, netloc)
890
 
        if self.port is not None:
891
 
            netloc = '%s:%d' % (netloc, self.port)
892
 
        return urlparse.urlunparse(
893
 
            (self.scheme, netloc, self.quoted_path, None, None, None))
894
 
 
895
 
    @staticmethod
896
 
    def _combine_paths(base_path, relpath):
897
 
        """Transform a Transport-relative path to a remote absolute path.
898
 
 
899
 
        This does not handle substitution of ~ but does handle '..' and '.'
900
 
        components.
901
 
 
902
 
        Examples::
903
 
 
904
 
            t._combine_paths('/home/sarah', 'project/foo')
905
 
                => '/home/sarah/project/foo'
906
 
            t._combine_paths('/home/sarah', '../../etc')
907
 
                => '/etc'
908
 
            t._combine_paths('/home/sarah', '/etc')
909
 
                => '/etc'
910
 
 
911
 
        :param base_path: base path
912
 
        :param relpath: relative url string for relative part of remote path.
913
 
        :return: urlencoded string for final path.
914
 
        """
915
 
        if not isinstance(relpath, str):
916
 
            raise errors.InvalidURL(relpath)
917
 
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
918
 
        if relpath.startswith('/'):
919
 
            base_parts = []
920
 
        else:
921
 
            base_parts = base_path.split('/')
922
 
        if len(base_parts) > 0 and base_parts[-1] == '':
923
 
            base_parts = base_parts[:-1]
924
 
        for p in relpath.split('/'):
925
 
            if p == '..':
926
 
                if len(base_parts) == 0:
927
 
                    # In most filesystems, a request for the parent
928
 
                    # of root, just returns root.
929
 
                    continue
930
 
                base_parts.pop()
931
 
            elif p == '.':
932
 
                continue # No-op
933
 
            elif p != '':
934
 
                base_parts.append(p)
935
 
        path = '/'.join(base_parts)
936
 
        if not path.startswith('/'):
937
 
            path = '/' + path
938
 
        return path
939
 
 
940
 
    def clone(self, offset=None):
941
 
        """Return a new URL for a path relative to this URL.
942
 
 
943
 
        :param offset: A relative path, already urlencoded
944
 
        :return: `URL` instance
945
 
        """
946
 
        if offset is not None:
947
 
            relative = unescape(offset).encode('utf-8')
948
 
            path = self._combine_paths(self.path, relative)
949
 
            path = quote(path, safe="/~")
950
 
        else:
951
 
            path = self.quoted_path
952
 
        return self.__class__(self.scheme, self.quoted_user,
953
 
                self.quoted_password, self.quoted_host, self.port,
954
 
                path)
955
 
 
956
732
 
957
733
def parse_url(url):
958
734
    """Extract the server address, the credentials and the path from the url.
961
737
    chars.
962
738
 
963
739
    :param url: an quoted url
 
740
 
964
741
    :return: (scheme, user, password, host, port, path) tuple, all fields
965
742
        are unquoted.
966
743
    """
967
 
    parsed_url = URL.from_string(url)
968
 
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
969
 
        parsed_url.host, parsed_url.port, parsed_url.path)
 
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)