##// END OF EJS Templates
py3: use bytes os.sep in doctest of pathutil.py
Yuya Nishihara -
r34255:cd022a11 default
parent child Browse files
Show More
@@ -1,221 +1,221 b''
1 1 from __future__ import absolute_import
2 2
3 3 import errno
4 4 import os
5 5 import posixpath
6 6 import stat
7 7
8 8 from .i18n import _
9 9 from . import (
10 10 encoding,
11 11 error,
12 12 pycompat,
13 13 util,
14 14 )
15 15
16 16 def _lowerclean(s):
17 17 return encoding.hfsignoreclean(s.lower())
18 18
19 19 class pathauditor(object):
20 20 '''ensure that a filesystem path contains no banned components.
21 21 the following properties of a path are checked:
22 22
23 23 - ends with a directory separator
24 24 - under top-level .hg
25 25 - starts at the root of a windows drive
26 26 - contains ".."
27 27
28 28 More check are also done about the file system states:
29 29 - traverses a symlink (e.g. a/symlink_here/b)
30 30 - inside a nested repository (a callback can be used to approve
31 31 some nested repositories, e.g., subrepositories)
32 32
33 33 The file system checks are only done when 'realfs' is set to True (the
34 34 default). They should be disable then we are auditing path for operation on
35 35 stored history.
36 36
37 37 If 'cached' is set to True, audited paths and sub-directories are cached.
38 38 Be careful to not keep the cache of unmanaged directories for long because
39 39 audited paths may be replaced with symlinks.
40 40 '''
41 41
42 42 def __init__(self, root, callback=None, realfs=True, cached=False):
43 43 self.audited = set()
44 44 self.auditeddir = set()
45 45 self.root = root
46 46 self._realfs = realfs
47 47 self._cached = cached
48 48 self.callback = callback
49 49 if os.path.lexists(root) and not util.fscasesensitive(root):
50 50 self.normcase = util.normcase
51 51 else:
52 52 self.normcase = lambda x: x
53 53
54 54 def __call__(self, path, mode=None):
55 55 '''Check the relative path.
56 56 path may contain a pattern (e.g. foodir/**.txt)'''
57 57
58 58 path = util.localpath(path)
59 59 normpath = self.normcase(path)
60 60 if normpath in self.audited:
61 61 return
62 62 # AIX ignores "/" at end of path, others raise EISDIR.
63 63 if util.endswithsep(path):
64 64 raise error.Abort(_("path ends in directory separator: %s") % path)
65 65 parts = util.splitpath(path)
66 66 if (os.path.splitdrive(path)[0]
67 67 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
68 68 or os.pardir in parts):
69 69 raise error.Abort(_("path contains illegal component: %s") % path)
70 70 # Windows shortname aliases
71 71 for p in parts:
72 72 if "~" in p:
73 73 first, last = p.split("~", 1)
74 74 if last.isdigit() and first.upper() in ["HG", "HG8B6C"]:
75 75 raise error.Abort(_("path contains illegal component: %s")
76 76 % path)
77 77 if '.hg' in _lowerclean(path):
78 78 lparts = [_lowerclean(p.lower()) for p in parts]
79 79 for p in '.hg', '.hg.':
80 80 if p in lparts[1:]:
81 81 pos = lparts.index(p)
82 82 base = os.path.join(*parts[:pos])
83 83 raise error.Abort(_("path '%s' is inside nested repo %r")
84 84 % (path, base))
85 85
86 86 normparts = util.splitpath(normpath)
87 87 assert len(parts) == len(normparts)
88 88
89 89 parts.pop()
90 90 normparts.pop()
91 91 prefixes = []
92 92 # It's important that we check the path parts starting from the root.
93 93 # This means we won't accidentally traverse a symlink into some other
94 94 # filesystem (which is potentially expensive to access).
95 95 for i in range(len(parts)):
96 96 prefix = pycompat.ossep.join(parts[:i + 1])
97 97 normprefix = pycompat.ossep.join(normparts[:i + 1])
98 98 if normprefix in self.auditeddir:
99 99 continue
100 100 if self._realfs:
101 101 self._checkfs(prefix, path)
102 102 prefixes.append(normprefix)
103 103
104 104 if self._cached:
105 105 self.audited.add(normpath)
106 106 # only add prefixes to the cache after checking everything: we don't
107 107 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
108 108 self.auditeddir.update(prefixes)
109 109
110 110 def _checkfs(self, prefix, path):
111 111 """raise exception if a file system backed check fails"""
112 112 curpath = os.path.join(self.root, prefix)
113 113 try:
114 114 st = os.lstat(curpath)
115 115 except OSError as err:
116 116 # EINVAL can be raised as invalid path syntax under win32.
117 117 # They must be ignored for patterns can be checked too.
118 118 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
119 119 raise
120 120 else:
121 121 if stat.S_ISLNK(st.st_mode):
122 122 msg = _('path %r traverses symbolic link %r') % (path, prefix)
123 123 raise error.Abort(msg)
124 124 elif (stat.S_ISDIR(st.st_mode) and
125 125 os.path.isdir(os.path.join(curpath, '.hg'))):
126 126 if not self.callback or not self.callback(curpath):
127 127 msg = _("path '%s' is inside nested repo %r")
128 128 raise error.Abort(msg % (path, prefix))
129 129
130 130 def check(self, path):
131 131 try:
132 132 self(path)
133 133 return True
134 134 except (OSError, error.Abort):
135 135 return False
136 136
137 137 def canonpath(root, cwd, myname, auditor=None):
138 138 '''return the canonical path of myname, given cwd and root'''
139 139 if util.endswithsep(root):
140 140 rootsep = root
141 141 else:
142 142 rootsep = root + pycompat.ossep
143 143 name = myname
144 144 if not os.path.isabs(name):
145 145 name = os.path.join(root, cwd, name)
146 146 name = os.path.normpath(name)
147 147 if auditor is None:
148 148 auditor = pathauditor(root)
149 149 if name != rootsep and name.startswith(rootsep):
150 150 name = name[len(rootsep):]
151 151 auditor(name)
152 152 return util.pconvert(name)
153 153 elif name == root:
154 154 return ''
155 155 else:
156 156 # Determine whether `name' is in the hierarchy at or beneath `root',
157 157 # by iterating name=dirname(name) until that causes no change (can't
158 158 # check name == '/', because that doesn't work on windows). The list
159 159 # `rel' holds the reversed list of components making up the relative
160 160 # file name we want.
161 161 rel = []
162 162 while True:
163 163 try:
164 164 s = util.samefile(name, root)
165 165 except OSError:
166 166 s = False
167 167 if s:
168 168 if not rel:
169 169 # name was actually the same as root (maybe a symlink)
170 170 return ''
171 171 rel.reverse()
172 172 name = os.path.join(*rel)
173 173 auditor(name)
174 174 return util.pconvert(name)
175 175 dirname, basename = util.split(name)
176 176 rel.append(basename)
177 177 if dirname == name:
178 178 break
179 179 name = dirname
180 180
181 181 # A common mistake is to use -R, but specify a file relative to the repo
182 182 # instead of cwd. Detect that case, and provide a hint to the user.
183 183 hint = None
184 184 try:
185 185 if cwd != root:
186 186 canonpath(root, root, myname, auditor)
187 187 hint = (_("consider using '--cwd %s'")
188 188 % os.path.relpath(root, cwd))
189 189 except error.Abort:
190 190 pass
191 191
192 192 raise error.Abort(_("%s not under root '%s'") % (myname, root),
193 193 hint=hint)
194 194
195 195 def normasprefix(path):
196 196 '''normalize the specified path as path prefix
197 197
198 198 Returned value can be used safely for "p.startswith(prefix)",
199 199 "p[len(prefix):]", and so on.
200 200
201 201 For efficiency, this expects "path" argument to be already
202 202 normalized by "os.path.normpath", "os.path.realpath", and so on.
203 203
204 204 See also issue3033 for detail about need of this function.
205 205
206 >>> normasprefix(b'/foo/bar').replace(os.sep, b'/')
206 >>> normasprefix(b'/foo/bar').replace(pycompat.ossep, b'/')
207 207 '/foo/bar/'
208 >>> normasprefix(b'/').replace(os.sep, b'/')
208 >>> normasprefix(b'/').replace(pycompat.ossep, b'/')
209 209 '/'
210 210 '''
211 211 d, p = os.path.splitdrive(path)
212 212 if len(p) != len(pycompat.ossep):
213 213 return path + pycompat.ossep
214 214 else:
215 215 return path
216 216
217 217 # forward two methods from posixpath that do what we need, but we'd
218 218 # rather not let our internals know that we're thinking in posix terms
219 219 # - instead we'll let them be oblivious.
220 220 join = posixpath.join
221 221 dirname = posixpath.dirname
@@ -1,81 +1,81 b''
1 1 # this is hack to make sure no escape characters are inserted into the output
2 2
3 3 from __future__ import absolute_import
4 4
5 5 import doctest
6 6 import os
7 7 import re
8 8 import sys
9 9
10 10 ispy3 = (sys.version_info[0] >= 3)
11 11
12 12 if 'TERM' in os.environ:
13 13 del os.environ['TERM']
14 14
15 15 class py3docchecker(doctest.OutputChecker):
16 16 def check_output(self, want, got, optionflags):
17 17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
18 18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
19 19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
20 20 # <exc.name>: <others> -> <name>: <others>
21 21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
22 22 got2, re.MULTILINE)
23 23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
24 24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
25 25 for w, g in [(want, got), (want2, got2)])
26 26
27 27 # TODO: migrate doctests to py3 and enable them on both versions
28 28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
29 29 if not (not ispy3 and py2 or ispy3 and py3):
30 30 return
31 31 __import__(name)
32 32 mod = sys.modules[name]
33 33 if testtarget is not None:
34 34 mod = getattr(mod, testtarget)
35 35
36 36 # minimal copy of doctest.testmod()
37 37 finder = doctest.DocTestFinder()
38 38 checker = None
39 39 if ispy3:
40 40 checker = py3docchecker()
41 41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
42 42 for test in finder.find(mod, name):
43 43 runner.run(test)
44 44 runner.summarize()
45 45
46 46 testmod('mercurial.changegroup')
47 47 testmod('mercurial.changelog')
48 48 testmod('mercurial.color')
49 49 testmod('mercurial.config')
50 50 testmod('mercurial.context')
51 51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE)
52 52 testmod('mercurial.dispatch')
53 53 testmod('mercurial.encoding')
54 54 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
55 55 testmod('mercurial.hg')
56 56 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
57 57 testmod('mercurial.match')
58 58 testmod('mercurial.mdiff')
59 59 testmod('mercurial.minirst')
60 60 testmod('mercurial.patch')
61 testmod('mercurial.pathutil', py3=False) # py3: os.sep
61 testmod('mercurial.pathutil')
62 62 testmod('mercurial.parser')
63 63 testmod('mercurial.pycompat')
64 64 testmod('mercurial.revsetlang')
65 65 testmod('mercurial.smartset')
66 66 testmod('mercurial.store')
67 67 testmod('mercurial.subrepo')
68 68 testmod('mercurial.templatefilters')
69 69 testmod('mercurial.templater')
70 70 testmod('mercurial.ui')
71 71 testmod('mercurial.url')
72 72 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
73 73 testmod('mercurial.util', testtarget='platform')
74 74 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
75 75 testmod('hgext.convert.cvsps')
76 76 testmod('hgext.convert.filemap')
77 77 testmod('hgext.convert.p4')
78 78 testmod('hgext.convert.subversion')
79 79 testmod('hgext.mq')
80 80 # Helper scripts in tests/ that have doctests:
81 81 testmod('drawdag')
General Comments 0
You need to be logged in to leave comments. Login now