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