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