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