##// END OF EJS Templates
pathutil: demote two local functions to just be forwards...
Augie Fackler -
r25286:127a11f7 default
parent child Browse files
Show More
@@ -1,239 +1,195 b''
1 1 import os, errno, stat, posixpath
2 2
3 3 import encoding
4 4 import util
5 5 from i18n import _
6 6
7 7 def _lowerclean(s):
8 8 return encoding.hfsignoreclean(s.lower())
9 9
10 10 class pathauditor(object):
11 11 '''ensure that a filesystem path contains no banned components.
12 12 the following properties of a path are checked:
13 13
14 14 - ends with a directory separator
15 15 - under top-level .hg
16 16 - starts at the root of a windows drive
17 17 - contains ".."
18 18 - traverses a symlink (e.g. a/symlink_here/b)
19 19 - inside a nested repository (a callback can be used to approve
20 20 some nested repositories, e.g., subrepositories)
21 21 '''
22 22
23 23 def __init__(self, root, callback=None):
24 24 self.audited = set()
25 25 self.auditeddir = set()
26 26 self.root = root
27 27 self.callback = callback
28 28 if os.path.lexists(root) and not util.checkcase(root):
29 29 self.normcase = util.normcase
30 30 else:
31 31 self.normcase = lambda x: x
32 32
33 33 def __call__(self, path):
34 34 '''Check the relative path.
35 35 path may contain a pattern (e.g. foodir/**.txt)'''
36 36
37 37 path = util.localpath(path)
38 38 normpath = self.normcase(path)
39 39 if normpath in self.audited:
40 40 return
41 41 # AIX ignores "/" at end of path, others raise EISDIR.
42 42 if util.endswithsep(path):
43 43 raise util.Abort(_("path ends in directory separator: %s") % path)
44 44 parts = util.splitpath(path)
45 45 if (os.path.splitdrive(path)[0]
46 46 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
47 47 or os.pardir in parts):
48 48 raise util.Abort(_("path contains illegal component: %s") % path)
49 49 # Windows shortname aliases
50 50 for p in parts:
51 51 if "~" in p:
52 52 first, last = p.split("~", 1)
53 53 if last.isdigit() and first.upper() in ["HG", "HG8B6C"]:
54 54 raise util.Abort(_("path contains illegal component: %s")
55 55 % path)
56 56 if '.hg' in _lowerclean(path):
57 57 lparts = [_lowerclean(p.lower()) for p in parts]
58 58 for p in '.hg', '.hg.':
59 59 if p in lparts[1:]:
60 60 pos = lparts.index(p)
61 61 base = os.path.join(*parts[:pos])
62 62 raise util.Abort(_("path '%s' is inside nested repo %r")
63 63 % (path, base))
64 64
65 65 normparts = util.splitpath(normpath)
66 66 assert len(parts) == len(normparts)
67 67
68 68 parts.pop()
69 69 normparts.pop()
70 70 prefixes = []
71 71 while parts:
72 72 prefix = os.sep.join(parts)
73 73 normprefix = os.sep.join(normparts)
74 74 if normprefix in self.auditeddir:
75 75 break
76 76 curpath = os.path.join(self.root, prefix)
77 77 try:
78 78 st = os.lstat(curpath)
79 79 except OSError, err:
80 80 # EINVAL can be raised as invalid path syntax under win32.
81 81 # They must be ignored for patterns can be checked too.
82 82 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
83 83 raise
84 84 else:
85 85 if stat.S_ISLNK(st.st_mode):
86 86 raise util.Abort(
87 87 _('path %r traverses symbolic link %r')
88 88 % (path, prefix))
89 89 elif (stat.S_ISDIR(st.st_mode) and
90 90 os.path.isdir(os.path.join(curpath, '.hg'))):
91 91 if not self.callback or not self.callback(curpath):
92 92 raise util.Abort(_("path '%s' is inside nested "
93 93 "repo %r")
94 94 % (path, prefix))
95 95 prefixes.append(normprefix)
96 96 parts.pop()
97 97 normparts.pop()
98 98
99 99 self.audited.add(normpath)
100 100 # only add prefixes to the cache after checking everything: we don't
101 101 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
102 102 self.auditeddir.update(prefixes)
103 103
104 104 def check(self, path):
105 105 try:
106 106 self(path)
107 107 return True
108 108 except (OSError, util.Abort):
109 109 return False
110 110
111 111 def canonpath(root, cwd, myname, auditor=None):
112 112 '''return the canonical path of myname, given cwd and root'''
113 113 if util.endswithsep(root):
114 114 rootsep = root
115 115 else:
116 116 rootsep = root + os.sep
117 117 name = myname
118 118 if not os.path.isabs(name):
119 119 name = os.path.join(root, cwd, name)
120 120 name = os.path.normpath(name)
121 121 if auditor is None:
122 122 auditor = pathauditor(root)
123 123 if name != rootsep and name.startswith(rootsep):
124 124 name = name[len(rootsep):]
125 125 auditor(name)
126 126 return util.pconvert(name)
127 127 elif name == root:
128 128 return ''
129 129 else:
130 130 # Determine whether `name' is in the hierarchy at or beneath `root',
131 131 # by iterating name=dirname(name) until that causes no change (can't
132 132 # check name == '/', because that doesn't work on windows). The list
133 133 # `rel' holds the reversed list of components making up the relative
134 134 # file name we want.
135 135 rel = []
136 136 while True:
137 137 try:
138 138 s = util.samefile(name, root)
139 139 except OSError:
140 140 s = False
141 141 if s:
142 142 if not rel:
143 143 # name was actually the same as root (maybe a symlink)
144 144 return ''
145 145 rel.reverse()
146 146 name = os.path.join(*rel)
147 147 auditor(name)
148 148 return util.pconvert(name)
149 149 dirname, basename = util.split(name)
150 150 rel.append(basename)
151 151 if dirname == name:
152 152 break
153 153 name = dirname
154 154
155 155 # A common mistake is to use -R, but specify a file relative to the repo
156 156 # instead of cwd. Detect that case, and provide a hint to the user.
157 157 hint = None
158 158 try:
159 159 if cwd != root:
160 160 canonpath(root, root, myname, auditor)
161 161 hint = (_("consider using '--cwd %s'")
162 162 % os.path.relpath(root, cwd))
163 163 except util.Abort:
164 164 pass
165 165
166 166 raise util.Abort(_("%s not under root '%s'") % (myname, root),
167 167 hint=hint)
168 168
169 169 def normasprefix(path):
170 170 '''normalize the specified path as path prefix
171 171
172 172 Returned value can be used safely for "p.startswith(prefix)",
173 173 "p[len(prefix):]", and so on.
174 174
175 175 For efficiency, this expects "path" argument to be already
176 176 normalized by "os.path.normpath", "os.path.realpath", and so on.
177 177
178 178 See also issue3033 for detail about need of this function.
179 179
180 180 >>> normasprefix('/foo/bar').replace(os.sep, '/')
181 181 '/foo/bar/'
182 182 >>> normasprefix('/').replace(os.sep, '/')
183 183 '/'
184 184 '''
185 185 d, p = os.path.splitdrive(path)
186 186 if len(p) != len(os.sep):
187 187 return path + os.sep
188 188 else:
189 189 return path
190 190
191 def join(*args):
192 '''Join two or more pathname components, inserting '/' as needed.
193
194 Based on the posix os.path.join() implementation.
195
196 >>> join('foo', 'bar')
197 'foo/bar'
198 >>> join('/foo', 'bar')
199 '/foo/bar'
200 >>> join('foo', '/bar')
201 '/bar'
202 >>> join('foo', 'bar/')
203 'foo/bar/'
204 >>> join('foo', 'bar', 'gah')
205 'foo/bar/gah'
206 >>> join('foo')
207 'foo'
208 >>> join('', 'foo')
209 'foo'
210 >>> join('foo/', 'bar')
211 'foo/bar'
212 >>> join('', '', '')
213 ''
214 >>> join ('foo', '', '', 'bar')
215 'foo/bar'
216 '''
217 return posixpath.join(*args)
218
219 def dirname(path):
220 '''returns the directory portion of the given path
221
222 Based on the posix os.path.split() implementation.
223
224 >>> dirname('foo')
225 ''
226 >>> dirname('foo/')
227 'foo'
228 >>> dirname('foo/bar')
229 'foo'
230 >>> dirname('/foo')
231 '/'
232 >>> dirname('/foo/bar')
233 '/foo'
234 >>> dirname('/foo//bar/poo')
235 '/foo//bar'
236 >>> dirname('/foo//bar')
237 '/foo'
238 '''
239 return posixpath.dirname(path)
191 # forward two methods from posixpath that do what we need, but we'd
192 # rather not let our internals know that we're thinking in posix terms
193 # - instead we'll let them be oblivious.
194 join = posixpath.join
195 dirname = posixpath.dirname
General Comments 0
You need to be logged in to leave comments. Login now