~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/urlutils.py

Merge bzr.dev, update to use new hooks.

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, normpath as _posix_normpath
 
25
from posixpath import split as _posix_split
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 = local_path_from_url(base)
82
 
    path = local_path_from_url(path)
 
81
    base = osutils.normpath(local_path_from_url(base))
 
82
    path = osutils.normpath(local_path_from_url(path))
83
83
    return escape(osutils.relpath(base, path))
84
84
 
85
85
 
181
181
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
182
182
def _posix_local_path_from_url(url):
183
183
    """Convert a url like file:///path/to/foo into /path/to/foo"""
 
184
    url = split_segment_parameters_raw(url)[0]
184
185
    file_localhost_prefix = 'file://localhost/'
185
186
    if url.startswith(file_localhost_prefix):
186
187
        path = url[len(file_localhost_prefix) - 1:]
200
201
    """
201
202
    # importing directly from posixpath allows us to test this
202
203
    # on non-posix platforms
203
 
    return 'file://' + escape(_posix_normpath(
204
 
        osutils._posix_abspath(path)))
 
204
    return 'file://' + escape(osutils._posix_abspath(path))
205
205
 
206
206
 
207
207
def _win32_local_path_from_url(url):
209
209
    if not url.startswith('file://'):
210
210
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
211
211
                                     'UNC path urls must start with file://')
 
212
    url = split_segment_parameters_raw(url)[0]
212
213
    # We strip off all 3 slashes
213
214
    win32_url = url[len('file:'):]
214
215
    # check for UNC path: //HOST/path
224
225
        return '/'
225
226
 
226
227
    # usual local path with drive letter
227
 
    if (win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
228
 
                             'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
228
    if (len(win32_url) < 6
 
229
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
230
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
229
231
        or win32_url[4] not in  '|:'
230
232
        or win32_url[5] != '/'):
231
233
        raise errors.InvalidURL(url, 'Win32 file urls start with'
385
387
    """On win32 the drive letter needs to be added to the url base."""
386
388
    # Strip off the drive letter
387
389
    # path is currently /C:/foo
388
 
    if len(path) < 3 or path[2] not in ':|' or path[3] != '/':
 
390
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
389
391
        raise errors.InvalidURL(url_base + path,
390
392
            'win32 file:/// paths need a drive letter')
391
393
    url_base += path[0:3] # file:// + /C:
439
441
    :param url: A relative or absolute URL
440
442
    :return: (url, subsegments)
441
443
    """
442
 
    (parent_url, child_dir) = split(url)
443
 
    subsegments = child_dir.split(",")
444
 
    if len(subsegments) == 1:
 
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:
445
451
        return (url, [])
446
 
    return (join(parent_url, subsegments[0]), subsegments[1:])
 
452
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
447
453
 
448
454
 
449
455
def split_segment_parameters(url):
729
735
    return osutils.pathjoin(*segments)
730
736
 
731
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
 
732
881
 
733
882
def parse_url(url):
734
883
    """Extract the server address, the credentials and the path from the url.
737
886
    chars.
738
887
 
739
888
    :param url: an quoted url
740
 
 
741
889
    :return: (scheme, user, password, host, port, path) tuple, all fields
742
890
        are unquoted.
743
891
    """
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)
 
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)