##// END OF EJS Templates
posix: fix test-permissions regression
Matt Mackall -
r26889:1aa5083c 3.6.1 stable
parent child Browse files
Show More
@@ -1,653 +1,655 b''
1 1 # posix.py - Posix utility function implementations for Mercurial
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import fcntl
12 12 import getpass
13 13 import grp
14 14 import os
15 15 import pwd
16 16 import re
17 17 import select
18 18 import socket
19 19 import stat
20 20 import sys
21 21 import tempfile
22 22 import unicodedata
23 23
24 24 from .i18n import _
25 25 from . import (
26 26 encoding,
27 27 )
28 28
29 29 posixfile = open
30 30 normpath = os.path.normpath
31 31 samestat = os.path.samestat
32 32 oslink = os.link
33 33 unlink = os.unlink
34 34 rename = os.rename
35 35 removedirs = os.removedirs
36 36 expandglobs = False
37 37
38 38 umask = os.umask(0)
39 39 os.umask(umask)
40 40
41 41 def split(p):
42 42 '''Same as posixpath.split, but faster
43 43
44 44 >>> import posixpath
45 45 >>> for f in ['/absolute/path/to/file',
46 46 ... 'relative/path/to/file',
47 47 ... 'file_alone',
48 48 ... 'path/to/directory/',
49 49 ... '/multiple/path//separators',
50 50 ... '/file_at_root',
51 51 ... '///multiple_leading_separators_at_root',
52 52 ... '']:
53 53 ... assert split(f) == posixpath.split(f), f
54 54 '''
55 55 ht = p.rsplit('/', 1)
56 56 if len(ht) == 1:
57 57 return '', p
58 58 nh = ht[0].rstrip('/')
59 59 if nh:
60 60 return nh, ht[1]
61 61 return ht[0] + '/', ht[1]
62 62
63 63 def openhardlinks():
64 64 '''return true if it is safe to hold open file handles to hardlinks'''
65 65 return True
66 66
67 67 def nlinks(name):
68 68 '''return number of hardlinks for the given file'''
69 69 return os.lstat(name).st_nlink
70 70
71 71 def parsepatchoutput(output_line):
72 72 """parses the output produced by patch and returns the filename"""
73 73 pf = output_line[14:]
74 74 if os.sys.platform == 'OpenVMS':
75 75 if pf[0] == '`':
76 76 pf = pf[1:-1] # Remove the quotes
77 77 else:
78 78 if pf.startswith("'") and pf.endswith("'") and " " in pf:
79 79 pf = pf[1:-1] # Remove the quotes
80 80 return pf
81 81
82 82 def sshargs(sshcmd, host, user, port):
83 83 '''Build argument list for ssh'''
84 84 args = user and ("%s@%s" % (user, host)) or host
85 85 return port and ("%s -p %s" % (args, port)) or args
86 86
87 87 def isexec(f):
88 88 """check whether a file is executable"""
89 89 return (os.lstat(f).st_mode & 0o100 != 0)
90 90
91 91 def setflags(f, l, x):
92 92 s = os.lstat(f).st_mode
93 93 if l:
94 94 if not stat.S_ISLNK(s):
95 95 # switch file to link
96 96 fp = open(f)
97 97 data = fp.read()
98 98 fp.close()
99 99 os.unlink(f)
100 100 try:
101 101 os.symlink(data, f)
102 102 except OSError:
103 103 # failed to make a link, rewrite file
104 104 fp = open(f, "w")
105 105 fp.write(data)
106 106 fp.close()
107 107 # no chmod needed at this point
108 108 return
109 109 if stat.S_ISLNK(s):
110 110 # switch link to file
111 111 data = os.readlink(f)
112 112 os.unlink(f)
113 113 fp = open(f, "w")
114 114 fp.write(data)
115 115 fp.close()
116 116 s = 0o666 & ~umask # avoid restatting for chmod
117 117
118 118 sx = s & 0o100
119 119 if x and not sx:
120 120 # Turn on +x for every +r bit when making a file executable
121 121 # and obey umask.
122 122 os.chmod(f, s | (s & 0o444) >> 2 & ~umask)
123 123 elif not x and sx:
124 124 # Turn off all +x bits
125 125 os.chmod(f, s & 0o666)
126 126
127 127 def copymode(src, dst, mode=None):
128 128 '''Copy the file mode from the file at path src to dst.
129 129 If src doesn't exist, we're using mode instead. If mode is None, we're
130 130 using umask.'''
131 131 try:
132 132 st_mode = os.lstat(src).st_mode & 0o777
133 133 except OSError as inst:
134 134 if inst.errno != errno.ENOENT:
135 135 raise
136 136 st_mode = mode
137 137 if st_mode is None:
138 138 st_mode = ~umask
139 139 st_mode &= 0o666
140 140 os.chmod(dst, st_mode)
141 141
142 142 def checkexec(path):
143 143 """
144 144 Check whether the given path is on a filesystem with UNIX-like exec flags
145 145
146 146 Requires a directory (like /foo/.hg)
147 147 """
148 148
149 149 # VFAT on some Linux versions can flip mode but it doesn't persist
150 150 # a FS remount. Frequently we can detect it if files are created
151 151 # with exec bit on.
152 152
153 153 try:
154 154 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
155 155 fh, fn = tempfile.mkstemp(dir=path, prefix='hg-checkexec-')
156 156 try:
157 157 os.close(fh)
158 158 m = os.stat(fn).st_mode & 0o777
159 159 new_file_has_exec = m & EXECFLAGS
160 160 os.chmod(fn, m ^ EXECFLAGS)
161 161 exec_flags_cannot_flip = ((os.stat(fn).st_mode & 0o777) == m)
162 162 finally:
163 163 os.unlink(fn)
164 164 except (IOError, OSError):
165 165 # we don't care, the user probably won't be able to commit anyway
166 166 return False
167 167 return not (new_file_has_exec or exec_flags_cannot_flip)
168 168
169 169 def checklink(path):
170 170 """check whether the given path is on a symlink-capable filesystem"""
171 171 # mktemp is not racy because symlink creation will fail if the
172 172 # file already exists
173 173 while True:
174 174 name = tempfile.mktemp(dir=path, prefix='hg-checklink-')
175 175 try:
176 176 fd = tempfile.NamedTemporaryFile(dir=path, prefix='hg-checklink-')
177 177 try:
178 178 os.symlink(os.path.basename(fd.name), name)
179 179 os.unlink(name)
180 180 return True
181 181 except OSError as inst:
182 182 # link creation might race, try again
183 183 if inst[0] == errno.EEXIST:
184 184 continue
185 # sshfs might report failure while successfully creating the link
186 if inst[0] == errno.EIO and os.path.exists(name):
187 os.unlink(name)
188 return False
185 raise
189 186 finally:
190 187 fd.close()
191 188 except AttributeError:
192 189 return False
190 except OSError as inst:
191 # sshfs might report failure while successfully creating the link
192 if inst[0] == errno.EIO and os.path.exists(name):
193 os.unlink(name)
194 return False
193 195
194 196 def checkosfilename(path):
195 197 '''Check that the base-relative path is a valid filename on this platform.
196 198 Returns None if the path is ok, or a UI string describing the problem.'''
197 199 pass # on posix platforms, every path is ok
198 200
199 201 def setbinary(fd):
200 202 pass
201 203
202 204 def pconvert(path):
203 205 return path
204 206
205 207 def localpath(path):
206 208 return path
207 209
208 210 def samefile(fpath1, fpath2):
209 211 """Returns whether path1 and path2 refer to the same file. This is only
210 212 guaranteed to work for files, not directories."""
211 213 return os.path.samefile(fpath1, fpath2)
212 214
213 215 def samedevice(fpath1, fpath2):
214 216 """Returns whether fpath1 and fpath2 are on the same device. This is only
215 217 guaranteed to work for files, not directories."""
216 218 st1 = os.lstat(fpath1)
217 219 st2 = os.lstat(fpath2)
218 220 return st1.st_dev == st2.st_dev
219 221
220 222 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
221 223 def normcase(path):
222 224 return path.lower()
223 225
224 226 # what normcase does to ASCII strings
225 227 normcasespec = encoding.normcasespecs.lower
226 228 # fallback normcase function for non-ASCII strings
227 229 normcasefallback = normcase
228 230
229 231 if sys.platform == 'darwin':
230 232
231 233 def normcase(path):
232 234 '''
233 235 Normalize a filename for OS X-compatible comparison:
234 236 - escape-encode invalid characters
235 237 - decompose to NFD
236 238 - lowercase
237 239 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
238 240
239 241 >>> normcase('UPPER')
240 242 'upper'
241 243 >>> normcase('Caf\xc3\xa9')
242 244 'cafe\\xcc\\x81'
243 245 >>> normcase('\xc3\x89')
244 246 'e\\xcc\\x81'
245 247 >>> normcase('\xb8\xca\xc3\xca\xbe\xc8.JPG') # issue3918
246 248 '%b8%ca%c3\\xca\\xbe%c8.jpg'
247 249 '''
248 250
249 251 try:
250 252 return encoding.asciilower(path) # exception for non-ASCII
251 253 except UnicodeDecodeError:
252 254 return normcasefallback(path)
253 255
254 256 normcasespec = encoding.normcasespecs.lower
255 257
256 258 def normcasefallback(path):
257 259 try:
258 260 u = path.decode('utf-8')
259 261 except UnicodeDecodeError:
260 262 # OS X percent-encodes any bytes that aren't valid utf-8
261 263 s = ''
262 264 g = ''
263 265 l = 0
264 266 for c in path:
265 267 o = ord(c)
266 268 if l and o < 128 or o >= 192:
267 269 # we want a continuation byte, but didn't get one
268 270 s += ''.join(["%%%02X" % ord(x) for x in g])
269 271 g = ''
270 272 l = 0
271 273 if l == 0 and o < 128:
272 274 # ascii
273 275 s += c
274 276 elif l == 0 and 194 <= o < 245:
275 277 # valid leading bytes
276 278 if o < 224:
277 279 l = 1
278 280 elif o < 240:
279 281 l = 2
280 282 else:
281 283 l = 3
282 284 g = c
283 285 elif l > 0 and 128 <= o < 192:
284 286 # valid continuations
285 287 g += c
286 288 l -= 1
287 289 if not l:
288 290 s += g
289 291 g = ''
290 292 else:
291 293 # invalid
292 294 s += "%%%02X" % o
293 295
294 296 # any remaining partial characters
295 297 s += ''.join(["%%%02X" % ord(x) for x in g])
296 298 u = s.decode('utf-8')
297 299
298 300 # Decompose then lowercase (HFS+ technote specifies lower)
299 301 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
300 302 # drop HFS+ ignored characters
301 303 return encoding.hfsignoreclean(enc)
302 304
303 305 if sys.platform == 'cygwin':
304 306 # workaround for cygwin, in which mount point part of path is
305 307 # treated as case sensitive, even though underlying NTFS is case
306 308 # insensitive.
307 309
308 310 # default mount points
309 311 cygwinmountpoints = sorted([
310 312 "/usr/bin",
311 313 "/usr/lib",
312 314 "/cygdrive",
313 315 ], reverse=True)
314 316
315 317 # use upper-ing as normcase as same as NTFS workaround
316 318 def normcase(path):
317 319 pathlen = len(path)
318 320 if (pathlen == 0) or (path[0] != os.sep):
319 321 # treat as relative
320 322 return encoding.upper(path)
321 323
322 324 # to preserve case of mountpoint part
323 325 for mp in cygwinmountpoints:
324 326 if not path.startswith(mp):
325 327 continue
326 328
327 329 mplen = len(mp)
328 330 if mplen == pathlen: # mount point itself
329 331 return mp
330 332 if path[mplen] == os.sep:
331 333 return mp + encoding.upper(path[mplen:])
332 334
333 335 return encoding.upper(path)
334 336
335 337 normcasespec = encoding.normcasespecs.other
336 338 normcasefallback = normcase
337 339
338 340 # Cygwin translates native ACLs to POSIX permissions,
339 341 # but these translations are not supported by native
340 342 # tools, so the exec bit tends to be set erroneously.
341 343 # Therefore, disable executable bit access on Cygwin.
342 344 def checkexec(path):
343 345 return False
344 346
345 347 # Similarly, Cygwin's symlink emulation is likely to create
346 348 # problems when Mercurial is used from both Cygwin and native
347 349 # Windows, with other native tools, or on shared volumes
348 350 def checklink(path):
349 351 return False
350 352
351 353 _needsshellquote = None
352 354 def shellquote(s):
353 355 if os.sys.platform == 'OpenVMS':
354 356 return '"%s"' % s
355 357 global _needsshellquote
356 358 if _needsshellquote is None:
357 359 _needsshellquote = re.compile(r'[^a-zA-Z0-9._/+-]').search
358 360 if s and not _needsshellquote(s):
359 361 # "s" shouldn't have to be quoted
360 362 return s
361 363 else:
362 364 return "'%s'" % s.replace("'", "'\\''")
363 365
364 366 def quotecommand(cmd):
365 367 return cmd
366 368
367 369 def popen(command, mode='r'):
368 370 return os.popen(command, mode)
369 371
370 372 def testpid(pid):
371 373 '''return False if pid dead, True if running or not sure'''
372 374 if os.sys.platform == 'OpenVMS':
373 375 return True
374 376 try:
375 377 os.kill(pid, 0)
376 378 return True
377 379 except OSError as inst:
378 380 return inst.errno != errno.ESRCH
379 381
380 382 def explainexit(code):
381 383 """return a 2-tuple (desc, code) describing a subprocess status
382 384 (codes from kill are negative - not os.system/wait encoding)"""
383 385 if code >= 0:
384 386 return _("exited with status %d") % code, code
385 387 return _("killed by signal %d") % -code, -code
386 388
387 389 def isowner(st):
388 390 """Return True if the stat object st is from the current user."""
389 391 return st.st_uid == os.getuid()
390 392
391 393 def findexe(command):
392 394 '''Find executable for command searching like which does.
393 395 If command is a basename then PATH is searched for command.
394 396 PATH isn't searched if command is an absolute or relative path.
395 397 If command isn't found None is returned.'''
396 398 if sys.platform == 'OpenVMS':
397 399 return command
398 400
399 401 def findexisting(executable):
400 402 'Will return executable if existing file'
401 403 if os.path.isfile(executable) and os.access(executable, os.X_OK):
402 404 return executable
403 405 return None
404 406
405 407 if os.sep in command:
406 408 return findexisting(command)
407 409
408 410 if sys.platform == 'plan9':
409 411 return findexisting(os.path.join('/bin', command))
410 412
411 413 for path in os.environ.get('PATH', '').split(os.pathsep):
412 414 executable = findexisting(os.path.join(path, command))
413 415 if executable is not None:
414 416 return executable
415 417 return None
416 418
417 419 def setsignalhandler():
418 420 pass
419 421
420 422 _wantedkinds = set([stat.S_IFREG, stat.S_IFLNK])
421 423
422 424 def statfiles(files):
423 425 '''Stat each file in files. Yield each stat, or None if a file does not
424 426 exist or has a type we don't care about.'''
425 427 lstat = os.lstat
426 428 getkind = stat.S_IFMT
427 429 for nf in files:
428 430 try:
429 431 st = lstat(nf)
430 432 if getkind(st.st_mode) not in _wantedkinds:
431 433 st = None
432 434 except OSError as err:
433 435 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
434 436 raise
435 437 st = None
436 438 yield st
437 439
438 440 def getuser():
439 441 '''return name of current user'''
440 442 return getpass.getuser()
441 443
442 444 def username(uid=None):
443 445 """Return the name of the user with the given uid.
444 446
445 447 If uid is None, return the name of the current user."""
446 448
447 449 if uid is None:
448 450 uid = os.getuid()
449 451 try:
450 452 return pwd.getpwuid(uid)[0]
451 453 except KeyError:
452 454 return str(uid)
453 455
454 456 def groupname(gid=None):
455 457 """Return the name of the group with the given gid.
456 458
457 459 If gid is None, return the name of the current group."""
458 460
459 461 if gid is None:
460 462 gid = os.getgid()
461 463 try:
462 464 return grp.getgrgid(gid)[0]
463 465 except KeyError:
464 466 return str(gid)
465 467
466 468 def groupmembers(name):
467 469 """Return the list of members of the group with the given
468 470 name, KeyError if the group does not exist.
469 471 """
470 472 return list(grp.getgrnam(name).gr_mem)
471 473
472 474 def spawndetached(args):
473 475 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
474 476 args[0], args)
475 477
476 478 def gethgcmd():
477 479 return sys.argv[:1]
478 480
479 481 def termwidth():
480 482 try:
481 483 import array
482 484 import termios
483 485 for dev in (sys.stderr, sys.stdout, sys.stdin):
484 486 try:
485 487 try:
486 488 fd = dev.fileno()
487 489 except AttributeError:
488 490 continue
489 491 if not os.isatty(fd):
490 492 continue
491 493 try:
492 494 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
493 495 width = array.array('h', arri)[1]
494 496 if width > 0:
495 497 return width
496 498 except AttributeError:
497 499 pass
498 500 except ValueError:
499 501 pass
500 502 except IOError as e:
501 503 if e[0] == errno.EINVAL:
502 504 pass
503 505 else:
504 506 raise
505 507 except ImportError:
506 508 pass
507 509 return 80
508 510
509 511 def makedir(path, notindexed):
510 512 os.mkdir(path)
511 513
512 514 def unlinkpath(f, ignoremissing=False):
513 515 """unlink and remove the directory if it is empty"""
514 516 try:
515 517 os.unlink(f)
516 518 except OSError as e:
517 519 if not (ignoremissing and e.errno == errno.ENOENT):
518 520 raise
519 521 # try removing directories that might now be empty
520 522 try:
521 523 os.removedirs(os.path.dirname(f))
522 524 except OSError:
523 525 pass
524 526
525 527 def lookupreg(key, name=None, scope=None):
526 528 return None
527 529
528 530 def hidewindow():
529 531 """Hide current shell window.
530 532
531 533 Used to hide the window opened when starting asynchronous
532 534 child process under Windows, unneeded on other systems.
533 535 """
534 536 pass
535 537
536 538 class cachestat(object):
537 539 def __init__(self, path):
538 540 self.stat = os.stat(path)
539 541
540 542 def cacheable(self):
541 543 return bool(self.stat.st_ino)
542 544
543 545 __hash__ = object.__hash__
544 546
545 547 def __eq__(self, other):
546 548 try:
547 549 # Only dev, ino, size, mtime and atime are likely to change. Out
548 550 # of these, we shouldn't compare atime but should compare the
549 551 # rest. However, one of the other fields changing indicates
550 552 # something fishy going on, so return False if anything but atime
551 553 # changes.
552 554 return (self.stat.st_mode == other.stat.st_mode and
553 555 self.stat.st_ino == other.stat.st_ino and
554 556 self.stat.st_dev == other.stat.st_dev and
555 557 self.stat.st_nlink == other.stat.st_nlink and
556 558 self.stat.st_uid == other.stat.st_uid and
557 559 self.stat.st_gid == other.stat.st_gid and
558 560 self.stat.st_size == other.stat.st_size and
559 561 self.stat.st_mtime == other.stat.st_mtime and
560 562 self.stat.st_ctime == other.stat.st_ctime)
561 563 except AttributeError:
562 564 return False
563 565
564 566 def __ne__(self, other):
565 567 return not self == other
566 568
567 569 def executablepath():
568 570 return None # available on Windows only
569 571
570 572 class unixdomainserver(socket.socket):
571 573 def __init__(self, join, subsystem):
572 574 '''Create a unix domain socket with the given prefix.'''
573 575 super(unixdomainserver, self).__init__(socket.AF_UNIX)
574 576 sockname = subsystem + '.sock'
575 577 self.realpath = self.path = join(sockname)
576 578 if os.path.islink(self.path):
577 579 if os.path.exists(self.path):
578 580 self.realpath = os.readlink(self.path)
579 581 else:
580 582 os.unlink(self.path)
581 583 try:
582 584 self.bind(self.realpath)
583 585 except socket.error as err:
584 586 if err.args[0] == 'AF_UNIX path too long':
585 587 tmpdir = tempfile.mkdtemp(prefix='hg-%s-' % subsystem)
586 588 self.realpath = os.path.join(tmpdir, sockname)
587 589 try:
588 590 self.bind(self.realpath)
589 591 os.symlink(self.realpath, self.path)
590 592 except (OSError, socket.error):
591 593 self.cleanup()
592 594 raise
593 595 else:
594 596 raise
595 597 self.listen(5)
596 598
597 599 def cleanup(self):
598 600 def okayifmissing(f, path):
599 601 try:
600 602 f(path)
601 603 except OSError as err:
602 604 if err.errno != errno.ENOENT:
603 605 raise
604 606
605 607 okayifmissing(os.unlink, self.path)
606 608 if self.realpath != self.path:
607 609 okayifmissing(os.unlink, self.realpath)
608 610 okayifmissing(os.rmdir, os.path.dirname(self.realpath))
609 611
610 612 def statislink(st):
611 613 '''check whether a stat result is a symlink'''
612 614 return st and stat.S_ISLNK(st.st_mode)
613 615
614 616 def statisexec(st):
615 617 '''check whether a stat result is an executable file'''
616 618 return st and (st.st_mode & 0o100 != 0)
617 619
618 620 def poll(fds):
619 621 """block until something happens on any file descriptor
620 622
621 623 This is a generic helper that will check for any activity
622 624 (read, write. exception) and return the list of touched files.
623 625
624 626 In unsupported cases, it will raise a NotImplementedError"""
625 627 try:
626 628 res = select.select(fds, fds, fds)
627 629 except ValueError: # out of range file descriptor
628 630 raise NotImplementedError()
629 631 return sorted(list(set(sum(res, []))))
630 632
631 633 def readpipe(pipe):
632 634 """Read all available data from a pipe."""
633 635 # We can't fstat() a pipe because Linux will always report 0.
634 636 # So, we set the pipe to non-blocking mode and read everything
635 637 # that's available.
636 638 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
637 639 flags |= os.O_NONBLOCK
638 640 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
639 641
640 642 try:
641 643 chunks = []
642 644 while True:
643 645 try:
644 646 s = pipe.read()
645 647 if not s:
646 648 break
647 649 chunks.append(s)
648 650 except IOError:
649 651 break
650 652
651 653 return ''.join(chunks)
652 654 finally:
653 655 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
General Comments 0
You need to be logged in to leave comments. Login now