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