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