##// END OF EJS Templates
pathauditor: check for codepoints ignored on OS X
Augie Fackler -
r23598:c02a05cc stable
parent child Browse files
Show More
@@ -1,166 +1,170 b''
1 1 import os, errno, stat
2 2
3 import encoding
3 4 import util
4 5 from i18n import _
5 6
7 def _lowerclean(s):
8 return encoding.hfsignoreclean(s.lower())
9
6 10 class pathauditor(object):
7 11 '''ensure that a filesystem path contains no banned components.
8 12 the following properties of a path are checked:
9 13
10 14 - ends with a directory separator
11 15 - under top-level .hg
12 16 - starts at the root of a windows drive
13 17 - contains ".."
14 18 - traverses a symlink (e.g. a/symlink_here/b)
15 19 - inside a nested repository (a callback can be used to approve
16 20 some nested repositories, e.g., subrepositories)
17 21 '''
18 22
19 23 def __init__(self, root, callback=None):
20 24 self.audited = set()
21 25 self.auditeddir = set()
22 26 self.root = root
23 27 self.callback = callback
24 28 if os.path.lexists(root) and not util.checkcase(root):
25 29 self.normcase = util.normcase
26 30 else:
27 31 self.normcase = lambda x: x
28 32
29 33 def __call__(self, path):
30 34 '''Check the relative path.
31 35 path may contain a pattern (e.g. foodir/**.txt)'''
32 36
33 37 path = util.localpath(path)
34 38 normpath = self.normcase(path)
35 39 if normpath in self.audited:
36 40 return
37 41 # AIX ignores "/" at end of path, others raise EISDIR.
38 42 if util.endswithsep(path):
39 43 raise util.Abort(_("path ends in directory separator: %s") % path)
40 44 parts = util.splitpath(path)
41 45 if (os.path.splitdrive(path)[0]
42 or parts[0].lower() in ('.hg', '.hg.', '')
46 or _lowerclean(parts[0]) in ('.hg', '.hg.', '')
43 47 or os.pardir in parts):
44 48 raise util.Abort(_("path contains illegal component: %s") % path)
45 if '.hg' in path.lower():
46 lparts = [p.lower() for p in parts]
49 if '.hg' in _lowerclean(path):
50 lparts = [_lowerclean(p.lower()) for p in parts]
47 51 for p in '.hg', '.hg.':
48 52 if p in lparts[1:]:
49 53 pos = lparts.index(p)
50 54 base = os.path.join(*parts[:pos])
51 55 raise util.Abort(_("path '%s' is inside nested repo %r")
52 56 % (path, base))
53 57
54 58 normparts = util.splitpath(normpath)
55 59 assert len(parts) == len(normparts)
56 60
57 61 parts.pop()
58 62 normparts.pop()
59 63 prefixes = []
60 64 while parts:
61 65 prefix = os.sep.join(parts)
62 66 normprefix = os.sep.join(normparts)
63 67 if normprefix in self.auditeddir:
64 68 break
65 69 curpath = os.path.join(self.root, prefix)
66 70 try:
67 71 st = os.lstat(curpath)
68 72 except OSError, err:
69 73 # EINVAL can be raised as invalid path syntax under win32.
70 74 # They must be ignored for patterns can be checked too.
71 75 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
72 76 raise
73 77 else:
74 78 if stat.S_ISLNK(st.st_mode):
75 79 raise util.Abort(
76 80 _('path %r traverses symbolic link %r')
77 81 % (path, prefix))
78 82 elif (stat.S_ISDIR(st.st_mode) and
79 83 os.path.isdir(os.path.join(curpath, '.hg'))):
80 84 if not self.callback or not self.callback(curpath):
81 85 raise util.Abort(_("path '%s' is inside nested "
82 86 "repo %r")
83 87 % (path, prefix))
84 88 prefixes.append(normprefix)
85 89 parts.pop()
86 90 normparts.pop()
87 91
88 92 self.audited.add(normpath)
89 93 # only add prefixes to the cache after checking everything: we don't
90 94 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
91 95 self.auditeddir.update(prefixes)
92 96
93 97 def check(self, path):
94 98 try:
95 99 self(path)
96 100 return True
97 101 except (OSError, util.Abort):
98 102 return False
99 103
100 104 def canonpath(root, cwd, myname, auditor=None):
101 105 '''return the canonical path of myname, given cwd and root'''
102 106 if util.endswithsep(root):
103 107 rootsep = root
104 108 else:
105 109 rootsep = root + os.sep
106 110 name = myname
107 111 if not os.path.isabs(name):
108 112 name = os.path.join(root, cwd, name)
109 113 name = os.path.normpath(name)
110 114 if auditor is None:
111 115 auditor = pathauditor(root)
112 116 if name != rootsep and name.startswith(rootsep):
113 117 name = name[len(rootsep):]
114 118 auditor(name)
115 119 return util.pconvert(name)
116 120 elif name == root:
117 121 return ''
118 122 else:
119 123 # Determine whether `name' is in the hierarchy at or beneath `root',
120 124 # by iterating name=dirname(name) until that causes no change (can't
121 125 # check name == '/', because that doesn't work on windows). The list
122 126 # `rel' holds the reversed list of components making up the relative
123 127 # file name we want.
124 128 rel = []
125 129 while True:
126 130 try:
127 131 s = util.samefile(name, root)
128 132 except OSError:
129 133 s = False
130 134 if s:
131 135 if not rel:
132 136 # name was actually the same as root (maybe a symlink)
133 137 return ''
134 138 rel.reverse()
135 139 name = os.path.join(*rel)
136 140 auditor(name)
137 141 return util.pconvert(name)
138 142 dirname, basename = util.split(name)
139 143 rel.append(basename)
140 144 if dirname == name:
141 145 break
142 146 name = dirname
143 147
144 148 raise util.Abort(_("%s not under root '%s'") % (myname, root))
145 149
146 150 def normasprefix(path):
147 151 '''normalize the specified path as path prefix
148 152
149 153 Returned vaule can be used safely for "p.startswith(prefix)",
150 154 "p[len(prefix):]", and so on.
151 155
152 156 For efficiency, this expects "path" argument to be already
153 157 normalized by "os.path.normpath", "os.path.realpath", and so on.
154 158
155 159 See also issue3033 for detail about need of this function.
156 160
157 161 >>> normasprefix('/foo/bar').replace(os.sep, '/')
158 162 '/foo/bar/'
159 163 >>> normasprefix('/').replace(os.sep, '/')
160 164 '/'
161 165 '''
162 166 d, p = os.path.splitdrive(path)
163 167 if len(p) != len(os.sep):
164 168 return path + os.sep
165 169 else:
166 170 return path
@@ -1,460 +1,477 b''
1 1 commit date test
2 2
3 3 $ hg init test
4 4 $ cd test
5 5 $ echo foo > foo
6 6 $ hg add foo
7 7 $ cat > $TESTTMP/checkeditform.sh <<EOF
8 8 > env | grep HGEDITFORM
9 9 > true
10 10 > EOF
11 11 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg commit -m ""
12 12 HGEDITFORM=commit.normal.normal
13 13 abort: empty commit message
14 14 [255]
15 15 $ hg commit -d '0 0' -m commit-1
16 16 $ echo foo >> foo
17 17 $ hg commit -d '1 4444444' -m commit-3
18 18 abort: impossible time zone offset: 4444444
19 19 [255]
20 20 $ hg commit -d '1 15.1' -m commit-4
21 21 abort: invalid date: '1\t15.1'
22 22 [255]
23 23 $ hg commit -d 'foo bar' -m commit-5
24 24 abort: invalid date: 'foo bar'
25 25 [255]
26 26 $ hg commit -d ' 1 4444' -m commit-6
27 27 $ hg commit -d '111111111111 0' -m commit-7
28 28 abort: date exceeds 32 bits: 111111111111
29 29 [255]
30 30 $ hg commit -d '-7654321 3600' -m commit-7
31 31 abort: negative date value: -7654321
32 32 [255]
33 33
34 34 commit added file that has been deleted
35 35
36 36 $ echo bar > bar
37 37 $ hg add bar
38 38 $ rm bar
39 39 $ hg commit -m commit-8
40 40 nothing changed (1 missing files, see 'hg status')
41 41 [1]
42 42 $ hg commit -m commit-8-2 bar
43 43 abort: bar: file not found!
44 44 [255]
45 45
46 46 $ hg -q revert -a --no-backup
47 47
48 48 $ mkdir dir
49 49 $ echo boo > dir/file
50 50 $ hg add
51 51 adding dir/file (glob)
52 52 $ hg -v commit -m commit-9 dir
53 53 dir/file
54 54 committed changeset 2:d2a76177cb42
55 55
56 56 $ echo > dir.file
57 57 $ hg add
58 58 adding dir.file
59 59 $ hg commit -m commit-10 dir dir.file
60 60 abort: dir: no match under directory!
61 61 [255]
62 62
63 63 $ echo >> dir/file
64 64 $ mkdir bleh
65 65 $ mkdir dir2
66 66 $ cd bleh
67 67 $ hg commit -m commit-11 .
68 68 abort: bleh: no match under directory!
69 69 [255]
70 70 $ hg commit -m commit-12 ../dir ../dir2
71 71 abort: dir2: no match under directory!
72 72 [255]
73 73 $ hg -v commit -m commit-13 ../dir
74 74 dir/file
75 75 committed changeset 3:1cd62a2d8db5
76 76 $ cd ..
77 77
78 78 $ hg commit -m commit-14 does-not-exist
79 79 abort: does-not-exist: * (glob)
80 80 [255]
81 81
82 82 #if symlink
83 83 $ ln -s foo baz
84 84 $ hg commit -m commit-15 baz
85 85 abort: baz: file not tracked!
86 86 [255]
87 87 #endif
88 88
89 89 $ touch quux
90 90 $ hg commit -m commit-16 quux
91 91 abort: quux: file not tracked!
92 92 [255]
93 93 $ echo >> dir/file
94 94 $ hg -v commit -m commit-17 dir/file
95 95 dir/file
96 96 committed changeset 4:49176991390e
97 97
98 98 An empty date was interpreted as epoch origin
99 99
100 100 $ echo foo >> foo
101 101 $ hg commit -d '' -m commit-no-date
102 102 $ hg tip --template '{date|isodate}\n' | grep '1970'
103 103 [1]
104 104
105 105 Make sure we do not obscure unknown requires file entries (issue2649)
106 106
107 107 $ echo foo >> foo
108 108 $ echo fake >> .hg/requires
109 109 $ hg commit -m bla
110 110 abort: repository requires features unknown to this Mercurial: fake!
111 111 (see http://mercurial.selenic.com/wiki/MissingRequirement for more information)
112 112 [255]
113 113
114 114 $ cd ..
115 115
116 116
117 117 partial subdir commit test
118 118
119 119 $ hg init test2
120 120 $ cd test2
121 121 $ mkdir foo
122 122 $ echo foo > foo/foo
123 123 $ mkdir bar
124 124 $ echo bar > bar/bar
125 125 $ hg add
126 126 adding bar/bar (glob)
127 127 adding foo/foo (glob)
128 128 $ HGEDITOR=cat hg ci -e -m commit-subdir-1 foo
129 129 commit-subdir-1
130 130
131 131
132 132 HG: Enter commit message. Lines beginning with 'HG:' are removed.
133 133 HG: Leave message empty to abort commit.
134 134 HG: --
135 135 HG: user: test
136 136 HG: branch 'default'
137 137 HG: added foo/foo
138 138
139 139
140 140 $ hg ci -m commit-subdir-2 bar
141 141
142 142 subdir log 1
143 143
144 144 $ hg log -v foo
145 145 changeset: 0:f97e73a25882
146 146 user: test
147 147 date: Thu Jan 01 00:00:00 1970 +0000
148 148 files: foo/foo
149 149 description:
150 150 commit-subdir-1
151 151
152 152
153 153
154 154 subdir log 2
155 155
156 156 $ hg log -v bar
157 157 changeset: 1:aa809156d50d
158 158 tag: tip
159 159 user: test
160 160 date: Thu Jan 01 00:00:00 1970 +0000
161 161 files: bar/bar
162 162 description:
163 163 commit-subdir-2
164 164
165 165
166 166
167 167 full log
168 168
169 169 $ hg log -v
170 170 changeset: 1:aa809156d50d
171 171 tag: tip
172 172 user: test
173 173 date: Thu Jan 01 00:00:00 1970 +0000
174 174 files: bar/bar
175 175 description:
176 176 commit-subdir-2
177 177
178 178
179 179 changeset: 0:f97e73a25882
180 180 user: test
181 181 date: Thu Jan 01 00:00:00 1970 +0000
182 182 files: foo/foo
183 183 description:
184 184 commit-subdir-1
185 185
186 186
187 187 $ cd ..
188 188
189 189
190 190 dot and subdir commit test
191 191
192 192 $ hg init test3
193 193 $ echo commit-foo-subdir > commit-log-test
194 194 $ cd test3
195 195 $ mkdir foo
196 196 $ echo foo content > foo/plain-file
197 197 $ hg add foo/plain-file
198 198 $ HGEDITOR=cat hg ci --edit -l ../commit-log-test foo
199 199 commit-foo-subdir
200 200
201 201
202 202 HG: Enter commit message. Lines beginning with 'HG:' are removed.
203 203 HG: Leave message empty to abort commit.
204 204 HG: --
205 205 HG: user: test
206 206 HG: branch 'default'
207 207 HG: added foo/plain-file
208 208
209 209
210 210 $ echo modified foo content > foo/plain-file
211 211 $ hg ci -m commit-foo-dot .
212 212
213 213 full log
214 214
215 215 $ hg log -v
216 216 changeset: 1:95b38e3a5b2e
217 217 tag: tip
218 218 user: test
219 219 date: Thu Jan 01 00:00:00 1970 +0000
220 220 files: foo/plain-file
221 221 description:
222 222 commit-foo-dot
223 223
224 224
225 225 changeset: 0:65d4e9386227
226 226 user: test
227 227 date: Thu Jan 01 00:00:00 1970 +0000
228 228 files: foo/plain-file
229 229 description:
230 230 commit-foo-subdir
231 231
232 232
233 233
234 234 subdir log
235 235
236 236 $ cd foo
237 237 $ hg log .
238 238 changeset: 1:95b38e3a5b2e
239 239 tag: tip
240 240 user: test
241 241 date: Thu Jan 01 00:00:00 1970 +0000
242 242 summary: commit-foo-dot
243 243
244 244 changeset: 0:65d4e9386227
245 245 user: test
246 246 date: Thu Jan 01 00:00:00 1970 +0000
247 247 summary: commit-foo-subdir
248 248
249 249 $ cd ..
250 250 $ cd ..
251 251
252 252 Issue1049: Hg permits partial commit of merge without warning
253 253
254 254 $ hg init issue1049
255 255 $ cd issue1049
256 256 $ echo a > a
257 257 $ hg ci -Ama
258 258 adding a
259 259 $ echo a >> a
260 260 $ hg ci -mb
261 261 $ hg up 0
262 262 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
263 263 $ echo b >> a
264 264 $ hg ci -mc
265 265 created new head
266 266 $ HGMERGE=true hg merge
267 267 merging a
268 268 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
269 269 (branch merge, don't forget to commit)
270 270
271 271 should fail because we are specifying a file name
272 272
273 273 $ hg ci -mmerge a
274 274 abort: cannot partially commit a merge (do not specify files or patterns)
275 275 [255]
276 276
277 277 should fail because we are specifying a pattern
278 278
279 279 $ hg ci -mmerge -I a
280 280 abort: cannot partially commit a merge (do not specify files or patterns)
281 281 [255]
282 282
283 283 should succeed
284 284
285 285 $ HGEDITOR="sh $TESTTMP/checkeditform.sh" hg ci -mmerge --edit
286 286 HGEDITFORM=commit.normal.merge
287 287 $ cd ..
288 288
289 289
290 290 test commit message content
291 291
292 292 $ hg init commitmsg
293 293 $ cd commitmsg
294 294 $ echo changed > changed
295 295 $ echo removed > removed
296 296 $ hg book currentbookmark
297 297 $ hg ci -qAm init
298 298
299 299 $ hg rm removed
300 300 $ echo changed >> changed
301 301 $ echo added > added
302 302 $ hg add added
303 303 $ HGEDITOR=cat hg ci -A
304 304
305 305
306 306 HG: Enter commit message. Lines beginning with 'HG:' are removed.
307 307 HG: Leave message empty to abort commit.
308 308 HG: --
309 309 HG: user: test
310 310 HG: branch 'default'
311 311 HG: bookmark 'currentbookmark'
312 312 HG: added added
313 313 HG: changed changed
314 314 HG: removed removed
315 315 abort: empty commit message
316 316 [255]
317 317
318 318 test saving last-message.txt
319 319
320 320 $ hg init sub
321 321 $ echo a > sub/a
322 322 $ hg -R sub add sub/a
323 323 $ cat > sub/.hg/hgrc <<EOF
324 324 > [hooks]
325 325 > precommit.test-saving-last-message = false
326 326 > EOF
327 327
328 328 $ echo 'sub = sub' > .hgsub
329 329 $ hg add .hgsub
330 330
331 331 $ cat > $TESTTMP/editor.sh <<EOF
332 332 > echo "==== before editing:"
333 333 > cat \$1
334 334 > echo "===="
335 335 > echo "test saving last-message.txt" >> \$1
336 336 > EOF
337 337
338 338 $ rm -f .hg/last-message.txt
339 339 $ HGEDITOR="sh $TESTTMP/editor.sh" hg commit -S -q
340 340 ==== before editing:
341 341
342 342
343 343 HG: Enter commit message. Lines beginning with 'HG:' are removed.
344 344 HG: Leave message empty to abort commit.
345 345 HG: --
346 346 HG: user: test
347 347 HG: branch 'default'
348 348 HG: bookmark 'currentbookmark'
349 349 HG: subrepo sub
350 350 HG: added .hgsub
351 351 HG: added added
352 352 HG: changed .hgsubstate
353 353 HG: changed changed
354 354 HG: removed removed
355 355 ====
356 356 abort: precommit.test-saving-last-message hook exited with status 1 (in subrepo sub)
357 357 [255]
358 358 $ cat .hg/last-message.txt
359 359
360 360
361 361 test saving last-message.txt
362 362
363 363 test that '[committemplate] changeset' definition and commit log
364 364 specific template keywords work well
365 365
366 366 $ cat >> .hg/hgrc <<EOF
367 367 > [committemplate]
368 368 > changeset.commit.normal = HG: this is "commit.normal" template
369 369 > HG: {extramsg}
370 370 > {if(currentbookmark,
371 371 > "HG: bookmark '{currentbookmark}' is activated\n",
372 372 > "HG: no bookmark is activated\n")}{subrepos %
373 373 > "HG: subrepo '{subrepo}' is changed\n"}
374 374 >
375 375 > changeset.commit = HG: this is "commit" template
376 376 > HG: {extramsg}
377 377 > {if(currentbookmark,
378 378 > "HG: bookmark '{currentbookmark}' is activated\n",
379 379 > "HG: no bookmark is activated\n")}{subrepos %
380 380 > "HG: subrepo '{subrepo}' is changed\n"}
381 381 >
382 382 > changeset = HG: this is customized commit template
383 383 > HG: {extramsg}
384 384 > {if(currentbookmark,
385 385 > "HG: bookmark '{currentbookmark}' is activated\n",
386 386 > "HG: no bookmark is activated\n")}{subrepos %
387 387 > "HG: subrepo '{subrepo}' is changed\n"}
388 388 > EOF
389 389
390 390 $ hg init sub2
391 391 $ echo a > sub2/a
392 392 $ hg -R sub2 add sub2/a
393 393 $ echo 'sub2 = sub2' >> .hgsub
394 394
395 395 $ HGEDITOR=cat hg commit -S -q
396 396 HG: this is "commit.normal" template
397 397 HG: Leave message empty to abort commit.
398 398 HG: bookmark 'currentbookmark' is activated
399 399 HG: subrepo 'sub' is changed
400 400 HG: subrepo 'sub2' is changed
401 401 abort: empty commit message
402 402 [255]
403 403
404 404 $ cat >> .hg/hgrc <<EOF
405 405 > [committemplate]
406 406 > changeset.commit.normal =
407 407 > # now, "changeset.commit" should be chosen for "hg commit"
408 408 > EOF
409 409
410 410 $ hg bookmark --inactive currentbookmark
411 411 $ hg forget .hgsub
412 412 $ HGEDITOR=cat hg commit -q
413 413 HG: this is "commit" template
414 414 HG: Leave message empty to abort commit.
415 415 HG: no bookmark is activated
416 416 abort: empty commit message
417 417 [255]
418 418
419 419 $ cat >> .hg/hgrc <<EOF
420 420 > [committemplate]
421 421 > changeset.commit =
422 422 > # now, "changeset" should be chosen for "hg commit"
423 423 > EOF
424 424
425 425 $ HGEDITOR=cat hg commit -q
426 426 HG: this is customized commit template
427 427 HG: Leave message empty to abort commit.
428 428 HG: no bookmark is activated
429 429 abort: empty commit message
430 430 [255]
431 431
432 432 $ cat >> .hg/hgrc <<EOF
433 433 > # disable customizing for subsequent tests
434 434 > [committemplate]
435 435 > changeset =
436 436 > EOF
437 437
438 438 $ cd ..
439 439
440 440
441 441 commit copy
442 442
443 443 $ hg init dir2
444 444 $ cd dir2
445 445 $ echo bleh > bar
446 446 $ hg add bar
447 447 $ hg ci -m 'add bar'
448 448
449 449 $ hg cp bar foo
450 450 $ echo >> bar
451 451 $ hg ci -m 'cp bar foo; change bar'
452 452
453 453 $ hg debugrename foo
454 454 foo renamed from bar:26d3ca0dfd18e44d796b564e38dd173c9668d3a9
455 455 $ hg debugindex bar
456 456 rev offset length ..... linkrev nodeid p1 p2 (re)
457 457 0 0 6 ..... 0 26d3ca0dfd18 000000000000 000000000000 (re)
458 458 1 6 7 ..... 1 d267bddd54f7 26d3ca0dfd18 000000000000 (re)
459 459
460 verify pathauditor blocks evil filepaths
461 $ cat > evil-commit.py <<EOF
462 > from mercurial import ui, hg, context, node
463 > notrc = u".h\u200cg".encode('utf-8') + '/hgrc'
464 > u = ui.ui()
465 > r = hg.repository(u, '.')
466 > def filectxfn(repo, memctx, path):
467 > return context.memfilectx(repo, path, '[hooks]\nupdate = echo owned')
468 > c = context.memctx(r, [r['tip'].node(), node.nullid],
469 > 'evil', [notrc], filectxfn, 0)
470 > r.commitctx(c)
471 > EOF
472 $ $PYTHON evil-commit.py
473 $ hg co --clean tip
474 abort: path contains illegal component: .h\xe2\x80\x8cg/hgrc (esc)
475 [255]
476
460 477 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now