/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 breezy/urlutils.py

  • Committer: Martin
  • Date: 2017-06-10 01:57:00 UTC
  • mto: This revision was merged to the branch mainline in revision 6679.
  • Revision ID: gzlist@googlemail.com-20170610015700-o3xeuyaqry2obiay
Go back to native str for urls and many other py3 changes

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2010 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""A collection of function for handling URL operations."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import os
 
22
import re
 
23
import sys
 
24
 
 
25
try:
 
26
    import urlparse
 
27
except ImportError:
 
28
    from urllib import parse as urlparse
 
29
 
 
30
from .lazy_import import lazy_import
 
31
lazy_import(globals(), """
 
32
from posixpath import split as _posix_split
 
33
 
 
34
from breezy import (
 
35
    errors,
 
36
    osutils,
 
37
    )
 
38
""")
 
39
 
 
40
from .sixish import (
 
41
    PY3,
 
42
    text_type,
 
43
    )
 
44
 
 
45
 
 
46
def basename(url, exclude_trailing_slash=True):
 
47
    """Return the last component of a URL.
 
48
 
 
49
    :param url: The URL in question
 
50
    :param exclude_trailing_slash: If the url looks like "path/to/foo/"
 
51
        ignore the final slash and return 'foo' rather than ''
 
52
    :return: Just the final component of the URL. This can return ''
 
53
        if you don't exclude_trailing_slash, or if you are at the
 
54
        root of the URL.
 
55
    """
 
56
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[1]
 
57
 
 
58
 
 
59
def dirname(url, exclude_trailing_slash=True):
 
60
    """Return the parent directory of the given path.
 
61
 
 
62
    :param url: Relative or absolute URL
 
63
    :param exclude_trailing_slash: Remove a final slash
 
64
        (treat http://host/foo/ as http://host/foo, but
 
65
        http://host/ stays http://host/)
 
66
    :return: Everything in the URL except the last path chunk
 
67
    """
 
68
    # TODO: jam 20060502 This was named dirname to be consistent
 
69
    #       with the os functions, but maybe "parent" would be better
 
70
    return split(url, exclude_trailing_slash=exclude_trailing_slash)[0]
 
71
 
 
72
 
 
73
# Private copies of quote and unquote, copied from Python's
 
74
# urllib module because urllib unconditionally imports socket, which imports
 
75
# ssl.
 
76
 
 
77
always_safe = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
78
               'abcdefghijklmnopqrstuvwxyz'
 
79
               '0123456789' '_.-')
 
80
_safe_map = {}
 
81
for i, c in zip(range(256), ''.join(map(chr, range(256)))):
 
82
    _safe_map[c] = c if (i < 128 and c in always_safe) else '%{0:02X}'.format(i)
 
83
_safe_quoters = {}
 
84
 
 
85
 
 
86
def quote(s, safe='/'):
 
87
    """quote('abc def') -> 'abc%20def'
 
88
 
 
89
    Each part of a URL, e.g. the path info, the query, etc., has a
 
90
    different set of reserved characters that must be quoted.
 
91
 
 
92
    RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists
 
93
    the following reserved characters.
 
94
 
 
95
    reserved    = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
 
96
                  "$" | ","
 
97
 
 
98
    Each of these characters is reserved in some component of a URL,
 
99
    but not necessarily in all of them.
 
100
 
 
101
    By default, the quote function is intended for quoting the path
 
102
    section of a URL.  Thus, it will not encode '/'.  This character
 
103
    is reserved, but in typical usage the quote function is being
 
104
    called on a path where the existing slash characters are used as
 
105
    reserved characters.
 
106
    """
 
107
    # fastpath
 
108
    if not s:
 
109
        if s is None:
 
110
            raise TypeError('None object cannot be quoted')
 
111
        return s
 
112
    cachekey = (safe, always_safe)
 
113
    try:
 
114
        (quoter, safe) = _safe_quoters[cachekey]
 
115
    except KeyError:
 
116
        safe_map = _safe_map.copy()
 
117
        safe_map.update([(c, c) for c in safe])
 
118
        quoter = safe_map.__getitem__
 
119
        safe = always_safe + safe
 
120
        _safe_quoters[cachekey] = (quoter, safe)
 
121
    if not s.rstrip(safe):
 
122
        return s
 
123
    return ''.join(map(quoter, s))
 
124
 
 
125
 
 
126
unquote = urlparse.unquote
 
127
 
 
128
 
 
129
def escape(relpath):
 
130
    """Escape relpath to be a valid url."""
 
131
    if not isinstance(relpath, str):
 
132
        relpath = relpath.encode('utf-8')
 
133
    return quote(relpath, safe='/~')
 
134
 
 
135
 
 
136
def file_relpath(base, path):
 
137
    """Compute just the relative sub-portion of a url
 
138
 
 
139
    This assumes that both paths are already fully specified file:// URLs.
 
140
    """
 
141
    if len(base) < MIN_ABS_FILEURL_LENGTH:
 
142
        raise ValueError('Length of base (%r) must equal or'
 
143
            ' exceed the platform minimum url length (which is %d)' %
 
144
            (base, MIN_ABS_FILEURL_LENGTH))
 
145
    base = osutils.normpath(local_path_from_url(base))
 
146
    path = osutils.normpath(local_path_from_url(path))
 
147
    return escape(osutils.relpath(base, path))
 
148
 
 
149
 
 
150
def _find_scheme_and_separator(url):
 
151
    """Find the scheme separator (://) and the first path separator
 
152
 
 
153
    This is just a helper functions for other path utilities.
 
154
    It could probably be replaced by urlparse
 
155
    """
 
156
    m = _url_scheme_re.match(url)
 
157
    if not m:
 
158
        return None, None
 
159
 
 
160
    scheme = m.group('scheme')
 
161
    path = m.group('path')
 
162
 
 
163
    # Find the path separating slash
 
164
    # (first slash after the ://)
 
165
    first_path_slash = path.find('/')
 
166
    if first_path_slash == -1:
 
167
        return len(scheme), None
 
168
    return len(scheme), first_path_slash+m.start('path')
 
169
 
 
170
 
 
171
def is_url(url):
 
172
    """Tests whether a URL is in actual fact a URL."""
 
173
    return _url_scheme_re.match(url) is not None
 
174
 
 
175
 
 
176
def join(base, *args):
 
177
    """Create a URL by joining sections.
 
178
 
 
179
    This will normalize '..', assuming that paths are absolute
 
180
    (it assumes no symlinks in either path)
 
181
 
 
182
    If any of *args is an absolute URL, it will be treated correctly.
 
183
    Example:
 
184
        join('http://foo', 'http://bar') => 'http://bar'
 
185
        join('http://foo', 'bar') => 'http://foo/bar'
 
186
        join('http://foo', 'bar', '../baz') => 'http://foo/baz'
 
187
    """
 
188
    if not args:
 
189
        return base
 
190
    scheme_end, path_start = _find_scheme_and_separator(base)
 
191
    if scheme_end is None and path_start is None:
 
192
        path_start = 0
 
193
    elif path_start is None:
 
194
        path_start = len(base)
 
195
    path = base[path_start:]
 
196
    for arg in args:
 
197
        arg_scheme_end, arg_path_start = _find_scheme_and_separator(arg)
 
198
        if arg_scheme_end is None and arg_path_start is None:
 
199
            arg_path_start = 0
 
200
        elif arg_path_start is None:
 
201
            arg_path_start = len(arg)
 
202
        if arg_scheme_end is not None:
 
203
            base = arg
 
204
            path = arg[arg_path_start:]
 
205
            scheme_end = arg_scheme_end
 
206
            path_start = arg_path_start
 
207
        else:
 
208
            path = joinpath(path, arg)
 
209
    return base[:path_start] + path
 
210
 
 
211
 
 
212
def joinpath(base, *args):
 
213
    """Join URL path segments to a URL path segment.
 
214
 
 
215
    This is somewhat like osutils.joinpath, but intended for URLs.
 
216
 
 
217
    XXX: this duplicates some normalisation logic, and also duplicates a lot of
 
218
    path handling logic that already exists in some Transport implementations.
 
219
    We really should try to have exactly one place in the code base responsible
 
220
    for combining paths of URLs.
 
221
    """
 
222
    path = base.split('/')
 
223
    if len(path) > 1 and path[-1] == '':
 
224
        #If the path ends in a trailing /, remove it.
 
225
        path.pop()
 
226
    for arg in args:
 
227
        if arg.startswith('/'):
 
228
            path = []
 
229
        for chunk in arg.split('/'):
 
230
            if chunk == '.':
 
231
                continue
 
232
            elif chunk == '..':
 
233
                if path == ['']:
 
234
                    raise errors.InvalidURLJoin('Cannot go above root',
 
235
                            base, args)
 
236
                path.pop()
 
237
            else:
 
238
                path.append(chunk)
 
239
    if path == ['']:
 
240
        return '/'
 
241
    else:
 
242
        return '/'.join(path)
 
243
 
 
244
 
 
245
# jam 20060502 Sorted to 'l' because the final target is 'local_path_from_url'
 
246
def _posix_local_path_from_url(url):
 
247
    """Convert a url like file:///path/to/foo into /path/to/foo"""
 
248
    url = split_segment_parameters_raw(url)[0]
 
249
    file_localhost_prefix = 'file://localhost/'
 
250
    if url.startswith(file_localhost_prefix):
 
251
        path = url[len(file_localhost_prefix) - 1:]
 
252
    elif not url.startswith('file:///'):
 
253
        raise errors.InvalidURL(
 
254
            url, 'local urls must start with file:/// or file://localhost/')
 
255
    else:
 
256
        path = url[len('file://'):]
 
257
    # We only strip off 2 slashes
 
258
    return unescape(path)
 
259
 
 
260
 
 
261
def _posix_local_path_to_url(path):
 
262
    """Convert a local path like ./foo into a URL like file:///path/to/foo
 
263
 
 
264
    This also handles transforming escaping unicode characters, etc.
 
265
    """
 
266
    # importing directly from posixpath allows us to test this
 
267
    # on non-posix platforms
 
268
    return 'file://' + escape(osutils._posix_abspath(path))
 
269
 
 
270
 
 
271
def _win32_local_path_from_url(url):
 
272
    """Convert a url like file:///C:/path/to/foo into C:/path/to/foo"""
 
273
    if not url.startswith('file://'):
 
274
        raise errors.InvalidURL(url, 'local urls must start with file:///, '
 
275
                                     'UNC path urls must start with file://')
 
276
    url = split_segment_parameters_raw(url)[0]
 
277
    # We strip off all 3 slashes
 
278
    win32_url = url[len('file:'):]
 
279
    # check for UNC path: //HOST/path
 
280
    if not win32_url.startswith('///'):
 
281
        if (win32_url[2] == '/'
 
282
            or win32_url[3] in '|:'):
 
283
            raise errors.InvalidURL(url, 'Win32 UNC path urls'
 
284
                ' have form file://HOST/path')
 
285
        return unescape(win32_url)
 
286
 
 
287
    # allow empty paths so we can serve all roots
 
288
    if win32_url == '///':
 
289
        return '/'
 
290
 
 
291
    # usual local path with drive letter
 
292
    if (len(win32_url) < 6
 
293
        or win32_url[3] not in ('abcdefghijklmnopqrstuvwxyz'
 
294
                                'ABCDEFGHIJKLMNOPQRSTUVWXYZ')
 
295
        or win32_url[4] not in  '|:'
 
296
        or win32_url[5] != '/'):
 
297
        raise errors.InvalidURL(url, 'Win32 file urls start with'
 
298
                ' file:///x:/, where x is a valid drive letter')
 
299
    return win32_url[3].upper() + u':' + unescape(win32_url[5:])
 
300
 
 
301
 
 
302
def _win32_local_path_to_url(path):
 
303
    """Convert a local path like ./foo into a URL like file:///C:/path/to/foo
 
304
 
 
305
    This also handles transforming escaping unicode characters, etc.
 
306
    """
 
307
    # importing directly from ntpath allows us to test this
 
308
    # on non-win32 platform
 
309
    # FIXME: It turns out that on nt, ntpath.abspath uses nt._getfullpathname
 
310
    #       which actually strips trailing space characters.
 
311
    #       The worst part is that on linux ntpath.abspath has different
 
312
    #       semantics, since 'nt' is not an available module.
 
313
    if path == '/':
 
314
        return 'file:///'
 
315
 
 
316
    win32_path = osutils._win32_abspath(path)
 
317
    # check for UNC path \\HOST\path
 
318
    if win32_path.startswith('//'):
 
319
        return 'file:' + escape(win32_path)
 
320
    return ('file:///' + str(win32_path[0].upper()) + ':' +
 
321
        escape(win32_path[2:]))
 
322
 
 
323
 
 
324
local_path_to_url = _posix_local_path_to_url
 
325
local_path_from_url = _posix_local_path_from_url
 
326
MIN_ABS_FILEURL_LENGTH = len('file:///')
 
327
WIN32_MIN_ABS_FILEURL_LENGTH = len('file:///C:/')
 
328
 
 
329
if sys.platform == 'win32':
 
330
    local_path_to_url = _win32_local_path_to_url
 
331
    local_path_from_url = _win32_local_path_from_url
 
332
 
 
333
    MIN_ABS_FILEURL_LENGTH = WIN32_MIN_ABS_FILEURL_LENGTH
 
334
 
 
335
 
 
336
_url_scheme_re = re.compile('^(?P<scheme>[^:/]{2,}):(//)?(?P<path>.*)$')
 
337
_url_hex_escapes_re = re.compile('(%[0-9a-fA-F]{2})')
 
338
 
 
339
 
 
340
def _unescape_safe_chars(matchobj):
 
341
    """re.sub callback to convert hex-escapes to plain characters (if safe).
 
342
 
 
343
    e.g. '%7E' will be converted to '~'.
 
344
    """
 
345
    hex_digits = matchobj.group(0)[1:]
 
346
    char = chr(int(hex_digits, 16))
 
347
    if char in _url_dont_escape_characters:
 
348
        return char
 
349
    else:
 
350
        return matchobj.group(0).upper()
 
351
 
 
352
 
 
353
def normalize_url(url):
 
354
    """Make sure that a path string is in fully normalized URL form.
 
355
 
 
356
    This handles URLs which have unicode characters, spaces,
 
357
    special characters, etc.
 
358
 
 
359
    It has two basic modes of operation, depending on whether the
 
360
    supplied string starts with a url specifier (scheme://) or not.
 
361
    If it does not have a specifier it is considered a local path,
 
362
    and will be converted into a file:/// url. Non-ascii characters
 
363
    will be encoded using utf-8.
 
364
    If it does have a url specifier, it will be treated as a "hybrid"
 
365
    URL. Basically, a URL that should have URL special characters already
 
366
    escaped (like +?&# etc), but may have unicode characters, etc
 
367
    which would not be valid in a real URL.
 
368
 
 
369
    :param url: Either a hybrid URL or a local path
 
370
    :return: A normalized URL which only includes 7-bit ASCII characters.
 
371
    """
 
372
    scheme_end, path_start = _find_scheme_and_separator(url)
 
373
    if scheme_end is None:
 
374
        return local_path_to_url(url)
 
375
    prefix = url[:path_start]
 
376
    path = url[path_start:]
 
377
    if not isinstance(url, text_type):
 
378
        for c in url:
 
379
            if c not in _url_safe_characters:
 
380
                raise errors.InvalidURL(url, 'URLs can only contain specific'
 
381
                                            ' safe characters (not %r)' % c)
 
382
        path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
 
383
        return str(prefix + ''.join(path))
 
384
 
 
385
    # We have a unicode (hybrid) url
 
386
    path_chars = list(path)
 
387
 
 
388
    for i in range(len(path_chars)):
 
389
        if path_chars[i] not in _url_safe_characters:
 
390
            chars = path_chars[i].encode('utf-8')
 
391
            path_chars[i] = ''.join(
 
392
                ['%%%02X' % ord(c) for c in path_chars[i].encode('utf-8')])
 
393
    path = ''.join(path_chars)
 
394
    path = _url_hex_escapes_re.sub(_unescape_safe_chars, path)
 
395
    return str(prefix + path)
 
396
 
 
397
 
 
398
def relative_url(base, other):
 
399
    """Return a path to other from base.
 
400
 
 
401
    If other is unrelated to base, return other. Else return a relative path.
 
402
    This assumes no symlinks as part of the url.
 
403
    """
 
404
    dummy, base_first_slash = _find_scheme_and_separator(base)
 
405
    if base_first_slash is None:
 
406
        return other
 
407
 
 
408
    dummy, other_first_slash = _find_scheme_and_separator(other)
 
409
    if other_first_slash is None:
 
410
        return other
 
411
 
 
412
    # this takes care of differing schemes or hosts
 
413
    base_scheme = base[:base_first_slash]
 
414
    other_scheme = other[:other_first_slash]
 
415
    if base_scheme != other_scheme:
 
416
        return other
 
417
    elif sys.platform == 'win32' and base_scheme == 'file://':
 
418
        base_drive = base[base_first_slash+1:base_first_slash+3]
 
419
        other_drive = other[other_first_slash+1:other_first_slash+3]
 
420
        if base_drive != other_drive:
 
421
            return other
 
422
 
 
423
    base_path = base[base_first_slash+1:]
 
424
    other_path = other[other_first_slash+1:]
 
425
 
 
426
    if base_path.endswith('/'):
 
427
        base_path = base_path[:-1]
 
428
 
 
429
    base_sections = base_path.split('/')
 
430
    other_sections = other_path.split('/')
 
431
 
 
432
    if base_sections == ['']:
 
433
        base_sections = []
 
434
    if other_sections == ['']:
 
435
        other_sections = []
 
436
 
 
437
    output_sections = []
 
438
    for b, o in zip(base_sections, other_sections):
 
439
        if b != o:
 
440
            break
 
441
        output_sections.append(b)
 
442
 
 
443
    match_len = len(output_sections)
 
444
    output_sections = ['..' for x in base_sections[match_len:]]
 
445
    output_sections.extend(other_sections[match_len:])
 
446
 
 
447
    return "/".join(output_sections) or "."
 
448
 
 
449
 
 
450
def _win32_extract_drive_letter(url_base, path):
 
451
    """On win32 the drive letter needs to be added to the url base."""
 
452
    # Strip off the drive letter
 
453
    # path is currently /C:/foo
 
454
    if len(path) < 4 or path[2] not in ':|' or path[3] != '/':
 
455
        raise errors.InvalidURL(url_base + path,
 
456
            'win32 file:/// paths need a drive letter')
 
457
    url_base += path[0:3] # file:// + /C:
 
458
    path = path[3:] # /foo
 
459
    return url_base, path
 
460
 
 
461
 
 
462
def split(url, exclude_trailing_slash=True):
 
463
    """Split a URL into its parent directory and a child directory.
 
464
 
 
465
    :param url: A relative or absolute URL
 
466
    :param exclude_trailing_slash: Strip off a final '/' if it is part
 
467
        of the path (but not if it is part of the protocol specification)
 
468
 
 
469
    :return: (parent_url, child_dir).  child_dir may be the empty string if we're at
 
470
        the root.
 
471
    """
 
472
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
 
473
 
 
474
    if first_path_slash is None:
 
475
        # We have either a relative path, or no separating slash
 
476
        if scheme_loc is None:
 
477
            # Relative path
 
478
            if exclude_trailing_slash and url.endswith('/'):
 
479
                url = url[:-1]
 
480
            return _posix_split(url)
 
481
        else:
 
482
            # Scheme with no path
 
483
            return url, ''
 
484
 
 
485
    # We have a fully defined path
 
486
    url_base = url[:first_path_slash] # http://host, file://
 
487
    path = url[first_path_slash:] # /file/foo
 
488
 
 
489
    if sys.platform == 'win32' and url.startswith('file:///'):
 
490
        # Strip off the drive letter
 
491
        # url_base is currently file://
 
492
        # path is currently /C:/foo
 
493
        url_base, path = _win32_extract_drive_letter(url_base, path)
 
494
        # now it should be file:///C: and /foo
 
495
 
 
496
    if exclude_trailing_slash and len(path) > 1 and path.endswith('/'):
 
497
        path = path[:-1]
 
498
    head, tail = _posix_split(path)
 
499
    return url_base + head, tail
 
500
 
 
501
 
 
502
def split_segment_parameters_raw(url):
 
503
    """Split the subsegment of the last segment of a URL.
 
504
 
 
505
    :param url: A relative or absolute URL
 
506
    :return: (url, subsegments)
 
507
    """
 
508
    # GZ 2011-11-18: Dodgy removing the terminal slash like this, function
 
509
    #                operates on urls not url+segments, and Transport classes
 
510
    #                should not be blindly adding slashes in the first place. 
 
511
    lurl = strip_trailing_slash(url)
 
512
    # Segments begin at first comma after last forward slash, if one exists
 
513
    segment_start = lurl.find(",", lurl.rfind("/")+1)
 
514
    if segment_start == -1:
 
515
        return (url, [])
 
516
    return (lurl[:segment_start], lurl[segment_start+1:].split(","))
 
517
 
 
518
 
 
519
def split_segment_parameters(url):
 
520
    """Split the segment parameters of the last segment of a URL.
 
521
 
 
522
    :param url: A relative or absolute URL
 
523
    :return: (url, segment_parameters)
 
524
    """
 
525
    (base_url, subsegments) = split_segment_parameters_raw(url)
 
526
    parameters = {}
 
527
    for subsegment in subsegments:
 
528
        (key, value) = subsegment.split("=", 1)
 
529
        parameters[key] = value
 
530
    return (base_url, parameters)
 
531
 
 
532
 
 
533
def join_segment_parameters_raw(base, *subsegments):
 
534
    """Create a new URL by adding subsegments to an existing one. 
 
535
 
 
536
    This adds the specified subsegments to the last path in the specified
 
537
    base URL. The subsegments should be bytestrings.
 
538
 
 
539
    :note: You probably want to use join_segment_parameters instead.
 
540
    """
 
541
    if not subsegments:
 
542
        return base
 
543
    for subsegment in subsegments:
 
544
        if not isinstance(subsegment, str):
 
545
            raise TypeError("Subsegment %r is not a bytestring" % subsegment)
 
546
        if "," in subsegment:
 
547
            raise errors.InvalidURLJoin(", exists in subsegments",
 
548
                                        base, subsegments)
 
549
    return ",".join((base,) + subsegments)
 
550
 
 
551
 
 
552
def join_segment_parameters(url, parameters):
 
553
    """Create a new URL by adding segment parameters to an existing one.
 
554
 
 
555
    The parameters of the last segment in the URL will be updated; if a
 
556
    parameter with the same key already exists it will be overwritten.
 
557
 
 
558
    :param url: A URL, as string
 
559
    :param parameters: Dictionary of parameters, keys and values as bytestrings
 
560
    """
 
561
    (base, existing_parameters) = split_segment_parameters(url)
 
562
    new_parameters = {}
 
563
    new_parameters.update(existing_parameters)
 
564
    for key, value in parameters.items():
 
565
        if not isinstance(key, str):
 
566
            raise TypeError("parameter key %r is not a bytestring" % key)
 
567
        if not isinstance(value, str):
 
568
            raise TypeError("parameter value %r for %s is not a bytestring" %
 
569
                (key, value))
 
570
        if "=" in key:
 
571
            raise errors.InvalidURLJoin("= exists in parameter key", url,
 
572
                parameters)
 
573
        new_parameters[key] = value
 
574
    return join_segment_parameters_raw(base, 
 
575
        *["%s=%s" % item for item in sorted(new_parameters.items())])
 
576
 
 
577
 
 
578
def _win32_strip_local_trailing_slash(url):
 
579
    """Strip slashes after the drive letter"""
 
580
    if len(url) > WIN32_MIN_ABS_FILEURL_LENGTH:
 
581
        return url[:-1]
 
582
    else:
 
583
        return url
 
584
 
 
585
 
 
586
def strip_trailing_slash(url):
 
587
    """Strip trailing slash, except for root paths.
 
588
 
 
589
    The definition of 'root path' is platform-dependent.
 
590
    This assumes that all URLs are valid netloc urls, such that they
 
591
    form:
 
592
    scheme://host/path
 
593
    It searches for ://, and then refuses to remove the next '/'.
 
594
    It can also handle relative paths
 
595
    Examples:
 
596
        path/to/foo       => path/to/foo
 
597
        path/to/foo/      => path/to/foo
 
598
        http://host/path/ => http://host/path
 
599
        http://host/path  => http://host/path
 
600
        http://host/      => http://host/
 
601
        file:///          => file:///
 
602
        file:///foo/      => file:///foo
 
603
        # This is unique on win32 platforms, and is the only URL
 
604
        # format which does it differently.
 
605
        file:///c|/       => file:///c:/
 
606
    """
 
607
    if not url.endswith('/'):
 
608
        # Nothing to do
 
609
        return url
 
610
    if sys.platform == 'win32' and url.startswith('file://'):
 
611
        return _win32_strip_local_trailing_slash(url)
 
612
 
 
613
    scheme_loc, first_path_slash = _find_scheme_and_separator(url)
 
614
    if scheme_loc is None:
 
615
        # This is a relative path, as it has no scheme
 
616
        # so just chop off the last character
 
617
        return url[:-1]
 
618
 
 
619
    if first_path_slash is None or first_path_slash == len(url)-1:
 
620
        # Don't chop off anything if the only slash is the path
 
621
        # separating slash
 
622
        return url
 
623
 
 
624
    return url[:-1]
 
625
 
 
626
 
 
627
def unescape(url):
 
628
    """Unescape relpath from url format.
 
629
 
 
630
    This returns a Unicode path from a URL
 
631
    """
 
632
    # jam 20060427 URLs are supposed to be ASCII only strings
 
633
    #       If they are passed in as unicode, unquote
 
634
    #       will return a UNICODE string, which actually contains
 
635
    #       utf-8 bytes. So we have to ensure that they are
 
636
    #       plain ASCII strings, or the final .decode will
 
637
    #       try to encode the UNICODE => ASCII, and then decode
 
638
    #       it into utf-8.
 
639
    if isinstance(url, text_type):
 
640
        try:
 
641
            url = url.encode("ascii")
 
642
        except UnicodeError as e:
 
643
            raise errors.InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
 
644
    if PY3:
 
645
        unquoted = urlparse.unquote_to_bytes(url)
 
646
    else:
 
647
        unquoted = unquote(url)
 
648
    try:
 
649
        unicode_path = unquoted.decode('utf-8')
 
650
    except UnicodeError as e:
 
651
        raise errors.InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
 
652
    return unicode_path
 
653
 
 
654
 
 
655
# These are characters that if escaped, should stay that way
 
656
_no_decode_chars = ';/?:@&=+$,#'
 
657
_no_decode_ords = [ord(c) for c in _no_decode_chars]
 
658
_no_decode_hex = (['%02x' % o for o in _no_decode_ords]
 
659
                + ['%02X' % o for o in _no_decode_ords])
 
660
_hex_display_map = dict(([('%02x' % o, chr(o)) for o in range(256)]
 
661
                    + [('%02X' % o, chr(o)) for o in range(256)]))
 
662
#These entries get mapped to themselves
 
663
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
 
664
 
 
665
# These characters shouldn't be percent-encoded, and it's always safe to
 
666
# unencode them if they are.
 
667
_url_dont_escape_characters = set(
 
668
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
669
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
670
   "0123456789" # Numbers
 
671
   "-._~"  # Unreserved characters
 
672
)
 
673
 
 
674
# These characters should not be escaped
 
675
_url_safe_characters = set(
 
676
   "abcdefghijklmnopqrstuvwxyz" # Lowercase alpha
 
677
   "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # Uppercase alpha
 
678
   "0123456789" # Numbers
 
679
   "_.-!~*'()"  # Unreserved characters
 
680
   "/;?:@&=+$," # Reserved characters
 
681
   "%#"         # Extra reserved characters
 
682
)
 
683
 
 
684
def unescape_for_display(url, encoding):
 
685
    """Decode what you can for a URL, so that we get a nice looking path.
 
686
 
 
687
    This will turn file:// urls into local paths, and try to decode
 
688
    any portions of a http:// style url that it can.
 
689
 
 
690
    Any sections of the URL which can't be represented in the encoding or
 
691
    need to stay as escapes are left alone.
 
692
 
 
693
    :param url: A 7-bit ASCII URL
 
694
    :param encoding: The final output encoding
 
695
 
 
696
    :return: A unicode string which can be safely encoded into the
 
697
         specified encoding.
 
698
    """
 
699
    if encoding is None:
 
700
        raise ValueError('you cannot specify None for the display encoding')
 
701
    if url.startswith('file://'):
 
702
        try:
 
703
            path = local_path_from_url(url)
 
704
            path.encode(encoding)
 
705
            return path
 
706
        except UnicodeError:
 
707
            return url
 
708
 
 
709
    # Split into sections to try to decode utf-8
 
710
    res = url.split('/')
 
711
    for i in range(1, len(res)):
 
712
        escaped_chunks = res[i].split('%')
 
713
        for j in range(1, len(escaped_chunks)):
 
714
            item = escaped_chunks[j]
 
715
            try:
 
716
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
 
717
            except KeyError:
 
718
                # Put back the percent symbol
 
719
                escaped_chunks[j] = '%' + item
 
720
            except UnicodeDecodeError:
 
721
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
 
722
        unescaped = ''.join(escaped_chunks)
 
723
        try:
 
724
            decoded = unescaped.decode('utf-8')
 
725
        except UnicodeDecodeError:
 
726
            # If this path segment cannot be properly utf-8 decoded
 
727
            # after doing unescaping we will just leave it alone
 
728
            pass
 
729
        else:
 
730
            try:
 
731
                decoded.encode(encoding)
 
732
            except UnicodeEncodeError:
 
733
                # If this chunk cannot be encoded in the local
 
734
                # encoding, then we should leave it alone
 
735
                pass
 
736
            else:
 
737
                # Otherwise take the url decoded one
 
738
                res[i] = decoded
 
739
    return u'/'.join(res)
 
740
 
 
741
 
 
742
def derive_to_location(from_location):
 
743
    """Derive a TO_LOCATION given a FROM_LOCATION.
 
744
 
 
745
    The normal case is a FROM_LOCATION of http://foo/bar => bar.
 
746
    The Right Thing for some logical destinations may differ though
 
747
    because no / may be present at all. In that case, the result is
 
748
    the full name without the scheme indicator, e.g. lp:foo-bar => foo-bar.
 
749
    This latter case also applies when a Windows drive
 
750
    is used without a path, e.g. c:foo-bar => foo-bar.
 
751
    If no /, path separator or : is found, the from_location is returned.
 
752
    """
 
753
    if from_location.find("/") >= 0 or from_location.find(os.sep) >= 0:
 
754
        return os.path.basename(from_location.rstrip("/\\"))
 
755
    else:
 
756
        sep = from_location.find(":")
 
757
        if sep > 0:
 
758
            return from_location[sep+1:]
 
759
        else:
 
760
            return from_location
 
761
 
 
762
 
 
763
def _is_absolute(url):
 
764
    return (osutils.pathjoin('/foo', url) == url)
 
765
 
 
766
 
 
767
def rebase_url(url, old_base, new_base):
 
768
    """Convert a relative path from an old base URL to a new base URL.
 
769
 
 
770
    The result will be a relative path.
 
771
    Absolute paths and full URLs are returned unaltered.
 
772
    """
 
773
    scheme, separator = _find_scheme_and_separator(url)
 
774
    if scheme is not None:
 
775
        return url
 
776
    if _is_absolute(url):
 
777
        return url
 
778
    old_parsed = urlparse.urlparse(old_base)
 
779
    new_parsed = urlparse.urlparse(new_base)
 
780
    if (old_parsed[:2]) != (new_parsed[:2]):
 
781
        raise errors.InvalidRebaseURLs(old_base, new_base)
 
782
    return determine_relative_path(new_parsed[2],
 
783
                                   join(old_parsed[2], url))
 
784
 
 
785
 
 
786
def determine_relative_path(from_path, to_path):
 
787
    """Determine a relative path from from_path to to_path."""
 
788
    from_segments = osutils.splitpath(from_path)
 
789
    to_segments = osutils.splitpath(to_path)
 
790
    count = -1
 
791
    for count, (from_element, to_element) in enumerate(zip(from_segments,
 
792
                                                       to_segments)):
 
793
        if from_element != to_element:
 
794
            break
 
795
    else:
 
796
        count += 1
 
797
    unique_from = from_segments[count:]
 
798
    unique_to = to_segments[count:]
 
799
    segments = (['..'] * len(unique_from) + unique_to)
 
800
    if len(segments) == 0:
 
801
        return '.'
 
802
    return osutils.pathjoin(*segments)
 
803
 
 
804
 
 
805
class URL(object):
 
806
    """Parsed URL."""
 
807
 
 
808
    def __init__(self, scheme, quoted_user, quoted_password, quoted_host,
 
809
            port, quoted_path):
 
810
        self.scheme = scheme
 
811
        self.quoted_host = quoted_host
 
812
        self.host = unquote(self.quoted_host)
 
813
        self.quoted_user = quoted_user
 
814
        if self.quoted_user is not None:
 
815
            self.user = unquote(self.quoted_user)
 
816
        else:
 
817
            self.user = None
 
818
        self.quoted_password = quoted_password
 
819
        if self.quoted_password is not None:
 
820
            self.password = unquote(self.quoted_password)
 
821
        else:
 
822
            self.password = None
 
823
        self.port = port
 
824
        self.quoted_path = _url_hex_escapes_re.sub(_unescape_safe_chars, quoted_path)
 
825
        self.path = unquote(self.quoted_path)
 
826
 
 
827
    def __eq__(self, other):
 
828
        return (isinstance(other, self.__class__) and
 
829
                self.scheme == other.scheme and
 
830
                self.host == other.host and
 
831
                self.user == other.user and
 
832
                self.password == other.password and
 
833
                self.path == other.path)
 
834
 
 
835
    def __repr__(self):
 
836
        return "<%s(%r, %r, %r, %r, %r, %r)>" % (
 
837
            self.__class__.__name__,
 
838
            self.scheme, self.quoted_user, self.quoted_password,
 
839
            self.quoted_host, self.port, self.quoted_path)
 
840
 
 
841
    @classmethod
 
842
    def from_string(cls, url):
 
843
        """Create a URL object from a string.
 
844
 
 
845
        :param url: URL as bytestring
 
846
        """
 
847
        # GZ 2017-06-09: Actually validate ascii-ness
 
848
        if not isinstance(url, str):
 
849
            raise errors.InvalidURL('should be ascii:\n%r' % url)
 
850
        (scheme, netloc, path, params,
 
851
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
852
        user = password = host = port = None
 
853
        if '@' in netloc:
 
854
            user, host = netloc.rsplit('@', 1)
 
855
            if ':' in user:
 
856
                user, password = user.split(':', 1)
 
857
        else:
 
858
            host = netloc
 
859
 
 
860
        if ':' in host and not (host[0] == '[' and host[-1] == ']'):
 
861
            # there *is* port
 
862
            host, port = host.rsplit(':',1)
 
863
            try:
 
864
                port = int(port)
 
865
            except ValueError:
 
866
                raise errors.InvalidURL('invalid port number %s in url:\n%s' %
 
867
                                        (port, url))
 
868
        if host != "" and host[0] == '[' and host[-1] == ']': #IPv6
 
869
            host = host[1:-1]
 
870
 
 
871
        return cls(scheme, user, password, host, port, path)
 
872
 
 
873
    def __str__(self):
 
874
        netloc = self.quoted_host
 
875
        if ":" in netloc:
 
876
            netloc = "[%s]" % netloc
 
877
        if self.quoted_user is not None:
 
878
            # Note that we don't put the password back even if we
 
879
            # have one so that it doesn't get accidentally
 
880
            # exposed.
 
881
            netloc = '%s@%s' % (self.quoted_user, netloc)
 
882
        if self.port is not None:
 
883
            netloc = '%s:%d' % (netloc, self.port)
 
884
        return urlparse.urlunparse(
 
885
            (self.scheme, netloc, self.quoted_path, None, None, None))
 
886
 
 
887
    @staticmethod
 
888
    def _combine_paths(base_path, relpath):
 
889
        """Transform a Transport-relative path to a remote absolute path.
 
890
 
 
891
        This does not handle substitution of ~ but does handle '..' and '.'
 
892
        components.
 
893
 
 
894
        Examples::
 
895
 
 
896
            t._combine_paths('/home/sarah', 'project/foo')
 
897
                => '/home/sarah/project/foo'
 
898
            t._combine_paths('/home/sarah', '../../etc')
 
899
                => '/etc'
 
900
            t._combine_paths('/home/sarah', '/etc')
 
901
                => '/etc'
 
902
 
 
903
        :param base_path: base path
 
904
        :param relpath: relative url string for relative part of remote path.
 
905
        :return: urlencoded string for final path.
 
906
        """
 
907
        if not isinstance(relpath, str):
 
908
            raise errors.InvalidURL(relpath)
 
909
        relpath = _url_hex_escapes_re.sub(_unescape_safe_chars, relpath)
 
910
        if relpath.startswith('/'):
 
911
            base_parts = []
 
912
        else:
 
913
            base_parts = base_path.split('/')
 
914
        if len(base_parts) > 0 and base_parts[-1] == '':
 
915
            base_parts = base_parts[:-1]
 
916
        for p in relpath.split('/'):
 
917
            if p == '..':
 
918
                if len(base_parts) == 0:
 
919
                    # In most filesystems, a request for the parent
 
920
                    # of root, just returns root.
 
921
                    continue
 
922
                base_parts.pop()
 
923
            elif p == '.':
 
924
                continue # No-op
 
925
            elif p != '':
 
926
                base_parts.append(p)
 
927
        path = '/'.join(base_parts)
 
928
        if not path.startswith('/'):
 
929
            path = '/' + path
 
930
        return path
 
931
 
 
932
    def clone(self, offset=None):
 
933
        """Return a new URL for a path relative to this URL.
 
934
 
 
935
        :param offset: A relative path, already urlencoded
 
936
        :return: `URL` instance
 
937
        """
 
938
        if offset is not None:
 
939
            relative = unescape(offset).encode('utf-8')
 
940
            path = self._combine_paths(self.path, relative)
 
941
            path = quote(path, safe="/~")
 
942
        else:
 
943
            path = self.quoted_path
 
944
        return self.__class__(self.scheme, self.quoted_user,
 
945
                self.quoted_password, self.quoted_host, self.port,
 
946
                path)
 
947
 
 
948
 
 
949
def parse_url(url):
 
950
    """Extract the server address, the credentials and the path from the url.
 
951
 
 
952
    user, password, host and path should be quoted if they contain reserved
 
953
    chars.
 
954
 
 
955
    :param url: an quoted url
 
956
    :return: (scheme, user, password, host, port, path) tuple, all fields
 
957
        are unquoted.
 
958
    """
 
959
    parsed_url = URL.from_string(url)
 
960
    return (parsed_url.scheme, parsed_url.user, parsed_url.password,
 
961
        parsed_url.host, parsed_url.port, parsed_url.path)