Mercurial > public > mercurial-scm > hg
comparison mercurial/pathutil.py @ 20033:f962870712da
pathutil: tease out a new library to break an import cycle from canonpath use
author | Augie Fackler <raf@durin42.com> |
---|---|
date | Wed, 06 Nov 2013 18:19:04 -0500 |
parents | |
children | 8dd17b19e722 |
comparison
equal
deleted
inserted
replaced
20032:175c6fd8cacc | 20033:f962870712da |
---|---|
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)) |