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