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