##// END OF EJS Templates
pathutil: add dirname and join functions...
Durham Goode -
r25281:660b178f default
parent child Browse files
Show More
@@ -1,189 +1,254
1 1 import os, errno, stat
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
191 def join(path, *paths):
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 sep = '/'
218 if not paths:
219 path[:0] + sep #23780: Ensure compatible data type even if p is null.
220 for piece in paths:
221 if piece.startswith(sep):
222 path = piece
223 elif not path or path.endswith(sep):
224 path += piece
225 else:
226 path += sep + piece
227 return path
228
229 def dirname(path):
230 '''returns the directory portion of the given path
231
232 Based on the posix os.path.split() implementation.
233
234 >>> dirname('foo')
235 ''
236 >>> dirname('foo/')
237 'foo'
238 >>> dirname('foo/bar')
239 'foo'
240 >>> dirname('/foo')
241 '/'
242 >>> dirname('/foo/bar')
243 '/foo'
244 >>> dirname('/foo//bar/poo')
245 '/foo//bar'
246 >>> dirname('/foo//bar')
247 '/foo'
248 '''
249 sep = '/'
250 i = path.rfind(sep) + 1
251 dirname = path[:i]
252 if dirname and dirname != sep * len(dirname):
253 dirname = dirname.rstrip(sep)
254 return dirname
General Comments 0
You need to be logged in to leave comments. Login now