##// END OF EJS Templates
pathauditor: change parts verification order to be root first...
Durham Goode -
r28087:0b7ce0b1 default
parent child Browse files
Show More
@@ -1,213 +1,214 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 while parts:
87 prefix = os.sep.join(parts)
88 normprefix = os.sep.join(normparts)
86 # It's important that we check the path parts starting from the root.
87 # This means we won't accidentaly traverse a symlink into some other
88 # filesystem (which is potentially expensive to access).
89 for i in range(len(parts)):
90 prefix = os.sep.join(parts[:i + 1])
91 normprefix = os.sep.join(normparts[:i + 1])
89 92 if normprefix in self.auditeddir:
90 break
93 continue
91 94 if self._realfs:
92 95 self._checkfs(prefix, path)
93 96 prefixes.append(normprefix)
94 parts.pop()
95 normparts.pop()
96 97
97 98 self.audited.add(normpath)
98 99 # only add prefixes to the cache after checking everything: we don't
99 100 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
100 101 self.auditeddir.update(prefixes)
101 102
102 103 def _checkfs(self, prefix, path):
103 104 """raise exception if a file system backed check fails"""
104 105 curpath = os.path.join(self.root, prefix)
105 106 try:
106 107 st = os.lstat(curpath)
107 108 except OSError as err:
108 109 # EINVAL can be raised as invalid path syntax under win32.
109 110 # They must be ignored for patterns can be checked too.
110 111 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
111 112 raise
112 113 else:
113 114 if stat.S_ISLNK(st.st_mode):
114 115 msg = _('path %r traverses symbolic link %r') % (path, prefix)
115 116 raise error.Abort(msg)
116 117 elif (stat.S_ISDIR(st.st_mode) and
117 118 os.path.isdir(os.path.join(curpath, '.hg'))):
118 119 if not self.callback or not self.callback(curpath):
119 120 msg = _("path '%s' is inside nested repo %r")
120 121 raise error.Abort(msg % (path, prefix))
121 122
122 123 def check(self, path):
123 124 try:
124 125 self(path)
125 126 return True
126 127 except (OSError, error.Abort):
127 128 return False
128 129
129 130 def canonpath(root, cwd, myname, auditor=None):
130 131 '''return the canonical path of myname, given cwd and root'''
131 132 if util.endswithsep(root):
132 133 rootsep = root
133 134 else:
134 135 rootsep = root + os.sep
135 136 name = myname
136 137 if not os.path.isabs(name):
137 138 name = os.path.join(root, cwd, name)
138 139 name = os.path.normpath(name)
139 140 if auditor is None:
140 141 auditor = pathauditor(root)
141 142 if name != rootsep and name.startswith(rootsep):
142 143 name = name[len(rootsep):]
143 144 auditor(name)
144 145 return util.pconvert(name)
145 146 elif name == root:
146 147 return ''
147 148 else:
148 149 # Determine whether `name' is in the hierarchy at or beneath `root',
149 150 # by iterating name=dirname(name) until that causes no change (can't
150 151 # check name == '/', because that doesn't work on windows). The list
151 152 # `rel' holds the reversed list of components making up the relative
152 153 # file name we want.
153 154 rel = []
154 155 while True:
155 156 try:
156 157 s = util.samefile(name, root)
157 158 except OSError:
158 159 s = False
159 160 if s:
160 161 if not rel:
161 162 # name was actually the same as root (maybe a symlink)
162 163 return ''
163 164 rel.reverse()
164 165 name = os.path.join(*rel)
165 166 auditor(name)
166 167 return util.pconvert(name)
167 168 dirname, basename = util.split(name)
168 169 rel.append(basename)
169 170 if dirname == name:
170 171 break
171 172 name = dirname
172 173
173 174 # A common mistake is to use -R, but specify a file relative to the repo
174 175 # instead of cwd. Detect that case, and provide a hint to the user.
175 176 hint = None
176 177 try:
177 178 if cwd != root:
178 179 canonpath(root, root, myname, auditor)
179 180 hint = (_("consider using '--cwd %s'")
180 181 % os.path.relpath(root, cwd))
181 182 except error.Abort:
182 183 pass
183 184
184 185 raise error.Abort(_("%s not under root '%s'") % (myname, root),
185 186 hint=hint)
186 187
187 188 def normasprefix(path):
188 189 '''normalize the specified path as path prefix
189 190
190 191 Returned value can be used safely for "p.startswith(prefix)",
191 192 "p[len(prefix):]", and so on.
192 193
193 194 For efficiency, this expects "path" argument to be already
194 195 normalized by "os.path.normpath", "os.path.realpath", and so on.
195 196
196 197 See also issue3033 for detail about need of this function.
197 198
198 199 >>> normasprefix('/foo/bar').replace(os.sep, '/')
199 200 '/foo/bar/'
200 201 >>> normasprefix('/').replace(os.sep, '/')
201 202 '/'
202 203 '''
203 204 d, p = os.path.splitdrive(path)
204 205 if len(p) != len(os.sep):
205 206 return path + os.sep
206 207 else:
207 208 return path
208 209
209 210 # forward two methods from posixpath that do what we need, but we'd
210 211 # rather not let our internals know that we're thinking in posix terms
211 212 # - instead we'll let them be oblivious.
212 213 join = posixpath.join
213 214 dirname = posixpath.dirname
General Comments 0
You need to be logged in to leave comments. Login now