mercurial/pathutil.py
changeset 20033 f962870712da
child 21568 8dd17b19e722
equal deleted inserted replaced
20032:175c6fd8cacc 20033:f962870712da
       
     1 import os, errno, stat
       
     2 
       
     3 import util
       
     4 from i18n import _
       
     5 
       
     6 class pathauditor(object):
       
     7     '''ensure that a filesystem path contains no banned components.
       
     8     the following properties of a path are checked:
       
     9 
       
    10     - ends with a directory separator
       
    11     - under top-level .hg
       
    12     - starts at the root of a windows drive
       
    13     - contains ".."
       
    14     - traverses a symlink (e.g. a/symlink_here/b)
       
    15     - inside a nested repository (a callback can be used to approve
       
    16       some nested repositories, e.g., subrepositories)
       
    17     '''
       
    18 
       
    19     def __init__(self, root, callback=None):
       
    20         self.audited = set()
       
    21         self.auditeddir = set()
       
    22         self.root = root
       
    23         self.callback = callback
       
    24         if os.path.lexists(root) and not util.checkcase(root):
       
    25             self.normcase = util.normcase
       
    26         else:
       
    27             self.normcase = lambda x: x
       
    28 
       
    29     def __call__(self, path):
       
    30         '''Check the relative path.
       
    31         path may contain a pattern (e.g. foodir/**.txt)'''
       
    32 
       
    33         path = util.localpath(path)
       
    34         normpath = self.normcase(path)
       
    35         if normpath in self.audited:
       
    36             return
       
    37         # AIX ignores "/" at end of path, others raise EISDIR.
       
    38         if util.endswithsep(path):
       
    39             raise util.Abort(_("path ends in directory separator: %s") % path)
       
    40         parts = util.splitpath(path)
       
    41         if (os.path.splitdrive(path)[0]
       
    42             or parts[0].lower() in ('.hg', '.hg.', '')
       
    43             or os.pardir in parts):
       
    44             raise util.Abort(_("path contains illegal component: %s") % path)
       
    45         if '.hg' in path.lower():
       
    46             lparts = [p.lower() for p in parts]
       
    47             for p in '.hg', '.hg.':
       
    48                 if p in lparts[1:]:
       
    49                     pos = lparts.index(p)
       
    50                     base = os.path.join(*parts[:pos])
       
    51                     raise util.Abort(_("path '%s' is inside nested repo %r")
       
    52                                      % (path, base))
       
    53 
       
    54         normparts = util.splitpath(normpath)
       
    55         assert len(parts) == len(normparts)
       
    56 
       
    57         parts.pop()
       
    58         normparts.pop()
       
    59         prefixes = []
       
    60         while parts:
       
    61             prefix = os.sep.join(parts)
       
    62             normprefix = os.sep.join(normparts)
       
    63             if normprefix in self.auditeddir:
       
    64                 break
       
    65             curpath = os.path.join(self.root, prefix)
       
    66             try:
       
    67                 st = os.lstat(curpath)
       
    68             except OSError, err:
       
    69                 # EINVAL can be raised as invalid path syntax under win32.
       
    70                 # They must be ignored for patterns can be checked too.
       
    71                 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
       
    72                     raise
       
    73             else:
       
    74                 if stat.S_ISLNK(st.st_mode):
       
    75                     raise util.Abort(
       
    76                         _('path %r traverses symbolic link %r')
       
    77                         % (path, prefix))
       
    78                 elif (stat.S_ISDIR(st.st_mode) and
       
    79                       os.path.isdir(os.path.join(curpath, '.hg'))):
       
    80                     if not self.callback or not self.callback(curpath):
       
    81                         raise util.Abort(_("path '%s' is inside nested "
       
    82                                            "repo %r")
       
    83                                          % (path, prefix))
       
    84             prefixes.append(normprefix)
       
    85             parts.pop()
       
    86             normparts.pop()
       
    87 
       
    88         self.audited.add(normpath)
       
    89         # only add prefixes to the cache after checking everything: we don't
       
    90         # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
       
    91         self.auditeddir.update(prefixes)
       
    92 
       
    93     def check(self, path):
       
    94         try:
       
    95             self(path)
       
    96             return True
       
    97         except (OSError, util.Abort):
       
    98             return False
       
    99 
       
   100 def canonpath(root, cwd, myname, auditor=None):
       
   101     '''return the canonical path of myname, given cwd and root'''
       
   102     if util.endswithsep(root):
       
   103         rootsep = root
       
   104     else:
       
   105         rootsep = root + os.sep
       
   106     name = myname
       
   107     if not os.path.isabs(name):
       
   108         name = os.path.join(root, cwd, name)
       
   109     name = os.path.normpath(name)
       
   110     if auditor is None:
       
   111         auditor = pathauditor(root)
       
   112     if name != rootsep and name.startswith(rootsep):
       
   113         name = name[len(rootsep):]
       
   114         auditor(name)
       
   115         return util.pconvert(name)
       
   116     elif name == root:
       
   117         return ''
       
   118     else:
       
   119         # Determine whether `name' is in the hierarchy at or beneath `root',
       
   120         # by iterating name=dirname(name) until that causes no change (can't
       
   121         # check name == '/', because that doesn't work on windows). The list
       
   122         # `rel' holds the reversed list of components making up the relative
       
   123         # file name we want.
       
   124         rel = []
       
   125         while True:
       
   126             try:
       
   127                 s = util.samefile(name, root)
       
   128             except OSError:
       
   129                 s = False
       
   130             if s:
       
   131                 if not rel:
       
   132                     # name was actually the same as root (maybe a symlink)
       
   133                     return ''
       
   134                 rel.reverse()
       
   135                 name = os.path.join(*rel)
       
   136                 auditor(name)
       
   137                 return util.pconvert(name)
       
   138             dirname, basename = util.split(name)
       
   139             rel.append(basename)
       
   140             if dirname == name:
       
   141                 break
       
   142             name = dirname
       
   143 
       
   144         raise util.Abort(_("%s not under root '%s'") % (myname, root))