##// END OF EJS Templates
posix: use getutf8char to handle OS X filename percent-escaping...
Matt Mackall -
r26876:b8381832 default
parent child Browse files
Show More
@@ -1,649 +1,626 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 name = tempfile.mktemp(dir=path, prefix='hg-checklink-')
174 174 try:
175 175 fd = tempfile.NamedTemporaryFile(dir=path, prefix='hg-checklink-')
176 176 try:
177 177 os.symlink(os.path.basename(fd.name), name)
178 178 os.unlink(name)
179 179 return True
180 180 finally:
181 181 fd.close()
182 182 except AttributeError:
183 183 return False
184 184 except OSError as inst:
185 185 # sshfs might report failure while successfully creating the link
186 186 if inst[0] == errno.EIO and os.path.exists(name):
187 187 os.unlink(name)
188 188 return False
189 189
190 190 def checkosfilename(path):
191 191 '''Check that the base-relative path is a valid filename on this platform.
192 192 Returns None if the path is ok, or a UI string describing the problem.'''
193 193 pass # on posix platforms, every path is ok
194 194
195 195 def setbinary(fd):
196 196 pass
197 197
198 198 def pconvert(path):
199 199 return path
200 200
201 201 def localpath(path):
202 202 return path
203 203
204 204 def samefile(fpath1, fpath2):
205 205 """Returns whether path1 and path2 refer to the same file. This is only
206 206 guaranteed to work for files, not directories."""
207 207 return os.path.samefile(fpath1, fpath2)
208 208
209 209 def samedevice(fpath1, fpath2):
210 210 """Returns whether fpath1 and fpath2 are on the same device. This is only
211 211 guaranteed to work for files, not directories."""
212 212 st1 = os.lstat(fpath1)
213 213 st2 = os.lstat(fpath2)
214 214 return st1.st_dev == st2.st_dev
215 215
216 216 # os.path.normcase is a no-op, which doesn't help us on non-native filesystems
217 217 def normcase(path):
218 218 return path.lower()
219 219
220 220 # what normcase does to ASCII strings
221 221 normcasespec = encoding.normcasespecs.lower
222 222 # fallback normcase function for non-ASCII strings
223 223 normcasefallback = normcase
224 224
225 225 if sys.platform == 'darwin':
226 226
227 227 def normcase(path):
228 228 '''
229 229 Normalize a filename for OS X-compatible comparison:
230 230 - escape-encode invalid characters
231 231 - decompose to NFD
232 232 - lowercase
233 233 - omit ignored characters [200c-200f, 202a-202e, 206a-206f,feff]
234 234
235 235 >>> normcase('UPPER')
236 236 'upper'
237 237 >>> normcase('Caf\xc3\xa9')
238 238 'cafe\\xcc\\x81'
239 239 >>> normcase('\xc3\x89')
240 240 'e\\xcc\\x81'
241 241 >>> normcase('\xb8\xca\xc3\xca\xbe\xc8.JPG') # issue3918
242 242 '%b8%ca%c3\\xca\\xbe%c8.jpg'
243 243 '''
244 244
245 245 try:
246 246 return encoding.asciilower(path) # exception for non-ASCII
247 247 except UnicodeDecodeError:
248 248 return normcasefallback(path)
249 249
250 250 normcasespec = encoding.normcasespecs.lower
251 251
252 252 def normcasefallback(path):
253 253 try:
254 254 u = path.decode('utf-8')
255 255 except UnicodeDecodeError:
256 256 # OS X percent-encodes any bytes that aren't valid utf-8
257 257 s = ''
258 g = ''
259 l = 0
260 for c in path:
261 o = ord(c)
262 if l and o < 128 or o >= 192:
263 # we want a continuation byte, but didn't get one
264 s += ''.join(["%%%02X" % ord(x) for x in g])
265 g = ''
266 l = 0
267 if l == 0 and o < 128:
268 # ascii
269 s += c
270 elif l == 0 and 194 <= o < 245:
271 # valid leading bytes
272 if o < 224:
273 l = 1
274 elif o < 240:
275 l = 2
276 else:
277 l = 3
278 g = c
279 elif l > 0 and 128 <= o < 192:
280 # valid continuations
281 g += c
282 l -= 1
283 if not l:
284 s += g
285 g = ''
286 else:
287 # invalid
288 s += "%%%02X" % o
258 pos = 0
259 l = len(s)
260 while pos < l:
261 try:
262 c = encoding.getutf8char(path, pos)
263 pos += len(c)
264 except ValueError:
265 c = '%%%%02X' % path[pos]
266 pos += 1
267 s += c
289 268
290 # any remaining partial characters
291 s += ''.join(["%%%02X" % ord(x) for x in g])
292 269 u = s.decode('utf-8')
293 270
294 271 # Decompose then lowercase (HFS+ technote specifies lower)
295 272 enc = unicodedata.normalize('NFD', u).lower().encode('utf-8')
296 273 # drop HFS+ ignored characters
297 274 return encoding.hfsignoreclean(enc)
298 275
299 276 if sys.platform == 'cygwin':
300 277 # workaround for cygwin, in which mount point part of path is
301 278 # treated as case sensitive, even though underlying NTFS is case
302 279 # insensitive.
303 280
304 281 # default mount points
305 282 cygwinmountpoints = sorted([
306 283 "/usr/bin",
307 284 "/usr/lib",
308 285 "/cygdrive",
309 286 ], reverse=True)
310 287
311 288 # use upper-ing as normcase as same as NTFS workaround
312 289 def normcase(path):
313 290 pathlen = len(path)
314 291 if (pathlen == 0) or (path[0] != os.sep):
315 292 # treat as relative
316 293 return encoding.upper(path)
317 294
318 295 # to preserve case of mountpoint part
319 296 for mp in cygwinmountpoints:
320 297 if not path.startswith(mp):
321 298 continue
322 299
323 300 mplen = len(mp)
324 301 if mplen == pathlen: # mount point itself
325 302 return mp
326 303 if path[mplen] == os.sep:
327 304 return mp + encoding.upper(path[mplen:])
328 305
329 306 return encoding.upper(path)
330 307
331 308 normcasespec = encoding.normcasespecs.other
332 309 normcasefallback = normcase
333 310
334 311 # Cygwin translates native ACLs to POSIX permissions,
335 312 # but these translations are not supported by native
336 313 # tools, so the exec bit tends to be set erroneously.
337 314 # Therefore, disable executable bit access on Cygwin.
338 315 def checkexec(path):
339 316 return False
340 317
341 318 # Similarly, Cygwin's symlink emulation is likely to create
342 319 # problems when Mercurial is used from both Cygwin and native
343 320 # Windows, with other native tools, or on shared volumes
344 321 def checklink(path):
345 322 return False
346 323
347 324 _needsshellquote = None
348 325 def shellquote(s):
349 326 if os.sys.platform == 'OpenVMS':
350 327 return '"%s"' % s
351 328 global _needsshellquote
352 329 if _needsshellquote is None:
353 330 _needsshellquote = re.compile(r'[^a-zA-Z0-9._/+-]').search
354 331 if s and not _needsshellquote(s):
355 332 # "s" shouldn't have to be quoted
356 333 return s
357 334 else:
358 335 return "'%s'" % s.replace("'", "'\\''")
359 336
360 337 def quotecommand(cmd):
361 338 return cmd
362 339
363 340 def popen(command, mode='r'):
364 341 return os.popen(command, mode)
365 342
366 343 def testpid(pid):
367 344 '''return False if pid dead, True if running or not sure'''
368 345 if os.sys.platform == 'OpenVMS':
369 346 return True
370 347 try:
371 348 os.kill(pid, 0)
372 349 return True
373 350 except OSError as inst:
374 351 return inst.errno != errno.ESRCH
375 352
376 353 def explainexit(code):
377 354 """return a 2-tuple (desc, code) describing a subprocess status
378 355 (codes from kill are negative - not os.system/wait encoding)"""
379 356 if code >= 0:
380 357 return _("exited with status %d") % code, code
381 358 return _("killed by signal %d") % -code, -code
382 359
383 360 def isowner(st):
384 361 """Return True if the stat object st is from the current user."""
385 362 return st.st_uid == os.getuid()
386 363
387 364 def findexe(command):
388 365 '''Find executable for command searching like which does.
389 366 If command is a basename then PATH is searched for command.
390 367 PATH isn't searched if command is an absolute or relative path.
391 368 If command isn't found None is returned.'''
392 369 if sys.platform == 'OpenVMS':
393 370 return command
394 371
395 372 def findexisting(executable):
396 373 'Will return executable if existing file'
397 374 if os.path.isfile(executable) and os.access(executable, os.X_OK):
398 375 return executable
399 376 return None
400 377
401 378 if os.sep in command:
402 379 return findexisting(command)
403 380
404 381 if sys.platform == 'plan9':
405 382 return findexisting(os.path.join('/bin', command))
406 383
407 384 for path in os.environ.get('PATH', '').split(os.pathsep):
408 385 executable = findexisting(os.path.join(path, command))
409 386 if executable is not None:
410 387 return executable
411 388 return None
412 389
413 390 def setsignalhandler():
414 391 pass
415 392
416 393 _wantedkinds = set([stat.S_IFREG, stat.S_IFLNK])
417 394
418 395 def statfiles(files):
419 396 '''Stat each file in files. Yield each stat, or None if a file does not
420 397 exist or has a type we don't care about.'''
421 398 lstat = os.lstat
422 399 getkind = stat.S_IFMT
423 400 for nf in files:
424 401 try:
425 402 st = lstat(nf)
426 403 if getkind(st.st_mode) not in _wantedkinds:
427 404 st = None
428 405 except OSError as err:
429 406 if err.errno not in (errno.ENOENT, errno.ENOTDIR):
430 407 raise
431 408 st = None
432 409 yield st
433 410
434 411 def getuser():
435 412 '''return name of current user'''
436 413 return getpass.getuser()
437 414
438 415 def username(uid=None):
439 416 """Return the name of the user with the given uid.
440 417
441 418 If uid is None, return the name of the current user."""
442 419
443 420 if uid is None:
444 421 uid = os.getuid()
445 422 try:
446 423 return pwd.getpwuid(uid)[0]
447 424 except KeyError:
448 425 return str(uid)
449 426
450 427 def groupname(gid=None):
451 428 """Return the name of the group with the given gid.
452 429
453 430 If gid is None, return the name of the current group."""
454 431
455 432 if gid is None:
456 433 gid = os.getgid()
457 434 try:
458 435 return grp.getgrgid(gid)[0]
459 436 except KeyError:
460 437 return str(gid)
461 438
462 439 def groupmembers(name):
463 440 """Return the list of members of the group with the given
464 441 name, KeyError if the group does not exist.
465 442 """
466 443 return list(grp.getgrnam(name).gr_mem)
467 444
468 445 def spawndetached(args):
469 446 return os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
470 447 args[0], args)
471 448
472 449 def gethgcmd():
473 450 return sys.argv[:1]
474 451
475 452 def termwidth():
476 453 try:
477 454 import array
478 455 import termios
479 456 for dev in (sys.stderr, sys.stdout, sys.stdin):
480 457 try:
481 458 try:
482 459 fd = dev.fileno()
483 460 except AttributeError:
484 461 continue
485 462 if not os.isatty(fd):
486 463 continue
487 464 try:
488 465 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
489 466 width = array.array('h', arri)[1]
490 467 if width > 0:
491 468 return width
492 469 except AttributeError:
493 470 pass
494 471 except ValueError:
495 472 pass
496 473 except IOError as e:
497 474 if e[0] == errno.EINVAL:
498 475 pass
499 476 else:
500 477 raise
501 478 except ImportError:
502 479 pass
503 480 return 80
504 481
505 482 def makedir(path, notindexed):
506 483 os.mkdir(path)
507 484
508 485 def unlinkpath(f, ignoremissing=False):
509 486 """unlink and remove the directory if it is empty"""
510 487 try:
511 488 os.unlink(f)
512 489 except OSError as e:
513 490 if not (ignoremissing and e.errno == errno.ENOENT):
514 491 raise
515 492 # try removing directories that might now be empty
516 493 try:
517 494 os.removedirs(os.path.dirname(f))
518 495 except OSError:
519 496 pass
520 497
521 498 def lookupreg(key, name=None, scope=None):
522 499 return None
523 500
524 501 def hidewindow():
525 502 """Hide current shell window.
526 503
527 504 Used to hide the window opened when starting asynchronous
528 505 child process under Windows, unneeded on other systems.
529 506 """
530 507 pass
531 508
532 509 class cachestat(object):
533 510 def __init__(self, path):
534 511 self.stat = os.stat(path)
535 512
536 513 def cacheable(self):
537 514 return bool(self.stat.st_ino)
538 515
539 516 __hash__ = object.__hash__
540 517
541 518 def __eq__(self, other):
542 519 try:
543 520 # Only dev, ino, size, mtime and atime are likely to change. Out
544 521 # of these, we shouldn't compare atime but should compare the
545 522 # rest. However, one of the other fields changing indicates
546 523 # something fishy going on, so return False if anything but atime
547 524 # changes.
548 525 return (self.stat.st_mode == other.stat.st_mode and
549 526 self.stat.st_ino == other.stat.st_ino and
550 527 self.stat.st_dev == other.stat.st_dev and
551 528 self.stat.st_nlink == other.stat.st_nlink and
552 529 self.stat.st_uid == other.stat.st_uid and
553 530 self.stat.st_gid == other.stat.st_gid and
554 531 self.stat.st_size == other.stat.st_size and
555 532 self.stat.st_mtime == other.stat.st_mtime and
556 533 self.stat.st_ctime == other.stat.st_ctime)
557 534 except AttributeError:
558 535 return False
559 536
560 537 def __ne__(self, other):
561 538 return not self == other
562 539
563 540 def executablepath():
564 541 return None # available on Windows only
565 542
566 543 class unixdomainserver(socket.socket):
567 544 def __init__(self, join, subsystem):
568 545 '''Create a unix domain socket with the given prefix.'''
569 546 super(unixdomainserver, self).__init__(socket.AF_UNIX)
570 547 sockname = subsystem + '.sock'
571 548 self.realpath = self.path = join(sockname)
572 549 if os.path.islink(self.path):
573 550 if os.path.exists(self.path):
574 551 self.realpath = os.readlink(self.path)
575 552 else:
576 553 os.unlink(self.path)
577 554 try:
578 555 self.bind(self.realpath)
579 556 except socket.error as err:
580 557 if err.args[0] == 'AF_UNIX path too long':
581 558 tmpdir = tempfile.mkdtemp(prefix='hg-%s-' % subsystem)
582 559 self.realpath = os.path.join(tmpdir, sockname)
583 560 try:
584 561 self.bind(self.realpath)
585 562 os.symlink(self.realpath, self.path)
586 563 except (OSError, socket.error):
587 564 self.cleanup()
588 565 raise
589 566 else:
590 567 raise
591 568 self.listen(5)
592 569
593 570 def cleanup(self):
594 571 def okayifmissing(f, path):
595 572 try:
596 573 f(path)
597 574 except OSError as err:
598 575 if err.errno != errno.ENOENT:
599 576 raise
600 577
601 578 okayifmissing(os.unlink, self.path)
602 579 if self.realpath != self.path:
603 580 okayifmissing(os.unlink, self.realpath)
604 581 okayifmissing(os.rmdir, os.path.dirname(self.realpath))
605 582
606 583 def statislink(st):
607 584 '''check whether a stat result is a symlink'''
608 585 return st and stat.S_ISLNK(st.st_mode)
609 586
610 587 def statisexec(st):
611 588 '''check whether a stat result is an executable file'''
612 589 return st and (st.st_mode & 0o100 != 0)
613 590
614 591 def poll(fds):
615 592 """block until something happens on any file descriptor
616 593
617 594 This is a generic helper that will check for any activity
618 595 (read, write. exception) and return the list of touched files.
619 596
620 597 In unsupported cases, it will raise a NotImplementedError"""
621 598 try:
622 599 res = select.select(fds, fds, fds)
623 600 except ValueError: # out of range file descriptor
624 601 raise NotImplementedError()
625 602 return sorted(list(set(sum(res, []))))
626 603
627 604 def readpipe(pipe):
628 605 """Read all available data from a pipe."""
629 606 # We can't fstat() a pipe because Linux will always report 0.
630 607 # So, we set the pipe to non-blocking mode and read everything
631 608 # that's available.
632 609 flags = fcntl.fcntl(pipe, fcntl.F_GETFL)
633 610 flags |= os.O_NONBLOCK
634 611 oldflags = fcntl.fcntl(pipe, fcntl.F_SETFL, flags)
635 612
636 613 try:
637 614 chunks = []
638 615 while True:
639 616 try:
640 617 s = pipe.read()
641 618 if not s:
642 619 break
643 620 chunks.append(s)
644 621 except IOError:
645 622 break
646 623
647 624 return ''.join(chunks)
648 625 finally:
649 626 fcntl.fcntl(pipe, fcntl.F_SETFL, oldflags)
General Comments 0
You need to be logged in to leave comments. Login now