##// 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 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 from mercurial import scmutil
87 from mercurial import scmutil, pathutil
88 from mercurial.hgweb import webcommands
88 from mercurial.hgweb import webcommands
89 from mercurial.i18n import _
89 from mercurial.i18n import _
90 import os, re, shutil, tempfile
90 import os, re, shutil, tempfile
@@ -673,7 +673,7 b' def reposetup(ui, repo):'
673 expansion. '''
673 expansion. '''
674 source = repo.dirstate.copied(dest)
674 source = repo.dirstate.copied(dest)
675 if 'l' in wctx.flags(source):
675 if 'l' in wctx.flags(source):
676 source = scmutil.canonpath(repo.root, cwd,
676 source = pathutil.canonpath(repo.root, cwd,
677 os.path.realpath(source))
677 os.path.realpath(source))
678 return kwt.match(source)
678 return kwt.match(source)
679
679
@@ -12,7 +12,7 b' import os'
12 import copy
12 import copy
13
13
14 from mercurial import hg, commands, util, cmdutil, scmutil, match as match_, \
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 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial.node import hex
17 from mercurial.node import hex
18 from hgext import rebase
18 from hgext import rebase
@@ -469,7 +469,7 b' def overridecopy(orig, ui, repo, pats, o'
469 return orig(ui, repo, pats, opts, rename)
469 return orig(ui, repo, pats, opts, rename)
470
470
471 def makestandin(relpath):
471 def makestandin(relpath):
472 path = scmutil.canonpath(repo.root, repo.getcwd(), relpath)
472 path = pathutil.canonpath(repo.root, repo.getcwd(), relpath)
473 return os.path.join(repo.wjoin(lfutil.standin(path)))
473 return os.path.join(repo.wjoin(lfutil.standin(path)))
474
474
475 fullpats = scmutil.expandpats(pats)
475 fullpats = scmutil.expandpats(pats)
@@ -10,7 +10,7 b' from i18n import _'
10 import os, sys, errno, re, tempfile
10 import os, sys, errno, re, tempfile
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 import match as matchmod
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 import changelog
14 import changelog
15 import bookmarks
15 import bookmarks
16 import lock as lockmod
16 import lock as lockmod
@@ -274,7 +274,7 b' def copy(ui, repo, pats, opts, rename=Fa'
274 # relsrc: ossep
274 # relsrc: ossep
275 # otarget: ossep
275 # otarget: ossep
276 def copyfile(abssrc, relsrc, otarget, exact):
276 def copyfile(abssrc, relsrc, otarget, exact):
277 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
277 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
278 if '/' in abstarget:
278 if '/' in abstarget:
279 # We cannot normalize abstarget itself, this would prevent
279 # We cannot normalize abstarget itself, this would prevent
280 # case only renames, like a => A.
280 # case only renames, like a => A.
@@ -367,7 +367,7 b' def copy(ui, repo, pats, opts, rename=Fa'
367 # return: function that takes hgsep and returns ossep
367 # return: function that takes hgsep and returns ossep
368 def targetpathfn(pat, dest, srcs):
368 def targetpathfn(pat, dest, srcs):
369 if os.path.isdir(pat):
369 if os.path.isdir(pat):
370 abspfx = scmutil.canonpath(repo.root, cwd, pat)
370 abspfx = pathutil.canonpath(repo.root, cwd, pat)
371 abspfx = util.localpath(abspfx)
371 abspfx = util.localpath(abspfx)
372 if destdirexists:
372 if destdirexists:
373 striplen = len(os.path.split(abspfx)[0])
373 striplen = len(os.path.split(abspfx)[0])
@@ -393,7 +393,7 b' def copy(ui, repo, pats, opts, rename=Fa'
393 res = lambda p: os.path.join(dest,
393 res = lambda p: os.path.join(dest,
394 os.path.basename(util.localpath(p)))
394 os.path.basename(util.localpath(p)))
395 else:
395 else:
396 abspfx = scmutil.canonpath(repo.root, cwd, pat)
396 abspfx = pathutil.canonpath(repo.root, cwd, pat)
397 if len(abspfx) < len(srcs[0][0]):
397 if len(abspfx) < len(srcs[0][0]):
398 # A directory. Either the target path contains the last
398 # A directory. Either the target path contains the last
399 # component of the source path or it does not.
399 # component of the source path or it does not.
@@ -2065,7 +2065,7 b' def revert(ui, repo, ctx, parents, *pats'
2065 fc = ctx[f]
2065 fc = ctx[f]
2066 repo.wwrite(f, fc.data(), fc.flags())
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 for f in remove[0]:
2069 for f in remove[0]:
2070 if repo.dirstate[f] == 'a':
2070 if repo.dirstate[f] == 'a':
2071 repo.dirstate.drop(f)
2071 repo.dirstate.drop(f)
@@ -8,7 +8,7 b' import errno'
8
8
9 from node import nullid
9 from node import nullid
10 from i18n import _
10 from i18n import _
11 import scmutil, util, ignore, osutil, parsers, encoding
11 import scmutil, util, ignore, osutil, parsers, encoding, pathutil
12 import os, stat, errno, gc
12 import os, stat, errno, gc
13
13
14 propertycache = util.propertycache
14 propertycache = util.propertycache
@@ -736,7 +736,7 b' class dirstate(object):'
736 # unknown == True means we walked the full directory tree above.
736 # unknown == True means we walked the full directory tree above.
737 # So if a file is not seen it was either a) not matching matchfn
737 # So if a file is not seen it was either a) not matching matchfn
738 # b) ignored, c) missing, or d) under a symlink directory.
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 for nf in iter(visit):
741 for nf in iter(visit):
742 # Report ignored items in the dmap as long as they are not
742 # Report ignored items in the dmap as long as they are not
@@ -7,7 +7,7 b''
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, copy
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 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial.node import hex, nullid
12 from mercurial.node import hex, nullid
13 from common import ErrorResponse
13 from common import ErrorResponse
@@ -196,7 +196,7 b' def showbookmark(repo, tmpl, t1, node=nu'
196
196
197 def cleanpath(repo, path):
197 def cleanpath(repo, path):
198 path = path.lstrip('/')
198 path = path.lstrip('/')
199 return scmutil.canonpath(repo.root, '', path)
199 return pathutil.canonpath(repo.root, '', path)
200
200
201 def changeidctx (repo, changeid):
201 def changeidctx (repo, changeid):
202 try:
202 try:
@@ -15,7 +15,7 b' import merge as mergemod'
15 import tags as tagsmod
15 import tags as tagsmod
16 from lock import release
16 from lock import release
17 import weakref, errno, os, time, inspect
17 import weakref, errno, os, time, inspect
18 import branchmap
18 import branchmap, pathutil
19 propertycache = util.propertycache
19 propertycache = util.propertycache
20 filecache = scmutil.filecache
20 filecache = scmutil.filecache
21
21
@@ -166,7 +166,7 b' class localrepository(object):'
166 self.root = self.wvfs.base
166 self.root = self.wvfs.base
167 self.path = self.wvfs.join(".hg")
167 self.path = self.wvfs.join(".hg")
168 self.origroot = path
168 self.origroot = path
169 self.auditor = scmutil.pathauditor(self.root, self._checknested)
169 self.auditor = pathutil.pathauditor(self.root, self._checknested)
170 self.vfs = scmutil.vfs(self.path)
170 self.vfs = scmutil.vfs(self.path)
171 self.opener = self.vfs
171 self.opener = self.vfs
172 self.baseui = baseui
172 self.baseui = baseui
@@ -6,7 +6,7 b''
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import re
8 import re
9 import scmutil, util, fileset
9 import util, fileset, pathutil
10 from i18n import _
10 from i18n import _
11
11
12 def _rematcher(pat):
12 def _rematcher(pat):
@@ -317,7 +317,7 b' def _normalize(names, default, root, cwd'
317 pats = []
317 pats = []
318 for kind, name in [_patsplit(p, default) for p in names]:
318 for kind, name in [_patsplit(p, default) for p in names]:
319 if kind in ('glob', 'relpath'):
319 if kind in ('glob', 'relpath'):
320 name = scmutil.canonpath(root, cwd, name, auditor)
320 name = pathutil.canonpath(root, cwd, name, auditor)
321 elif kind in ('relglob', 'path'):
321 elif kind in ('relglob', 'path'):
322 name = util.normpath(name)
322 name = util.normpath(name)
323 elif kind in ('listfile', 'listfile0'):
323 elif kind in ('listfile', 'listfile0'):
@@ -8,8 +8,9 b''
8 from i18n import _
8 from i18n import _
9 from mercurial.node import nullrev
9 from mercurial.node import nullrev
10 import util, error, osutil, revset, similar, encoding, phases, parsers
10 import util, error, osutil, revset, similar, encoding, phases, parsers
11 import pathutil
11 import match as matchmod
12 import match as matchmod
12 import os, errno, re, stat, glob
13 import os, errno, re, glob
13
14
14 if os.name == 'nt':
15 if os.name == 'nt':
15 import scmwindows as scmplatform
16 import scmwindows as scmplatform
@@ -108,100 +109,6 b' class casecollisionauditor(object):'
108 self._loweredfiles.add(fl)
109 self._loweredfiles.add(fl)
109 self._newfiles.add(f)
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 class abstractvfs(object):
112 class abstractvfs(object):
206 """Abstract base class; cannot be instantiated"""
113 """Abstract base class; cannot be instantiated"""
207
114
@@ -310,7 +217,7 b' class vfs(abstractvfs):'
310 def _setmustaudit(self, onoff):
217 def _setmustaudit(self, onoff):
311 self._audit = onoff
218 self._audit = onoff
312 if onoff:
219 if onoff:
313 self.audit = pathauditor(self.base)
220 self.audit = pathutil.pathauditor(self.base)
314 else:
221 else:
315 self.audit = util.always
222 self.audit = util.always
316
223
@@ -445,52 +352,6 b' class readonlyvfs(abstractvfs, auditvfs)'
445 return self.vfs(path, mode, *args, **kw)
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 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
355 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
495 '''yield every hg repository under path, always recursively.
356 '''yield every hg repository under path, always recursively.
496 The recurse flag will only control recursion into repo working dirs'''
357 The recurse flag will only control recursion into repo working dirs'''
@@ -768,7 +629,7 b' def _interestingfiles(repo, matcher):'
768 This is different from dirstate.status because it doesn't care about
629 This is different from dirstate.status because it doesn't care about
769 whether files are modified or clean.'''
630 whether files are modified or clean.'''
770 added, unknown, deleted, removed = [], [], [], []
631 added, unknown, deleted, removed = [], [], [], []
771 audit_path = pathauditor(repo.root)
632 audit_path = pathutil.pathauditor(repo.root)
772
633
773 ctx = repo[None]
634 ctx = repo[None]
774 dirstate = repo.dirstate
635 dirstate = repo.dirstate
@@ -9,7 +9,8 b' import errno, os, re, shutil, posixpath,'
9 import xml.dom.minidom
9 import xml.dom.minidom
10 import stat, subprocess, tarfile
10 import stat, subprocess, tarfile
11 from i18n import _
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 hg = None
14 hg = None
14 propertycache = util.propertycache
15 propertycache = util.propertycache
15
16
@@ -332,7 +333,7 b' def subrepo(ctx, path):'
332 import hg as h
333 import hg as h
333 hg = h
334 hg = h
334
335
335 scmutil.pathauditor(ctx._repo.root)(path)
336 pathutil.pathauditor(ctx._repo.root)(path)
336 state = ctx.substate[path]
337 state = ctx.substate[path]
337 if state[2] not in types:
338 if state[2] not in types:
338 raise util.Abort(_('unknown subrepo type %s') % state[2])
339 raise util.Abort(_('unknown subrepo type %s') % state[2])
General Comments 0
You need to be logged in to leave comments. Login now