comparison mercurial/pathutil.py @ 20033:f962870712da

pathutil: tease out a new library to break an import cycle from canonpath use
author Augie Fackler <raf@durin42.com>
date Wed, 06 Nov 2013 18:19:04 -0500
parents
children 8dd17b19e722
comparison
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))