##// END OF EJS Templates
pathutil: tease out a new library to break an import cycle from canonpath use
Augie Fackler -
r20033:f9628707 default
parent child Browse files
Show More
@@ -0,0 +1,144 b''
1 import os, errno, stat
2
3 import util
4 from i18n import _
5
6 class pathauditor(object):
7 '''ensure that a filesystem path contains no banned components.
8 the following properties of a path are checked:
9
10 - ends with a directory separator
11 - under top-level .hg
12 - starts at the root of a windows drive
13 - contains ".."
14 - traverses a symlink (e.g. a/symlink_here/b)
15 - inside a nested repository (a callback can be used to approve
16 some nested repositories, e.g., subrepositories)
17 '''
18
19 def __init__(self, root, callback=None):
20 self.audited = set()
21 self.auditeddir = set()
22 self.root = root
23 self.callback = callback
24 if os.path.lexists(root) and not util.checkcase(root):
25 self.normcase = util.normcase
26 else:
27 self.normcase = lambda x: x
28
29 def __call__(self, path):
30 '''Check the relative path.
31 path may contain a pattern (e.g. foodir/**.txt)'''
32
33 path = util.localpath(path)
34 normpath = self.normcase(path)
35 if normpath in self.audited:
36 return
37 # AIX ignores "/" at end of path, others raise EISDIR.
38 if util.endswithsep(path):
39 raise util.Abort(_("path ends in directory separator: %s") % path)
40 parts = util.splitpath(path)
41 if (os.path.splitdrive(path)[0]
42 or parts[0].lower() in ('.hg', '.hg.', '')
43 or os.pardir in parts):
44 raise util.Abort(_("path contains illegal component: %s") % path)
45 if '.hg' in path.lower():
46 lparts = [p.lower() for p in parts]
47 for p in '.hg', '.hg.':
48 if p in lparts[1:]:
49 pos = lparts.index(p)
50 base = os.path.join(*parts[:pos])
51 raise util.Abort(_("path '%s' is inside nested repo %r")
52 % (path, base))
53
54 normparts = util.splitpath(normpath)
55 assert len(parts) == len(normparts)
56
57 parts.pop()
58 normparts.pop()
59 prefixes = []
60 while parts:
61 prefix = os.sep.join(parts)
62 normprefix = os.sep.join(normparts)
63 if normprefix in self.auditeddir:
64 break
65 curpath = os.path.join(self.root, prefix)
66 try:
67 st = os.lstat(curpath)
68 except OSError, err:
69 # EINVAL can be raised as invalid path syntax under win32.
70 # They must be ignored for patterns can be checked too.
71 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
72 raise
73 else:
74 if stat.S_ISLNK(st.st_mode):
75 raise util.Abort(
76 _('path %r traverses symbolic link %r')
77 % (path, prefix))
78 elif (stat.S_ISDIR(st.st_mode) and
79 os.path.isdir(os.path.join(curpath, '.hg'))):
80 if not self.callback or not self.callback(curpath):
81 raise util.Abort(_("path '%s' is inside nested "
82 "repo %r")
83 % (path, prefix))
84 prefixes.append(normprefix)
85 parts.pop()
86 normparts.pop()
87
88 self.audited.add(normpath)
89 # only add prefixes to the cache after checking everything: we don't
90 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
91 self.auditeddir.update(prefixes)
92
93 def check(self, path):
94 try:
95 self(path)
96 return True
97 except (OSError, util.Abort):
98 return False
99
100 def canonpath(root, cwd, myname, auditor=None):
101 '''return the canonical path of myname, given cwd and root'''
102 if util.endswithsep(root):
103 rootsep = root
104 else:
105 rootsep = root + os.sep
106 name = myname
107 if not os.path.isabs(name):
108 name = os.path.join(root, cwd, name)
109 name = os.path.normpath(name)
110 if auditor is None:
111 auditor = pathauditor(root)
112 if name != rootsep and name.startswith(rootsep):
113 name = name[len(rootsep):]
114 auditor(name)
115 return util.pconvert(name)
116 elif name == root:
117 return ''
118 else:
119 # Determine whether `name' is in the hierarchy at or beneath `root',
120 # by iterating name=dirname(name) until that causes no change (can't
121 # check name == '/', because that doesn't work on windows). The list
122 # `rel' holds the reversed list of components making up the relative
123 # file name we want.
124 rel = []
125 while True:
126 try:
127 s = util.samefile(name, root)
128 except OSError:
129 s = False
130 if s:
131 if not rel:
132 # name was actually the same as root (maybe a symlink)
133 return ''
134 rel.reverse()
135 name = os.path.join(*rel)
136 auditor(name)
137 return util.pconvert(name)
138 dirname, basename = util.split(name)
139 rel.append(basename)
140 if dirname == name:
141 break
142 name = dirname
143
144 raise util.Abort(_("%s not under root '%s'") % (myname, root))
@@ -84,7 +84,7 b" like CVS' $Log$, are not supported. A ke"
84 84
85 85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 from mercurial import scmutil
87 from mercurial import scmutil, pathutil
88 88 from mercurial.hgweb import webcommands
89 89 from mercurial.i18n import _
90 90 import os, re, shutil, tempfile
@@ -673,7 +673,7 b' def reposetup(ui, repo):'
673 673 expansion. '''
674 674 source = repo.dirstate.copied(dest)
675 675 if 'l' in wctx.flags(source):
676 source = scmutil.canonpath(repo.root, cwd,
676 source = pathutil.canonpath(repo.root, cwd,
677 677 os.path.realpath(source))
678 678 return kwt.match(source)
679 679
@@ -12,7 +12,7 b' import os'
12 12 import copy
13 13
14 14 from mercurial import hg, commands, util, cmdutil, scmutil, match as match_, \
15 node, archival, error, merge, discovery
15 node, archival, error, merge, discovery, pathutil
16 16 from mercurial.i18n import _
17 17 from mercurial.node import hex
18 18 from hgext import rebase
@@ -469,7 +469,7 b' def overridecopy(orig, ui, repo, pats, o'
469 469 return orig(ui, repo, pats, opts, rename)
470 470
471 471 def makestandin(relpath):
472 path = scmutil.canonpath(repo.root, repo.getcwd(), relpath)
472 path = pathutil.canonpath(repo.root, repo.getcwd(), relpath)
473 473 return os.path.join(repo.wjoin(lfutil.standin(path)))
474 474
475 475 fullpats = scmutil.expandpats(pats)
@@ -10,7 +10,7 b' from i18n import _'
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 import subrepo, context, repair, graphmod, revset, phases, obsolete
13 import subrepo, context, repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import lock as lockmod
@@ -274,7 +274,7 b' def copy(ui, repo, pats, opts, rename=Fa'
274 274 # relsrc: ossep
275 275 # otarget: ossep
276 276 def copyfile(abssrc, relsrc, otarget, exact):
277 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
277 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
278 278 if '/' in abstarget:
279 279 # We cannot normalize abstarget itself, this would prevent
280 280 # case only renames, like a => A.
@@ -367,7 +367,7 b' def copy(ui, repo, pats, opts, rename=Fa'
367 367 # return: function that takes hgsep and returns ossep
368 368 def targetpathfn(pat, dest, srcs):
369 369 if os.path.isdir(pat):
370 abspfx = scmutil.canonpath(repo.root, cwd, pat)
370 abspfx = pathutil.canonpath(repo.root, cwd, pat)
371 371 abspfx = util.localpath(abspfx)
372 372 if destdirexists:
373 373 striplen = len(os.path.split(abspfx)[0])
@@ -393,7 +393,7 b' def copy(ui, repo, pats, opts, rename=Fa'
393 393 res = lambda p: os.path.join(dest,
394 394 os.path.basename(util.localpath(p)))
395 395 else:
396 abspfx = scmutil.canonpath(repo.root, cwd, pat)
396 abspfx = pathutil.canonpath(repo.root, cwd, pat)
397 397 if len(abspfx) < len(srcs[0][0]):
398 398 # A directory. Either the target path contains the last
399 399 # component of the source path or it does not.
@@ -2065,7 +2065,7 b' def revert(ui, repo, ctx, parents, *pats'
2065 2065 fc = ctx[f]
2066 2066 repo.wwrite(f, fc.data(), fc.flags())
2067 2067
2068 audit_path = scmutil.pathauditor(repo.root)
2068 audit_path = pathutil.pathauditor(repo.root)
2069 2069 for f in remove[0]:
2070 2070 if repo.dirstate[f] == 'a':
2071 2071 repo.dirstate.drop(f)
@@ -8,7 +8,7 b' import errno'
8 8
9 9 from node import nullid
10 10 from i18n import _
11 import scmutil, util, ignore, osutil, parsers, encoding
11 import scmutil, util, ignore, osutil, parsers, encoding, pathutil
12 12 import os, stat, errno, gc
13 13
14 14 propertycache = util.propertycache
@@ -736,7 +736,7 b' class dirstate(object):'
736 736 # unknown == True means we walked the full directory tree above.
737 737 # So if a file is not seen it was either a) not matching matchfn
738 738 # b) ignored, c) missing, or d) under a symlink directory.
739 audit_path = scmutil.pathauditor(self._root)
739 audit_path = pathutil.pathauditor(self._root)
740 740
741 741 for nf in iter(visit):
742 742 # Report ignored items in the dmap as long as they are not
@@ -7,7 +7,7 b''
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, copy
10 from mercurial import match, patch, scmutil, error, ui, util
10 from mercurial import match, patch, error, ui, util, pathutil
11 11 from mercurial.i18n import _
12 12 from mercurial.node import hex, nullid
13 13 from common import ErrorResponse
@@ -196,7 +196,7 b' def showbookmark(repo, tmpl, t1, node=nu'
196 196
197 197 def cleanpath(repo, path):
198 198 path = path.lstrip('/')
199 return scmutil.canonpath(repo.root, '', path)
199 return pathutil.canonpath(repo.root, '', path)
200 200
201 201 def changeidctx (repo, changeid):
202 202 try:
@@ -15,7 +15,7 b' import merge as mergemod'
15 15 import tags as tagsmod
16 16 from lock import release
17 17 import weakref, errno, os, time, inspect
18 import branchmap
18 import branchmap, pathutil
19 19 propertycache = util.propertycache
20 20 filecache = scmutil.filecache
21 21
@@ -166,7 +166,7 b' class localrepository(object):'
166 166 self.root = self.wvfs.base
167 167 self.path = self.wvfs.join(".hg")
168 168 self.origroot = path
169 self.auditor = scmutil.pathauditor(self.root, self._checknested)
169 self.auditor = pathutil.pathauditor(self.root, self._checknested)
170 170 self.vfs = scmutil.vfs(self.path)
171 171 self.opener = self.vfs
172 172 self.baseui = baseui
@@ -6,7 +6,7 b''
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import re
9 import scmutil, util, fileset
9 import util, fileset, pathutil
10 10 from i18n import _
11 11
12 12 def _rematcher(pat):
@@ -317,7 +317,7 b' def _normalize(names, default, root, cwd'
317 317 pats = []
318 318 for kind, name in [_patsplit(p, default) for p in names]:
319 319 if kind in ('glob', 'relpath'):
320 name = scmutil.canonpath(root, cwd, name, auditor)
320 name = pathutil.canonpath(root, cwd, name, auditor)
321 321 elif kind in ('relglob', 'path'):
322 322 name = util.normpath(name)
323 323 elif kind in ('listfile', 'listfile0'):
@@ -8,8 +8,9 b''
8 8 from i18n import _
9 9 from mercurial.node import nullrev
10 10 import util, error, osutil, revset, similar, encoding, phases, parsers
11 import pathutil
11 12 import match as matchmod
12 import os, errno, re, stat, glob
13 import os, errno, re, glob
13 14
14 15 if os.name == 'nt':
15 16 import scmwindows as scmplatform
@@ -108,100 +109,6 b' class casecollisionauditor(object):'
108 109 self._loweredfiles.add(fl)
109 110 self._newfiles.add(f)
110 111
111 class pathauditor(object):
112 '''ensure that a filesystem path contains no banned components.
113 the following properties of a path are checked:
114
115 - ends with a directory separator
116 - under top-level .hg
117 - starts at the root of a windows drive
118 - contains ".."
119 - traverses a symlink (e.g. a/symlink_here/b)
120 - inside a nested repository (a callback can be used to approve
121 some nested repositories, e.g., subrepositories)
122 '''
123
124 def __init__(self, root, callback=None):
125 self.audited = set()
126 self.auditeddir = set()
127 self.root = root
128 self.callback = callback
129 if os.path.lexists(root) and not util.checkcase(root):
130 self.normcase = util.normcase
131 else:
132 self.normcase = lambda x: x
133
134 def __call__(self, path):
135 '''Check the relative path.
136 path may contain a pattern (e.g. foodir/**.txt)'''
137
138 path = util.localpath(path)
139 normpath = self.normcase(path)
140 if normpath in self.audited:
141 return
142 # AIX ignores "/" at end of path, others raise EISDIR.
143 if util.endswithsep(path):
144 raise util.Abort(_("path ends in directory separator: %s") % path)
145 parts = util.splitpath(path)
146 if (os.path.splitdrive(path)[0]
147 or parts[0].lower() in ('.hg', '.hg.', '')
148 or os.pardir in parts):
149 raise util.Abort(_("path contains illegal component: %s") % path)
150 if '.hg' in path.lower():
151 lparts = [p.lower() for p in parts]
152 for p in '.hg', '.hg.':
153 if p in lparts[1:]:
154 pos = lparts.index(p)
155 base = os.path.join(*parts[:pos])
156 raise util.Abort(_("path '%s' is inside nested repo %r")
157 % (path, base))
158
159 normparts = util.splitpath(normpath)
160 assert len(parts) == len(normparts)
161
162 parts.pop()
163 normparts.pop()
164 prefixes = []
165 while parts:
166 prefix = os.sep.join(parts)
167 normprefix = os.sep.join(normparts)
168 if normprefix in self.auditeddir:
169 break
170 curpath = os.path.join(self.root, prefix)
171 try:
172 st = os.lstat(curpath)
173 except OSError, err:
174 # EINVAL can be raised as invalid path syntax under win32.
175 # They must be ignored for patterns can be checked too.
176 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
177 raise
178 else:
179 if stat.S_ISLNK(st.st_mode):
180 raise util.Abort(
181 _('path %r traverses symbolic link %r')
182 % (path, prefix))
183 elif (stat.S_ISDIR(st.st_mode) and
184 os.path.isdir(os.path.join(curpath, '.hg'))):
185 if not self.callback or not self.callback(curpath):
186 raise util.Abort(_("path '%s' is inside nested "
187 "repo %r")
188 % (path, prefix))
189 prefixes.append(normprefix)
190 parts.pop()
191 normparts.pop()
192
193 self.audited.add(normpath)
194 # only add prefixes to the cache after checking everything: we don't
195 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
196 self.auditeddir.update(prefixes)
197
198 def check(self, path):
199 try:
200 self(path)
201 return True
202 except (OSError, util.Abort):
203 return False
204
205 112 class abstractvfs(object):
206 113 """Abstract base class; cannot be instantiated"""
207 114
@@ -310,7 +217,7 b' class vfs(abstractvfs):'
310 217 def _setmustaudit(self, onoff):
311 218 self._audit = onoff
312 219 if onoff:
313 self.audit = pathauditor(self.base)
220 self.audit = pathutil.pathauditor(self.base)
314 221 else:
315 222 self.audit = util.always
316 223
@@ -445,52 +352,6 b' class readonlyvfs(abstractvfs, auditvfs)'
445 352 return self.vfs(path, mode, *args, **kw)
446 353
447 354
448 def canonpath(root, cwd, myname, auditor=None):
449 '''return the canonical path of myname, given cwd and root'''
450 if util.endswithsep(root):
451 rootsep = root
452 else:
453 rootsep = root + os.sep
454 name = myname
455 if not os.path.isabs(name):
456 name = os.path.join(root, cwd, name)
457 name = os.path.normpath(name)
458 if auditor is None:
459 auditor = pathauditor(root)
460 if name != rootsep and name.startswith(rootsep):
461 name = name[len(rootsep):]
462 auditor(name)
463 return util.pconvert(name)
464 elif name == root:
465 return ''
466 else:
467 # Determine whether `name' is in the hierarchy at or beneath `root',
468 # by iterating name=dirname(name) until that causes no change (can't
469 # check name == '/', because that doesn't work on windows). The list
470 # `rel' holds the reversed list of components making up the relative
471 # file name we want.
472 rel = []
473 while True:
474 try:
475 s = util.samefile(name, root)
476 except OSError:
477 s = False
478 if s:
479 if not rel:
480 # name was actually the same as root (maybe a symlink)
481 return ''
482 rel.reverse()
483 name = os.path.join(*rel)
484 auditor(name)
485 return util.pconvert(name)
486 dirname, basename = util.split(name)
487 rel.append(basename)
488 if dirname == name:
489 break
490 name = dirname
491
492 raise util.Abort(_("%s not under root '%s'") % (myname, root))
493
494 355 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
495 356 '''yield every hg repository under path, always recursively.
496 357 The recurse flag will only control recursion into repo working dirs'''
@@ -768,7 +629,7 b' def _interestingfiles(repo, matcher):'
768 629 This is different from dirstate.status because it doesn't care about
769 630 whether files are modified or clean.'''
770 631 added, unknown, deleted, removed = [], [], [], []
771 audit_path = pathauditor(repo.root)
632 audit_path = pathutil.pathauditor(repo.root)
772 633
773 634 ctx = repo[None]
774 635 dirstate = repo.dirstate
@@ -9,7 +9,8 b' import errno, os, re, shutil, posixpath,'
9 9 import xml.dom.minidom
10 10 import stat, subprocess, tarfile
11 11 from i18n import _
12 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
12 import config, util, node, error, cmdutil, bookmarks, match as matchmod
13 import pathutil
13 14 hg = None
14 15 propertycache = util.propertycache
15 16
@@ -332,7 +333,7 b' def subrepo(ctx, path):'
332 333 import hg as h
333 334 hg = h
334 335
335 scmutil.pathauditor(ctx._repo.root)(path)
336 pathutil.pathauditor(ctx._repo.root)(path)
336 337 state = ctx.substate[path]
337 338 if state[2] not in types:
338 339 raise util.Abort(_('unknown subrepo type %s') % state[2])
General Comments 0
You need to be logged in to leave comments. Login now