/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/osutils.py

  • Committer: John Arbash Meinel
  • Date: 2006-04-28 17:18:42 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: john@arbash-meinel.com-20060428171842-04f65c89de82599e
A couple more fixes to make sure memory:/// works correctly.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Bazaar-NG -- distributed version control
 
2
#
 
3
# Copyright (C) 2005 by Canonical Ltd
 
4
#
 
5
# This program is free software; you can redistribute it and/or modify
 
6
# it under the terms of the GNU General Public License as published by
 
7
# the Free Software Foundation; either version 2 of the License, or
 
8
# (at your option) any later version.
 
9
#
 
10
# This program is distributed in the hope that it will be useful,
 
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
13
# GNU General Public License for more details.
 
14
#
 
15
# You should have received a copy of the GNU General Public License
 
16
# along with this program; if not, write to the Free Software
 
17
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
18
 
 
19
from shutil import copyfile
 
20
from stat import (S_ISREG, S_ISDIR, S_ISLNK, ST_MODE, ST_SIZE,
 
21
                  S_ISCHR, S_ISBLK, S_ISFIFO, S_ISSOCK)
 
22
from cStringIO import StringIO
 
23
import errno
 
24
import os
 
25
import re
 
26
import sha
 
27
import string
 
28
import sys
 
29
import time
 
30
import types
 
31
import tempfile
 
32
import unicodedata
 
33
import urllib
 
34
from ntpath import (abspath as _nt_abspath,
 
35
                    join as _nt_join,
 
36
                    normpath as _nt_normpath,
 
37
                    realpath as _nt_realpath,
 
38
                    )
 
39
 
 
40
import bzrlib
 
41
from bzrlib.errors import (BzrError,
 
42
                           BzrBadParameterNotUnicode,
 
43
                           NoSuchFile,
 
44
                           PathNotChild,
 
45
                           IllegalPath,
 
46
                           InvalidURL,
 
47
                           )
 
48
from bzrlib.trace import mutter
 
49
 
 
50
 
 
51
def make_readonly(filename):
 
52
    """Make a filename read-only."""
 
53
    mod = os.stat(filename).st_mode
 
54
    mod = mod & 0777555
 
55
    os.chmod(filename, mod)
 
56
 
 
57
 
 
58
def make_writable(filename):
 
59
    mod = os.stat(filename).st_mode
 
60
    mod = mod | 0200
 
61
    os.chmod(filename, mod)
 
62
 
 
63
 
 
64
_QUOTE_RE = None
 
65
 
 
66
 
 
67
def quotefn(f):
 
68
    """Return a quoted filename filename
 
69
 
 
70
    This previously used backslash quoting, but that works poorly on
 
71
    Windows."""
 
72
    # TODO: I'm not really sure this is the best format either.x
 
73
    global _QUOTE_RE
 
74
    if _QUOTE_RE == None:
 
75
        _QUOTE_RE = re.compile(r'([^a-zA-Z0-9.,:/\\_~-])')
 
76
        
 
77
    if _QUOTE_RE.search(f):
 
78
        return '"' + f + '"'
 
79
    else:
 
80
        return f
 
81
 
 
82
 
 
83
def file_kind(f):
 
84
    mode = os.lstat(f)[ST_MODE]
 
85
    if S_ISREG(mode):
 
86
        return 'file'
 
87
    elif S_ISDIR(mode):
 
88
        return 'directory'
 
89
    elif S_ISLNK(mode):
 
90
        return 'symlink'
 
91
    elif S_ISCHR(mode):
 
92
        return 'chardev'
 
93
    elif S_ISBLK(mode):
 
94
        return 'block'
 
95
    elif S_ISFIFO(mode):
 
96
        return 'fifo'
 
97
    elif S_ISSOCK(mode):
 
98
        return 'socket'
 
99
    else:
 
100
        return 'unknown'
 
101
 
 
102
 
 
103
def kind_marker(kind):
 
104
    if kind == 'file':
 
105
        return ''
 
106
    elif kind == 'directory':
 
107
        return '/'
 
108
    elif kind == 'symlink':
 
109
        return '@'
 
110
    else:
 
111
        raise BzrError('invalid file kind %r' % kind)
 
112
 
 
113
def lexists(f):
 
114
    if hasattr(os.path, 'lexists'):
 
115
        return os.path.lexists(f)
 
116
    try:
 
117
        if hasattr(os, 'lstat'):
 
118
            os.lstat(f)
 
119
        else:
 
120
            os.stat(f)
 
121
        return True
 
122
    except OSError,e:
 
123
        if e.errno == errno.ENOENT:
 
124
            return False;
 
125
        else:
 
126
            raise BzrError("lstat/stat of (%r): %r" % (f, e))
 
127
 
 
128
def fancy_rename(old, new, rename_func, unlink_func):
 
129
    """A fancy rename, when you don't have atomic rename.
 
130
    
 
131
    :param old: The old path, to rename from
 
132
    :param new: The new path, to rename to
 
133
    :param rename_func: The potentially non-atomic rename function
 
134
    :param unlink_func: A way to delete the target file if the full rename succeeds
 
135
    """
 
136
 
 
137
    # sftp rename doesn't allow overwriting, so play tricks:
 
138
    import random
 
139
    base = os.path.basename(new)
 
140
    dirname = os.path.dirname(new)
 
141
    tmp_name = u'tmp.%s.%.9f.%d.%s' % (base, time.time(), os.getpid(), rand_chars(10))
 
142
    tmp_name = pathjoin(dirname, tmp_name)
 
143
 
 
144
    # Rename the file out of the way, but keep track if it didn't exist
 
145
    # We don't want to grab just any exception
 
146
    # something like EACCES should prevent us from continuing
 
147
    # The downside is that the rename_func has to throw an exception
 
148
    # with an errno = ENOENT, or NoSuchFile
 
149
    file_existed = False
 
150
    try:
 
151
        rename_func(new, tmp_name)
 
152
    except (NoSuchFile,), e:
 
153
        pass
 
154
    except IOError, e:
 
155
        # RBC 20060103 abstraction leakage: the paramiko SFTP clients rename
 
156
        # function raises an IOError with errno == None when a rename fails.
 
157
        # This then gets caught here.
 
158
        if e.errno not in (None, errno.ENOENT, errno.ENOTDIR):
 
159
            raise
 
160
    except Exception, e:
 
161
        if (not hasattr(e, 'errno') 
 
162
            or e.errno not in (errno.ENOENT, errno.ENOTDIR)):
 
163
            raise
 
164
    else:
 
165
        file_existed = True
 
166
 
 
167
    success = False
 
168
    try:
 
169
        # This may throw an exception, in which case success will
 
170
        # not be set.
 
171
        rename_func(old, new)
 
172
        success = True
 
173
    finally:
 
174
        if file_existed:
 
175
            # If the file used to exist, rename it back into place
 
176
            # otherwise just delete it from the tmp location
 
177
            if success:
 
178
                unlink_func(tmp_name)
 
179
            else:
 
180
                rename_func(tmp_name, new)
 
181
 
 
182
 
 
183
def urlescape(relpath):
 
184
    """Escape relpath to be a valid url."""
 
185
    if isinstance(relpath, unicode):
 
186
        relpath = relpath.encode('utf-8')
 
187
    # After quoting and encoding, the path should be perfectly
 
188
    # safe as a plain ASCII string, str() just enforces this
 
189
    return str(urllib.quote(relpath))
 
190
 
 
191
 
 
192
def urlunescape(url):
 
193
    """Unescape relpath from url format.
 
194
 
 
195
    This returns a Unicode path from a URL
 
196
    """
 
197
    # jam 20060427 URLs are supposed to be ASCII only strings
 
198
    #       If they are passed in as unicode, urllib.unquote
 
199
    #       will return a UNICODE string, which actually contains
 
200
    #       utf-8 bytes. So we have to ensure that they are
 
201
    #       plain ASCII strings, or the final .decode will
 
202
    #       try to encode the UNICODE => ASCII, and then decode
 
203
    #       it into utf-8.
 
204
    try:
 
205
        url = str(url)
 
206
    except UnicodeError, e:
 
207
        raise InvalidURL(url, 'URL was not a plain ASCII url: %s' % (e,))
 
208
    unquoted = urllib.unquote(url)
 
209
    try:
 
210
        unicode_path = unquoted.decode('utf-8')
 
211
    except UnicodeError, e:
 
212
        raise InvalidURL(url, 'Unable to encode the URL as utf-8: %s' % (e,))
 
213
    return unicode_path
 
214
 
 
215
 
 
216
# These are characters that if escaped, should stay that way
 
217
_no_decode_chars = ';/?:@&=+$,#'
 
218
_no_decode_ords = [ord(c) for c in _no_decode_chars]
 
219
_no_decode_hex = (['%02x' % o for o in _no_decode_ords] 
 
220
                + ['%02X' % o for o in _no_decode_ords])
 
221
_hex_display_map = urllib._hextochr.copy()
 
222
_hex_display_map.update((hex,'%'+hex) for hex in _no_decode_hex)
 
223
#These entries get mapped to themselves
 
224
 
 
225
 
 
226
def urlfordisplay(url):
 
227
    """Decode what you can for a URL, so that we get a nice looking path.
 
228
 
 
229
    This will turn file:// urls into local paths, and try to decode
 
230
    any portions of a http:// style url that it can.
 
231
    """
 
232
    if url.startswith('file://'):
 
233
        return local_path_from_url(url)
 
234
 
 
235
    # Split into sections to try to decode utf-8
 
236
    res = url.split('/')
 
237
    for i in xrange(1, len(res)):
 
238
        escaped_chunks = res[i].split('%')
 
239
        for j in xrange(1, len(escaped_chunks)):
 
240
            item = escaped_chunks[j]
 
241
            try:
 
242
                escaped_chunks[j] = _hex_display_map[item[:2]] + item[2:]
 
243
            except KeyError:
 
244
                # Put back the percent symbol
 
245
                escaped_chunks[j] = '%' + item
 
246
            except UnicodeDecodeError:
 
247
                escaped_chunks[j] = unichr(int(item[:2], 16)) + item[2:]
 
248
        unescaped = ''.join(escaped_chunks)
 
249
        try:
 
250
            res[i] = unescaped.decode('utf-8')
 
251
        except UnicodeDecodeError:
 
252
            # If this path segment cannot be properly utf-8 decoded
 
253
            # after doing unescaping we will just leave it alone
 
254
            pass
 
255
    return '/'.join(res)
 
256
 
 
257
def _posix_local_path_to_url(path):
 
258
    """Convert a local path like ./foo into a URL like file:///path/to/foo
 
259
 
 
260
    This also handles transforming escaping unicode characters, etc.
 
261
    """
 
262
    # importing directly from posixpath allows us to test this 
 
263
    # on non-posix platforms
 
264
    from posixpath import normpath
 
265
    return 'file://' + urlescape(normpath(_posix_abspath(path)))
 
266
 
 
267
 
 
268
def _posix_local_path_from_url(url):
 
269
    """Convert a url like file:///path/to/foo into /path/to/foo"""
 
270
    if not url.startswith('file:///'):
 
271
        raise InvalidURL(url, 'local urls must start with file:///')
 
272
    # We only strip off 2 slashes
 
273
    return urlunescape(url[len('file://'):])
 
274
 
 
275
 
 
276
def _win32_local_path_to_url(path):
 
277
    """Convert a local path like ./foo into a URL like file:///C|/path/to/foo
 
278
 
 
279
    This also handles transforming escaping unicode characters, etc.
 
280
    """
 
281
    # importing directly from ntpath allows us to test this 
 
282
    # on non-win32 platforms
 
283
    # TODO: jam 20060426 consider moving this import outside of the function
 
284
    win32_path = _nt_normpath(_win32_abspath(path)).replace('\\', '/')
 
285
    return 'file:///' + win32_path[0] + '|' + urlescape(win32_path[2:])
 
286
 
 
287
 
 
288
def _win32_local_path_from_url(url):
 
289
    """Convert a url like file:///C|/path/to/foo into C:/path/to/foo"""
 
290
    if not url.startswith('file:///'):
 
291
        raise InvalidURL(url, 'local urls must start with file:///')
 
292
    # We strip off all 3 slashes
 
293
    win32_url = url[len('file:///'):]
 
294
    if (win32_url[0] not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
 
295
        or win32_url[1] not in  '|:'
 
296
        or win32_url[2] != '/'):
 
297
        raise InvalidURL(url, 'Win32 file urls start with file:///X|/, where X is a valid drive letter')
 
298
    # TODO: jam 20060426, we could .upper() or .lower() the drive letter
 
299
    #       for better consistency.
 
300
    return win32_url[0] + u':' + urlunescape(win32_url[2:])
 
301
 
 
302
 
 
303
# In Python 2.4.2 and older, os.path.abspath and os.path.realpath
 
304
# choke on a Unicode string containing a relative path if
 
305
# os.getcwd() returns a non-sys.getdefaultencoding()-encoded
 
306
# string.
 
307
_fs_enc = sys.getfilesystemencoding()
 
308
def _posix_abspath(path):
 
309
    return os.path.abspath(path.encode(_fs_enc)).decode(_fs_enc)
 
310
    # jam 20060426 This is another possibility which mimics 
 
311
    # os.path.abspath, only uses unicode characters instead
 
312
    # if not os.path.isabs(path):
 
313
    #     return os.path.join(os.getcwdu(), path)
 
314
    # return path
 
315
 
 
316
 
 
317
def _posix_realpath(path):
 
318
    return os.path.realpath(path.encode(_fs_enc)).decode(_fs_enc)
 
319
 
 
320
 
 
321
def _win32_abspath(path):
 
322
    return _nt_abspath(path.encode(_fs_enc)).decode(_fs_enc).replace('\\', '/')
 
323
 
 
324
 
 
325
def _win32_realpath(path):
 
326
    return _nt_realpath(path.encode(_fs_enc)).decode(_fs_enc).replace('\\', '/')
 
327
 
 
328
 
 
329
def _win32_pathjoin(*args):
 
330
    return _nt_join(*args).replace('\\', '/')
 
331
 
 
332
 
 
333
def _win32_normpath(path):
 
334
    return _nt_normpath(path).replace('\\', '/')
 
335
 
 
336
 
 
337
def _win32_getcwd():
 
338
    return os.getcwdu().replace('\\', '/')
 
339
 
 
340
 
 
341
def _win32_mkdtemp(*args, **kwargs):
 
342
    return tempfile.mkdtemp(*args, **kwargs).replace('\\', '/')
 
343
 
 
344
 
 
345
def _win32_rename(old, new):
 
346
    fancy_rename(old, new, rename_func=os.rename, unlink_func=os.unlink)
 
347
 
 
348
 
 
349
# Default is to just use the python builtins
 
350
abspath = _posix_abspath
 
351
realpath = _posix_realpath
 
352
pathjoin = os.path.join
 
353
normpath = os.path.normpath
 
354
getcwd = os.getcwdu
 
355
mkdtemp = tempfile.mkdtemp
 
356
rename = os.rename
 
357
dirname = os.path.dirname
 
358
basename = os.path.basename
 
359
local_path_to_url = _posix_local_path_to_url
 
360
local_path_from_url = _posix_local_path_from_url
 
361
 
 
362
MIN_ABS_PATHLENGTH = 1
 
363
MIN_ABS_URLPATHLENGTH = len('file:///')
 
364
 
 
365
 
 
366
if sys.platform == 'win32':
 
367
    abspath = _win32_abspath
 
368
    realpath = _win32_realpath
 
369
    pathjoin = _win32_pathjoin
 
370
    normpath = _win32_normpath
 
371
    getcwd = _win32_getcwd
 
372
    mkdtemp = _win32_mkdtemp
 
373
    rename = _win32_rename
 
374
 
 
375
    local_path_to_url = _win32_local_path_to_url
 
376
    local_path_from_url = _win32_local_path_from_url
 
377
 
 
378
    MIN_ABS_PATHLENGTH = 3
 
379
    MIN_ABS_URLPATHLENGTH = len('file:///C|/')
 
380
 
 
381
 
 
382
def normalizepath(f):
 
383
    if hasattr(os.path, 'realpath'):
 
384
        F = realpath
 
385
    else:
 
386
        F = abspath
 
387
    [p,e] = os.path.split(f)
 
388
    if e == "" or e == "." or e == "..":
 
389
        return F(f)
 
390
    else:
 
391
        return pathjoin(F(p), e)
 
392
 
 
393
 
 
394
def backup_file(fn):
 
395
    """Copy a file to a backup.
 
396
 
 
397
    Backups are named in GNU-style, with a ~ suffix.
 
398
 
 
399
    If the file is already a backup, it's not copied.
 
400
    """
 
401
    if fn[-1] == '~':
 
402
        return
 
403
    bfn = fn + '~'
 
404
 
 
405
    if has_symlinks() and os.path.islink(fn):
 
406
        target = os.readlink(fn)
 
407
        os.symlink(target, bfn)
 
408
        return
 
409
    inf = file(fn, 'rb')
 
410
    try:
 
411
        content = inf.read()
 
412
    finally:
 
413
        inf.close()
 
414
    
 
415
    outf = file(bfn, 'wb')
 
416
    try:
 
417
        outf.write(content)
 
418
    finally:
 
419
        outf.close()
 
420
 
 
421
 
 
422
def isdir(f):
 
423
    """True if f is an accessible directory."""
 
424
    try:
 
425
        return S_ISDIR(os.lstat(f)[ST_MODE])
 
426
    except OSError:
 
427
        return False
 
428
 
 
429
 
 
430
def isfile(f):
 
431
    """True if f is a regular file."""
 
432
    try:
 
433
        return S_ISREG(os.lstat(f)[ST_MODE])
 
434
    except OSError:
 
435
        return False
 
436
 
 
437
def islink(f):
 
438
    """True if f is a symlink."""
 
439
    try:
 
440
        return S_ISLNK(os.lstat(f)[ST_MODE])
 
441
    except OSError:
 
442
        return False
 
443
 
 
444
def is_inside(dir, fname):
 
445
    """True if fname is inside dir.
 
446
    
 
447
    The parameters should typically be passed to osutils.normpath first, so
 
448
    that . and .. and repeated slashes are eliminated, and the separators
 
449
    are canonical for the platform.
 
450
    
 
451
    The empty string as a dir name is taken as top-of-tree and matches 
 
452
    everything.
 
453
    
 
454
    >>> is_inside('src', pathjoin('src', 'foo.c'))
 
455
    True
 
456
    >>> is_inside('src', 'srccontrol')
 
457
    False
 
458
    >>> is_inside('src', pathjoin('src', 'a', 'a', 'a', 'foo.c'))
 
459
    True
 
460
    >>> is_inside('foo.c', 'foo.c')
 
461
    True
 
462
    >>> is_inside('foo.c', '')
 
463
    False
 
464
    >>> is_inside('', 'foo.c')
 
465
    True
 
466
    """
 
467
    # XXX: Most callers of this can actually do something smarter by 
 
468
    # looking at the inventory
 
469
    if dir == fname:
 
470
        return True
 
471
    
 
472
    if dir == '':
 
473
        return True
 
474
 
 
475
    if dir[-1] != '/':
 
476
        dir += '/'
 
477
 
 
478
    return fname.startswith(dir)
 
479
 
 
480
 
 
481
def is_inside_any(dir_list, fname):
 
482
    """True if fname is inside any of given dirs."""
 
483
    for dirname in dir_list:
 
484
        if is_inside(dirname, fname):
 
485
            return True
 
486
    else:
 
487
        return False
 
488
 
 
489
 
 
490
def pumpfile(fromfile, tofile):
 
491
    """Copy contents of one file to another."""
 
492
    BUFSIZE = 32768
 
493
    while True:
 
494
        b = fromfile.read(BUFSIZE)
 
495
        if not b:
 
496
            break
 
497
        tofile.write(b)
 
498
 
 
499
 
 
500
def file_iterator(input_file, readsize=32768):
 
501
    while True:
 
502
        b = input_file.read(readsize)
 
503
        if len(b) == 0:
 
504
            break
 
505
        yield b
 
506
 
 
507
 
 
508
def sha_file(f):
 
509
    if hasattr(f, 'tell'):
 
510
        assert f.tell() == 0
 
511
    s = sha.new()
 
512
    BUFSIZE = 128<<10
 
513
    while True:
 
514
        b = f.read(BUFSIZE)
 
515
        if not b:
 
516
            break
 
517
        s.update(b)
 
518
    return s.hexdigest()
 
519
 
 
520
 
 
521
 
 
522
def sha_strings(strings):
 
523
    """Return the sha-1 of concatenation of strings"""
 
524
    s = sha.new()
 
525
    map(s.update, strings)
 
526
    return s.hexdigest()
 
527
 
 
528
 
 
529
def sha_string(f):
 
530
    s = sha.new()
 
531
    s.update(f)
 
532
    return s.hexdigest()
 
533
 
 
534
 
 
535
def fingerprint_file(f):
 
536
    s = sha.new()
 
537
    b = f.read()
 
538
    s.update(b)
 
539
    size = len(b)
 
540
    return {'size': size,
 
541
            'sha1': s.hexdigest()}
 
542
 
 
543
 
 
544
def compare_files(a, b):
 
545
    """Returns true if equal in contents"""
 
546
    BUFSIZE = 4096
 
547
    while True:
 
548
        ai = a.read(BUFSIZE)
 
549
        bi = b.read(BUFSIZE)
 
550
        if ai != bi:
 
551
            return False
 
552
        if ai == '':
 
553
            return True
 
554
 
 
555
 
 
556
def local_time_offset(t=None):
 
557
    """Return offset of local zone from GMT, either at present or at time t."""
 
558
    # python2.3 localtime() can't take None
 
559
    if t == None:
 
560
        t = time.time()
 
561
        
 
562
    if time.localtime(t).tm_isdst and time.daylight:
 
563
        return -time.altzone
 
564
    else:
 
565
        return -time.timezone
 
566
 
 
567
    
 
568
def format_date(t, offset=0, timezone='original', date_fmt=None, 
 
569
                show_offset=True):
 
570
    ## TODO: Perhaps a global option to use either universal or local time?
 
571
    ## Or perhaps just let people set $TZ?
 
572
    assert isinstance(t, float)
 
573
    
 
574
    if timezone == 'utc':
 
575
        tt = time.gmtime(t)
 
576
        offset = 0
 
577
    elif timezone == 'original':
 
578
        if offset == None:
 
579
            offset = 0
 
580
        tt = time.gmtime(t + offset)
 
581
    elif timezone == 'local':
 
582
        tt = time.localtime(t)
 
583
        offset = local_time_offset(t)
 
584
    else:
 
585
        raise BzrError("unsupported timezone format %r" % timezone,
 
586
                       ['options are "utc", "original", "local"'])
 
587
    if date_fmt is None:
 
588
        date_fmt = "%a %Y-%m-%d %H:%M:%S"
 
589
    if show_offset:
 
590
        offset_str = ' %+03d%02d' % (offset / 3600, (offset / 60) % 60)
 
591
    else:
 
592
        offset_str = ''
 
593
    return (time.strftime(date_fmt, tt) +  offset_str)
 
594
 
 
595
 
 
596
def compact_date(when):
 
597
    return time.strftime('%Y%m%d%H%M%S', time.gmtime(when))
 
598
    
 
599
 
 
600
 
 
601
def filesize(f):
 
602
    """Return size of given open file."""
 
603
    return os.fstat(f.fileno())[ST_SIZE]
 
604
 
 
605
 
 
606
# Define rand_bytes based on platform.
 
607
try:
 
608
    # Python 2.4 and later have os.urandom,
 
609
    # but it doesn't work on some arches
 
610
    os.urandom(1)
 
611
    rand_bytes = os.urandom
 
612
except (NotImplementedError, AttributeError):
 
613
    # If python doesn't have os.urandom, or it doesn't work,
 
614
    # then try to first pull random data from /dev/urandom
 
615
    if os.path.exists("/dev/urandom"):
 
616
        rand_bytes = file('/dev/urandom', 'rb').read
 
617
    # Otherwise, use this hack as a last resort
 
618
    else:
 
619
        # not well seeded, but better than nothing
 
620
        def rand_bytes(n):
 
621
            import random
 
622
            s = ''
 
623
            while n:
 
624
                s += chr(random.randint(0, 255))
 
625
                n -= 1
 
626
            return s
 
627
 
 
628
 
 
629
ALNUM = '0123456789abcdefghijklmnopqrstuvwxyz'
 
630
def rand_chars(num):
 
631
    """Return a random string of num alphanumeric characters
 
632
    
 
633
    The result only contains lowercase chars because it may be used on 
 
634
    case-insensitive filesystems.
 
635
    """
 
636
    s = ''
 
637
    for raw_byte in rand_bytes(num):
 
638
        s += ALNUM[ord(raw_byte) % 36]
 
639
    return s
 
640
 
 
641
 
 
642
## TODO: We could later have path objects that remember their list
 
643
## decomposition (might be too tricksy though.)
 
644
 
 
645
def splitpath(p):
 
646
    """Turn string into list of parts.
 
647
 
 
648
    >>> splitpath('a')
 
649
    ['a']
 
650
    >>> splitpath('a/b')
 
651
    ['a', 'b']
 
652
    >>> splitpath('a/./b')
 
653
    ['a', 'b']
 
654
    >>> splitpath('a/.b')
 
655
    ['a', '.b']
 
656
    >>> splitpath('a/../b')
 
657
    Traceback (most recent call last):
 
658
    ...
 
659
    BzrError: sorry, '..' not allowed in path
 
660
    """
 
661
    assert isinstance(p, types.StringTypes)
 
662
 
 
663
    # split on either delimiter because people might use either on
 
664
    # Windows
 
665
    ps = re.split(r'[\\/]', p)
 
666
 
 
667
    rps = []
 
668
    for f in ps:
 
669
        if f == '..':
 
670
            raise BzrError("sorry, %r not allowed in path" % f)
 
671
        elif (f == '.') or (f == ''):
 
672
            pass
 
673
        else:
 
674
            rps.append(f)
 
675
    return rps
 
676
 
 
677
def joinpath(p):
 
678
    assert isinstance(p, list)
 
679
    for f in p:
 
680
        if (f == '..') or (f == None) or (f == ''):
 
681
            raise BzrError("sorry, %r not allowed in path" % f)
 
682
    return pathjoin(*p)
 
683
 
 
684
 
 
685
def appendpath(p1, p2):
 
686
    if p1 == '':
 
687
        return p2
 
688
    else:
 
689
        return pathjoin(p1, p2)
 
690
    
 
691
 
 
692
def split_lines(s):
 
693
    """Split s into lines, but without removing the newline characters."""
 
694
    lines = s.split('\n')
 
695
    result = [line + '\n' for line in lines[:-1]]
 
696
    if lines[-1]:
 
697
        result.append(lines[-1])
 
698
    return result
 
699
 
 
700
 
 
701
def hardlinks_good():
 
702
    return sys.platform not in ('win32', 'cygwin', 'darwin')
 
703
 
 
704
 
 
705
def link_or_copy(src, dest):
 
706
    """Hardlink a file, or copy it if it can't be hardlinked."""
 
707
    if not hardlinks_good():
 
708
        copyfile(src, dest)
 
709
        return
 
710
    try:
 
711
        os.link(src, dest)
 
712
    except (OSError, IOError), e:
 
713
        if e.errno != errno.EXDEV:
 
714
            raise
 
715
        copyfile(src, dest)
 
716
 
 
717
def delete_any(full_path):
 
718
    """Delete a file or directory."""
 
719
    try:
 
720
        os.unlink(full_path)
 
721
    except OSError, e:
 
722
    # We may be renaming a dangling inventory id
 
723
        if e.errno not in (errno.EISDIR, errno.EACCES, errno.EPERM):
 
724
            raise
 
725
        os.rmdir(full_path)
 
726
 
 
727
 
 
728
def has_symlinks():
 
729
    if hasattr(os, 'symlink'):
 
730
        return True
 
731
    else:
 
732
        return False
 
733
        
 
734
 
 
735
def contains_whitespace(s):
 
736
    """True if there are any whitespace characters in s."""
 
737
    for ch in string.whitespace:
 
738
        if ch in s:
 
739
            return True
 
740
    else:
 
741
        return False
 
742
 
 
743
 
 
744
def contains_linebreaks(s):
 
745
    """True if there is any vertical whitespace in s."""
 
746
    for ch in '\f\n\r':
 
747
        if ch in s:
 
748
            return True
 
749
    else:
 
750
        return False
 
751
 
 
752
 
 
753
def relpath(base, path):
 
754
    """Return path relative to base, or raise exception.
 
755
 
 
756
    The path may be either an absolute path or a path relative to the
 
757
    current working directory.
 
758
 
 
759
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
760
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
761
    avoids that problem.
 
762
    """
 
763
 
 
764
    assert len(base) >= MIN_ABS_PATHLENGTH, ('Length of base must be equal or'
 
765
        ' exceed the platform minimum length (which is %d)' % 
 
766
        MIN_ABS_PATHLENGTH)
 
767
 
 
768
    rp = abspath(path)
 
769
 
 
770
    s = []
 
771
    head = rp
 
772
    while len(head) >= len(base):
 
773
        if head == base:
 
774
            break
 
775
        head, tail = os.path.split(head)
 
776
        if tail:
 
777
            s.insert(0, tail)
 
778
    else:
 
779
        raise PathNotChild(rp, base)
 
780
 
 
781
    if s:
 
782
        return pathjoin(*s)
 
783
    else:
 
784
        return ''
 
785
 
 
786
 
 
787
def urlrelpath(base, path):
 
788
    """Compute just the relative sub-portion of a url
 
789
    
 
790
    This assumes that both paths are already fully specified file:// URLs.
 
791
    """
 
792
    assert len(base) >= MIN_ABS_URLPATHLENGTH, ('Length of base must be equal or'
 
793
        ' exceed the platform minimum url length (which is %d)' % 
 
794
        MIN_ABS_URLPATHLENGTH)
 
795
 
 
796
    base = local_path_from_url(base)
 
797
    path = local_path_from_url(path)
 
798
    return urlescape(relpath(base, path))
 
799
 
 
800
 
 
801
def safe_unicode(unicode_or_utf8_string):
 
802
    """Coerce unicode_or_utf8_string into unicode.
 
803
 
 
804
    If it is unicode, it is returned.
 
805
    Otherwise it is decoded from utf-8. If a decoding error
 
806
    occurs, it is wrapped as a If the decoding fails, the exception is wrapped 
 
807
    as a BzrBadParameter exception.
 
808
    """
 
809
    if isinstance(unicode_or_utf8_string, unicode):
 
810
        return unicode_or_utf8_string
 
811
    try:
 
812
        return unicode_or_utf8_string.decode('utf8')
 
813
    except UnicodeDecodeError:
 
814
        raise BzrBadParameterNotUnicode(unicode_or_utf8_string)
 
815
 
 
816
 
 
817
_platform_normalizes_filenames = False
 
818
if sys.platform == 'darwin':
 
819
    _platform_normalizes_filenames = True
 
820
 
 
821
 
 
822
def normalizes_filenames():
 
823
    """Return True if this platform normalizes unicode filenames.
 
824
 
 
825
    Mac OSX does, Windows/Linux do not.
 
826
    """
 
827
    return _platform_normalizes_filenames
 
828
 
 
829
 
 
830
if _platform_normalizes_filenames:
 
831
    def unicode_filename(path):
 
832
        """Make sure 'path' is a properly normalized filename.
 
833
 
 
834
        On platforms where the system normalizes filenames (Mac OSX),
 
835
        you can access a file by any path which will normalize
 
836
        correctly.
 
837
        Internally, bzr only supports NFC/NFKC normalization, since
 
838
        that is the standard for XML documents.
 
839
        So we return an normalized path, and indicate this has been
 
840
        properly normalized.
 
841
 
 
842
        :return: (path, is_normalized) Return a path which can
 
843
                access the file, and whether or not this path is
 
844
                normalized.
 
845
        """
 
846
        return unicodedata.normalize('NFKC', path), True
 
847
else:
 
848
    def unicode_filename(path):
 
849
        """Make sure 'path' is a properly normalized filename.
 
850
 
 
851
        On platforms where the system does not normalize filenames 
 
852
        (Windows, Linux), you have to access a file by its exact path.
 
853
        Internally, bzr only supports NFC/NFKC normalization, since
 
854
        that is the standard for XML documents.
 
855
        So we return the original path, and indicate if this is
 
856
        properly normalized.
 
857
 
 
858
        :return: (path, is_normalized) Return a path which can
 
859
                access the file, and whether or not this path is
 
860
                normalized.
 
861
        """
 
862
        return path, unicodedata.normalize('NFKC', path) == path
 
863
 
 
864
 
 
865
def terminal_width():
 
866
    """Return estimated terminal width."""
 
867
 
 
868
    # TODO: Do something smart on Windows?
 
869
 
 
870
    # TODO: Is there anything that gets a better update when the window
 
871
    # is resized while the program is running? We could use the Python termcap
 
872
    # library.
 
873
    try:
 
874
        return int(os.environ['COLUMNS'])
 
875
    except (IndexError, KeyError, ValueError):
 
876
        return 80
 
877
 
 
878
def supports_executable():
 
879
    return sys.platform != "win32"
 
880
 
 
881
 
 
882
def strip_url_trailing_slash(path):
 
883
    """Strip trailing slash, except for root paths.
 
884
    The definition of 'root path' is platform-dependent.
 
885
    """
 
886
    assert path.startswith('file:///'), \
 
887
        'strip_url_trailing_slash expects file:// urls (%s)' % path
 
888
    if len(path) != MIN_ABS_URLPATHLENGTH and path[-1] == '/':
 
889
        return path[:-1]
 
890
    else:
 
891
        return path
 
892
 
 
893
 
 
894
_validWin32PathRE = re.compile(r'^([A-Za-z]:[/\\])?[^:<>*"?\|]*$')
 
895
 
 
896
 
 
897
def check_legal_path(path):
 
898
    """Check whether the supplied path is legal.  
 
899
    This is only required on Windows, so we don't test on other platforms
 
900
    right now.
 
901
    """
 
902
    if sys.platform != "win32":
 
903
        return
 
904
    if _validWin32PathRE.match(path) is None:
 
905
        raise IllegalPath(path)