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