]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
metadata: fix symlink stat() vs. readlink() race
[bup.git] / lib / bup / metadata.py
1 """Metadata read/write support for bup."""
2
3 # Copyright (C) 2010 Rob Browning
4 #
5 # This code is covered under the terms of the GNU Library General
6 # Public License as described in the bup LICENSE file.
7
8 from __future__ import absolute_import, print_function
9 from binascii import hexlify
10 from copy import deepcopy
11 from errno import EACCES, EINVAL, ENOTTY, ENOSYS, EOPNOTSUPP
12 from io import BytesIO
13 from time import gmtime, strftime
14 import errno, os, sys, stat, time, pwd, grp, socket, struct
15
16 from bup import compat, vint, xstat
17 from bup.compat import py_maj
18 from bup.drecurse import recursive_dirlist
19 from bup.helpers import add_error, mkdirp, log, is_superuser, format_filesize
20 from bup.io import path_msg
21 from bup.pwdgrp import pwd_from_uid, pwd_from_name, grp_from_gid, grp_from_name
22 from bup.xstat import utime, lutime
23
24 xattr = None
25 if sys.platform.startswith('linux'):
26     # prefer python-pyxattr (it's a lot faster), but fall back to python-xattr
27     # as the two are incompatible and only one can be installed on a system
28     try:
29         import xattr
30     except ImportError:
31         log('Warning: Linux xattr support missing; install python-pyxattr.\n')
32     if xattr and getattr(xattr, 'get_all', None) is None:
33         try:
34             from xattr import pyxattr_compat as xattr
35             if not isinstance(xattr.NS_USER, bytes):
36                 xattr = None
37         except ImportError:
38             xattr = None
39         if xattr is None:
40             log('Warning: python-xattr module is too old; '
41                 'upgrade or install python-pyxattr instead.\n')
42
43 try:
44     from bup._helpers import read_acl, apply_acl
45 except ImportError:
46     read_acl = apply_acl = None
47
48 try:
49     from bup._helpers import get_linux_file_attr, set_linux_file_attr
50 except ImportError:
51     # No need for a warning here; the only reason they won't exist is that we're
52     # not on Linux, in which case files don't have any linux attrs anyway, so
53     # lacking the functions isn't a problem.
54     get_linux_file_attr = set_linux_file_attr = None
55
56
57 # See the bup_get_linux_file_attr() comments.
58 _suppress_linux_file_attr = \
59     sys.byteorder == 'big' and struct.calcsize('@l') > struct.calcsize('@i')
60
61 def check_linux_file_attr_api():
62     global get_linux_file_attr, set_linux_file_attr
63     if not (get_linux_file_attr or set_linux_file_attr):
64         return
65     if _suppress_linux_file_attr:
66         log('Warning: Linux attr support disabled (see "bup help index").\n')
67         get_linux_file_attr = set_linux_file_attr = None
68
69
70 # WARNING: the metadata encoding is *not* stable yet.  Caveat emptor!
71
72 # Q: Consider hardlink support?
73 # Q: Is it OK to store raw linux attr (chattr) flags?
74 # Q: Can anything other than S_ISREG(x) or S_ISDIR(x) support posix1e ACLs?
75 # Q: Is the application of posix1e has_extended() correct?
76 # Q: Is one global --numeric-ids argument sufficient?
77 # Q: Do nfsv4 acls trump posix1e acls? (seems likely)
78 # Q: Add support for crtime -- ntfs, and (only internally?) ext*?
79
80 # FIXME: Fix relative/abs path detection/stripping wrt other platforms.
81 # FIXME: Add nfsv4 acl handling - see nfs4-acl-tools.
82 # FIXME: Consider other entries mentioned in stat(2) (S_IFDOOR, etc.).
83 # FIXME: Consider pack('vvvvsss', ...) optimization.
84
85 ## FS notes:
86 #
87 # osx (varies between hfs and hfs+):
88 #   type - regular dir char block fifo socket ...
89 #   perms - rwxrwxrwxsgt
90 #   times - ctime atime mtime
91 #   uid
92 #   gid
93 #   hard-link-info (hfs+ only)
94 #   link-target
95 #   device-major/minor
96 #   attributes-osx see chflags
97 #   content-type
98 #   content-creator
99 #   forks
100 #
101 # ntfs
102 #   type - regular dir ...
103 #   times - creation, modification, posix change, access
104 #   hard-link-info
105 #   link-target
106 #   attributes - see attrib
107 #   ACLs
108 #   forks (alternate data streams)
109 #   crtime?
110 #
111 # fat
112 #   type - regular dir ...
113 #   perms - rwxrwxrwx (maybe - see wikipedia)
114 #   times - creation, modification, access
115 #   attributes - see attrib
116
117 verbose = 0
118
119 _have_lchmod = hasattr(os, 'lchmod')
120
121
122 def _clean_up_path_for_archive(p):
123     # Not the most efficient approach.
124     result = p
125
126     # Take everything after any '/../'.
127     pos = result.rfind(b'/../')
128     if pos != -1:
129         result = result[result.rfind(b'/../') + 4:]
130
131     # Take everything after any remaining '../'.
132     if result.startswith(b"../"):
133         result = result[3:]
134
135     # Remove any '/./' sequences.
136     pos = result.find(b'/./')
137     while pos != -1:
138         result = result[0:pos] + b'/' + result[pos + 3:]
139         pos = result.find(b'/./')
140
141     # Remove any leading '/'s.
142     result = result.lstrip(b'/')
143
144     # Replace '//' with '/' everywhere.
145     pos = result.find(b'//')
146     while pos != -1:
147         result = result[0:pos] + b'/' + result[pos + 2:]
148         pos = result.find(b'//')
149
150     # Take everything after any remaining './'.
151     if result.startswith(b'./'):
152         result = result[2:]
153
154     # Take everything before any remaining '/.'.
155     if result.endswith(b'/.'):
156         result = result[:-2]
157
158     if result == b'' or result.endswith(b'/..'):
159         result = b'.'
160
161     return result
162
163
164 def _risky_path(p):
165     if p.startswith(b'/'):
166         return True
167     if p.find(b'/../') != -1:
168         return True
169     if p.startswith(b'../'):
170         return True
171     if p.endswith(b'/..'):
172         return True
173     return False
174
175
176 def _clean_up_extract_path(p):
177     result = p.lstrip(b'/')
178     if result == b'':
179         return b'.'
180     elif _risky_path(result):
181         return None
182     else:
183         return result
184
185
186 # These tags are currently conceptually private to Metadata, and they
187 # must be unique, and must *never* be changed.
188 _rec_tag_end = 0
189 _rec_tag_path = 1
190 _rec_tag_common_v1 = 2 # times, user, group, type, perms, etc. (legacy/broken)
191 _rec_tag_symlink_target = 3
192 _rec_tag_posix1e_acl = 4      # getfacl(1), setfacl(1), etc.
193 _rec_tag_nfsv4_acl = 5        # intended to supplant posix1e? (unimplemented)
194 _rec_tag_linux_attr = 6       # lsattr(1) chattr(1)
195 _rec_tag_linux_xattr = 7      # getfattr(1) setfattr(1)
196 _rec_tag_hardlink_target = 8 # hard link target path
197 _rec_tag_common_v2 = 9 # times, user, group, type, perms, etc. (current)
198 _rec_tag_common_v3 = 10  # adds optional size to v2
199
200 _warned_about_attr_einval = None
201
202
203 class ApplyError(Exception):
204     # Thrown when unable to apply any given bit of metadata to a path.
205     pass
206
207
208 class Metadata:
209     # Metadata is stored as a sequence of tagged binary records.  Each
210     # record will have some subset of add, encode, load, create, and
211     # apply methods, i.e. _add_foo...
212
213     # We do allow an "empty" object as a special case, i.e. no
214     # records.  One can be created by trying to write Metadata(), and
215     # for such an object, read() will return None.  This is used by
216     # "bup save", for example, as a placeholder in cases where
217     # from_path() fails.
218
219     # NOTE: if any relevant fields are added or removed, be sure to
220     # update same_file() below.
221
222     ## Common records
223
224     # Timestamps are (sec, ns), relative to 1970-01-01 00:00:00, ns
225     # must be non-negative and < 10**9.
226
227     def _add_common(self, path, st):
228         assert(st.st_uid >= 0)
229         assert(st.st_gid >= 0)
230         self.size = st.st_size
231         self.uid = st.st_uid
232         self.gid = st.st_gid
233         self.atime = st.st_atime
234         self.mtime = st.st_mtime
235         self.ctime = st.st_ctime
236         self.user = self.group = b''
237         entry = pwd_from_uid(st.st_uid)
238         if entry:
239             self.user = entry.pw_name
240         entry = grp_from_gid(st.st_gid)
241         if entry:
242             self.group = entry.gr_name
243         self.mode = st.st_mode
244         # Only collect st_rdev if we might need it for a mknod()
245         # during restore.  On some platforms (i.e. kFreeBSD), it isn't
246         # stable for other file types.  For example "cp -a" will
247         # change it for a plain file.
248         if stat.S_ISCHR(st.st_mode) or stat.S_ISBLK(st.st_mode):
249             self.rdev = st.st_rdev
250         else:
251             self.rdev = 0
252
253     def _same_common(self, other):
254         """Return true or false to indicate similarity in the hardlink sense."""
255         return self.uid == other.uid \
256             and self.gid == other.gid \
257             and self.rdev == other.rdev \
258             and self.mtime == other.mtime \
259             and self.ctime == other.ctime \
260             and self.user == other.user \
261             and self.group == other.group \
262             and self.size == other.size
263
264     def _encode_common(self):
265         if not self.mode:
266             return None
267         atime = xstat.nsecs_to_timespec(self.atime)
268         mtime = xstat.nsecs_to_timespec(self.mtime)
269         ctime = xstat.nsecs_to_timespec(self.ctime)
270         result = vint.pack('vvsvsvvVvVvVv',
271                            self.mode,
272                            self.uid,
273                            self.user,
274                            self.gid,
275                            self.group,
276                            self.rdev,
277                            atime[0],
278                            atime[1],
279                            mtime[0],
280                            mtime[1],
281                            ctime[0],
282                            ctime[1],
283                            self.size if self.size is not None else -1)
284         return result
285
286     def _load_common_rec(self, port, version=3):
287         if version == 3:
288             # Added trailing size to v2, negative when None.
289             unpack_fmt = 'vvsvsvvVvVvVv'
290         elif version == 2:
291             unpack_fmt = 'vvsvsvvVvVvV'
292         elif version == 1:
293             unpack_fmt = 'VVsVsVvVvVvV'
294         else:
295             raise Exception('unexpected common_rec version %d' % version)
296         data = vint.read_bvec(port)
297         values = vint.unpack(unpack_fmt, data)
298         if version == 3:
299             (self.mode, self.uid, self.user, self.gid, self.group,
300              self.rdev,
301              self.atime, atime_ns,
302              self.mtime, mtime_ns,
303              self.ctime, ctime_ns, size) = values
304             if size >= 0:
305                 self.size = size
306         else:
307             (self.mode, self.uid, self.user, self.gid, self.group,
308              self.rdev,
309              self.atime, atime_ns,
310              self.mtime, mtime_ns,
311              self.ctime, ctime_ns) = values
312         self.atime = xstat.timespec_to_nsecs((self.atime, atime_ns))
313         self.mtime = xstat.timespec_to_nsecs((self.mtime, mtime_ns))
314         self.ctime = xstat.timespec_to_nsecs((self.ctime, ctime_ns))
315
316     def _recognized_file_type(self):
317         return stat.S_ISREG(self.mode) \
318             or stat.S_ISDIR(self.mode) \
319             or stat.S_ISCHR(self.mode) \
320             or stat.S_ISBLK(self.mode) \
321             or stat.S_ISFIFO(self.mode) \
322             or stat.S_ISSOCK(self.mode) \
323             or stat.S_ISLNK(self.mode)
324
325     def _create_via_common_rec(self, path, create_symlinks=True):
326         if not self.mode:
327             raise ApplyError('no metadata - cannot create path '
328                              + path_msg(path))
329
330         # If the path already exists and is a dir, try rmdir.
331         # If the path already exists and is anything else, try unlink.
332         st = None
333         try:
334             st = xstat.lstat(path)
335         except OSError as e:
336             if e.errno != errno.ENOENT:
337                 raise
338         if st:
339             if stat.S_ISDIR(st.st_mode):
340                 try:
341                     os.rmdir(path)
342                 except OSError as e:
343                     if e.errno in (errno.ENOTEMPTY, errno.EEXIST):
344                         raise Exception('refusing to overwrite non-empty dir '
345                                         + path_msg(path))
346                     raise
347             else:
348                 os.unlink(path)
349
350         if stat.S_ISREG(self.mode):
351             assert(self._recognized_file_type())
352             fd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL, 0o600)
353             os.close(fd)
354         elif stat.S_ISDIR(self.mode):
355             assert(self._recognized_file_type())
356             os.mkdir(path, 0o700)
357         elif stat.S_ISCHR(self.mode):
358             assert(self._recognized_file_type())
359             os.mknod(path, 0o600 | stat.S_IFCHR, self.rdev)
360         elif stat.S_ISBLK(self.mode):
361             assert(self._recognized_file_type())
362             os.mknod(path, 0o600 | stat.S_IFBLK, self.rdev)
363         elif stat.S_ISFIFO(self.mode):
364             assert(self._recognized_file_type())
365             os.mkfifo(path, 0o600 | stat.S_IFIFO)
366         elif stat.S_ISSOCK(self.mode):
367             try:
368                 os.mknod(path, 0o600 | stat.S_IFSOCK)
369             except OSError as e:
370                 if e.errno in (errno.EINVAL, errno.EPERM):
371                     s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
372                     s.bind(path)
373                 else:
374                     raise
375         elif stat.S_ISLNK(self.mode):
376             assert(self._recognized_file_type())
377             if self.symlink_target and create_symlinks:
378                 # on MacOS, symlink() permissions depend on umask, and there's
379                 # no way to chown a symlink after creating it, so we have to
380                 # be careful here!
381                 oldumask = os.umask((self.mode & 0o777) ^ 0o777)
382                 try:
383                     os.symlink(self.symlink_target, path)
384                 finally:
385                     os.umask(oldumask)
386         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
387         else:
388             assert(not self._recognized_file_type())
389             add_error('not creating "%s" with unrecognized mode "0x%x"\n'
390                       % (path_msg(path), self.mode))
391
392     def _apply_common_rec(self, path, restore_numeric_ids=False):
393         if not self.mode:
394             raise ApplyError('no metadata - cannot apply to ' + path_msg(path))
395
396         # FIXME: S_ISDOOR, S_IFMPB, S_IFCMP, S_IFNWK, ... see stat(2).
397         # EACCES errors at this stage are fatal for the current path.
398         if lutime and stat.S_ISLNK(self.mode):
399             try:
400                 lutime(path, (self.atime, self.mtime))
401             except OSError as e:
402                 if e.errno == errno.EACCES:
403                     raise ApplyError('lutime: %s' % e)
404                 else:
405                     raise
406         else:
407             try:
408                 utime(path, (self.atime, self.mtime))
409             except OSError as e:
410                 if e.errno == errno.EACCES:
411                     raise ApplyError('utime: %s' % e)
412                 else:
413                     raise
414
415         uid = gid = -1 # By default, do nothing.
416         if is_superuser():
417             if self.uid is not None:
418                 uid = self.uid
419             if self.gid is not None:
420                 gid = self.gid
421             if not restore_numeric_ids:
422                 if self.uid != 0 and self.user:
423                     entry = pwd_from_name(self.user)
424                     if entry:
425                         uid = entry.pw_uid
426                 if self.gid != 0 and self.group:
427                     entry = grp_from_name(self.group)
428                     if entry:
429                         gid = entry.gr_gid
430         else: # not superuser - only consider changing the group/gid
431             user_gids = os.getgroups()
432             if self.gid in user_gids:
433                 gid = self.gid
434             if not restore_numeric_ids and self.gid != 0:
435                 # The grp might not exist on the local system.
436                 grps = filter(None, [grp_from_gid(x) for x in user_gids])
437                 if self.group in [x.gr_name for x in grps]:
438                     g = grp_from_name(self.group)
439                     if g:
440                         gid = g.gr_gid
441
442         if uid != -1 or gid != -1:
443             try:
444                 os.lchown(path, uid, gid)
445             except OSError as e:
446                 if e.errno == errno.EPERM:
447                     add_error('lchown: %s' %  e)
448                 elif sys.platform.startswith('cygwin') \
449                    and e.errno == errno.EINVAL:
450                     add_error('lchown: unknown uid/gid (%d/%d) for %s'
451                               %  (uid, gid, path_msg(path)))
452                 else:
453                     raise
454
455         if _have_lchmod:
456             try:
457                 os.lchmod(path, stat.S_IMODE(self.mode))
458             except errno.ENOSYS:  # Function not implemented
459                 pass
460         elif not stat.S_ISLNK(self.mode):
461             os.chmod(path, stat.S_IMODE(self.mode))
462
463
464     ## Path records
465
466     def _encode_path(self):
467         if self.path:
468             return vint.pack('s', self.path)
469         else:
470             return None
471
472     def _load_path_rec(self, port):
473         self.path = vint.unpack('s', vint.read_bvec(port))[0]
474
475
476     ## Symlink targets
477
478     def _add_symlink_target(self, path, st):
479         try:
480             if stat.S_ISLNK(st.st_mode):
481                 self.symlink_target = os.readlink(path)
482                 # might have read a different link than the
483                 # one that was in place when we did stat()
484                 self.size = len(self.symlink_target)
485         except OSError as e:
486             add_error('readlink: %s' % e)
487
488     def _encode_symlink_target(self):
489         return self.symlink_target
490
491     def _load_symlink_target_rec(self, port):
492         target = vint.read_bvec(port)
493         self.symlink_target = target
494         if self.size is None:
495             self.size = len(target)
496         else:
497             assert(self.size == len(target))
498
499
500     ## Hardlink targets
501
502     def _add_hardlink_target(self, target):
503         self.hardlink_target = target
504
505     def _same_hardlink_target(self, other):
506         """Return true or false to indicate similarity in the hardlink sense."""
507         return self.hardlink_target == other.hardlink_target
508
509     def _encode_hardlink_target(self):
510         return self.hardlink_target
511
512     def _load_hardlink_target_rec(self, port):
513         self.hardlink_target = vint.read_bvec(port)
514
515
516     ## POSIX1e ACL records
517
518     # Recorded as a list:
519     #   [txt_id_acl, num_id_acl]
520     # or, if a directory:
521     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
522     # The numeric/text distinction only matters when reading/restoring
523     # a stored record.
524     def _add_posix1e_acl(self, path, st):
525         if not read_acl:
526             return
527         if not stat.S_ISLNK(st.st_mode):
528             isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
529             self.posix1e_acl = read_acl(path, isdir)
530
531     def _same_posix1e_acl(self, other):
532         """Return true or false to indicate similarity in the hardlink sense."""
533         return self.posix1e_acl == other.posix1e_acl
534
535     def _encode_posix1e_acl(self):
536         # Encode as two strings (w/default ACL string possibly empty).
537         if self.posix1e_acl:
538             acls = self.posix1e_acl
539             if len(acls) == 2:
540                 return vint.pack('ssss', acls[0], acls[1], b'', b'')
541             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
542         else:
543             return None
544
545     def _load_posix1e_acl_rec(self, port):
546         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
547         if acl_rep[2] == b'':
548             acl_rep = acl_rep[:2]
549         self.posix1e_acl = acl_rep
550
551     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
552         if not self.posix1e_acl:
553             return
554
555         if not apply_acl:
556             add_error("%s: can't restore ACLs; posix1e support missing.\n"
557                       % path_msg(path))
558             return
559
560         try:
561             acls = self.posix1e_acl
562             offs = 1 if restore_numeric_ids else 0
563             if len(acls) > 2:
564                 apply_acl(path, acls[offs], acls[offs + 2])
565             else:
566                 apply_acl(path, acls[offs])
567         except IOError as e:
568             if e.errno == errno.EINVAL:
569                 # libacl returns with errno set to EINVAL if a user
570                 # (or group) doesn't exist
571                 raise ApplyError("POSIX1e ACL: can't create %r for %r"
572                                  % (acls, path_msg(path)))
573             elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
574                 raise ApplyError('POSIX1e ACL applyto: %s' % e)
575             else:
576                 raise
577
578
579     ## Linux attributes (lsattr(1), chattr(1))
580
581     def _add_linux_attr(self, path, st):
582         check_linux_file_attr_api()
583         if not get_linux_file_attr: return
584         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
585             try:
586                 attr = get_linux_file_attr(path)
587                 if attr != 0:
588                     self.linux_attr = attr
589             except OSError as e:
590                 if e.errno == errno.EACCES:
591                     add_error('read Linux attr: %s' % e)
592                 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
593                     # Assume filesystem doesn't support attrs.
594                     return
595                 elif e.errno == EINVAL:
596                     global _warned_about_attr_einval
597                     if not _warned_about_attr_einval:
598                         log("Ignoring attr EINVAL;"
599                             + " if you're not using ntfs-3g, please report: "
600                             + path_msg(path) + '\n')
601                         _warned_about_attr_einval = True
602                     return
603                 else:
604                     raise
605
606     def _same_linux_attr(self, other):
607         """Return true or false to indicate similarity in the hardlink sense."""
608         return self.linux_attr == other.linux_attr
609
610     def _encode_linux_attr(self):
611         if self.linux_attr:
612             return vint.pack('V', self.linux_attr)
613         else:
614             return None
615
616     def _load_linux_attr_rec(self, port):
617         data = vint.read_bvec(port)
618         self.linux_attr = vint.unpack('V', data)[0]
619
620     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
621         if self.linux_attr:
622             check_linux_file_attr_api()
623             if not set_linux_file_attr:
624                 add_error("%s: can't restore linuxattrs: "
625                           "linuxattr support missing.\n" % path_msg(path))
626                 return
627             try:
628                 set_linux_file_attr(path, self.linux_attr)
629             except OSError as e:
630                 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
631                     raise ApplyError('Linux chattr: %s (0x%s)'
632                                      % (e, hex(self.linux_attr)))
633                 elif e.errno == EINVAL:
634                     msg = "if you're not using ntfs-3g, please report"
635                     raise ApplyError('Linux chattr: %s (0x%s) (%s)'
636                                      % (e, hex(self.linux_attr), msg))
637                 else:
638                     raise
639
640
641     ## Linux extended attributes (getfattr(1), setfattr(1))
642
643     def _add_linux_xattr(self, path, st):
644         if not xattr: return
645         try:
646             self.linux_xattr = xattr.get_all(path, nofollow=True)
647         except EnvironmentError as e:
648             if e.errno != errno.EOPNOTSUPP:
649                 raise
650
651     def _same_linux_xattr(self, other):
652         """Return true or false to indicate similarity in the hardlink sense."""
653         return self.linux_xattr == other.linux_xattr
654
655     def _encode_linux_xattr(self):
656         if self.linux_xattr:
657             result = vint.pack('V', len(self.linux_xattr))
658             for name, value in self.linux_xattr:
659                 result += vint.pack('ss', name, value)
660             return result
661         else:
662             return None
663
664     def _load_linux_xattr_rec(self, file):
665         data = vint.read_bvec(file)
666         memfile = BytesIO(data)
667         result = []
668         for i in range(vint.read_vuint(memfile)):
669             key = vint.read_bvec(memfile)
670             value = vint.read_bvec(memfile)
671             result.append((key, value))
672         self.linux_xattr = result
673
674     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
675         if not xattr:
676             if self.linux_xattr:
677                 add_error("%s: can't restore xattr; xattr support missing.\n"
678                           % path_msg(path))
679             return
680         if not self.linux_xattr:
681             return
682         try:
683             existing_xattrs = set(xattr.list(path, nofollow=True))
684         except IOError as e:
685             if e.errno == errno.EACCES:
686                 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
687             else:
688                 raise
689         for k, v in self.linux_xattr:
690             if k not in existing_xattrs \
691                     or v != xattr.get(path, k, nofollow=True):
692                 try:
693                     xattr.set(path, k, v, nofollow=True)
694                 except IOError as e:
695                     if e.errno == errno.EPERM \
696                             or e.errno == errno.EOPNOTSUPP:
697                         raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
698                     else:
699                         raise
700             existing_xattrs -= frozenset([k])
701         for k in existing_xattrs:
702             try:
703                 xattr.remove(path, k, nofollow=True)
704             except IOError as e:
705                 if e.errno in (errno.EPERM, errno.EACCES):
706                     raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
707                 else:
708                     raise
709
710     def __init__(self):
711         self.mode = self.uid = self.gid = self.user = self.group = None
712         self.atime = self.mtime = self.ctime = None
713         # optional members
714         self.path = None
715         self.size = None
716         self.symlink_target = None
717         self.hardlink_target = None
718         self.linux_attr = None
719         self.linux_xattr = None
720         self.posix1e_acl = None
721
722     def __eq__(self, other):
723         if not isinstance(other, Metadata): return False
724         if self.mode != other.mode: return False
725         if self.mtime != other.mtime: return False
726         if self.ctime != other.ctime: return False
727         if self.atime != other.atime: return False
728         if self.path != other.path: return False
729         if self.uid != other.uid: return False
730         if self.gid != other.gid: return False
731         if self.size != other.size: return False
732         if self.user != other.user: return False
733         if self.group != other.group: return False
734         if self.symlink_target != other.symlink_target: return False
735         if self.hardlink_target != other.hardlink_target: return False
736         if self.linux_attr != other.linux_attr: return False
737         if self.posix1e_acl != other.posix1e_acl: return False
738         return True
739
740     def __ne__(self, other):
741         return not self.__eq__(other)
742
743     def __hash__(self):
744         return hash((self.mode,
745                      self.mtime,
746                      self.ctime,
747                      self.atime,
748                      self.path,
749                      self.uid,
750                      self.gid,
751                      self.size,
752                      self.user,
753                      self.group,
754                      self.symlink_target,
755                      self.hardlink_target,
756                      self.linux_attr,
757                      self.posix1e_acl))
758
759     def __repr__(self):
760         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
761         if self.path is not None:
762             result += ' path:' + repr(self.path)
763         if self.mode is not None:
764             result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
765         if self.uid is not None:
766             result += ' uid:' + str(self.uid)
767         if self.gid is not None:
768             result += ' gid:' + str(self.gid)
769         if self.user is not None:
770             result += ' user:' + repr(self.user)
771         if self.group is not None:
772             result += ' group:' + repr(self.group)
773         if self.size is not None:
774             result += ' size:' + repr(self.size)
775         for name, val in (('atime', self.atime),
776                           ('mtime', self.mtime),
777                           ('ctime', self.ctime)):
778             if val is not None:
779                 result += ' %s:%r (%d)' \
780                           % (name,
781                              strftime('%Y-%m-%d %H:%M %z',
782                                       gmtime(xstat.fstime_floor_secs(val))),
783                              val)
784         result += '>'
785         return ''.join(result)
786
787     def write(self, port, include_path=True):
788         port.write(self.encode(include_path=include_path))
789
790     def encode(self, include_path=True):
791         ret = []
792         records = include_path and [(_rec_tag_path, self._encode_path())] or []
793         records.extend([(_rec_tag_common_v3, self._encode_common()),
794                         (_rec_tag_symlink_target,
795                          self._encode_symlink_target()),
796                         (_rec_tag_hardlink_target,
797                          self._encode_hardlink_target()),
798                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
799                         (_rec_tag_linux_attr, self._encode_linux_attr()),
800                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
801         for tag, data in records:
802             if data:
803                 ret.extend((vint.encode_vuint(tag),
804                             vint.encode_bvec(data)))
805         ret.append(vint.encode_vuint(_rec_tag_end))
806         return b''.join(ret)
807
808     def copy(self):
809         return deepcopy(self)
810
811     @staticmethod
812     def read(port):
813         # This method should either return a valid Metadata object,
814         # return None if there was no information at all (just a
815         # _rec_tag_end), throw EOFError if there was nothing at all to
816         # read, or throw an Exception if a valid object could not be
817         # read completely.
818         tag = vint.read_vuint(port)
819         if tag == _rec_tag_end:
820             return None
821         try: # From here on, EOF is an error.
822             result = Metadata()
823             while True: # only exit is error (exception) or _rec_tag_end
824                 if tag == _rec_tag_path:
825                     result._load_path_rec(port)
826                 elif tag == _rec_tag_common_v3:
827                     result._load_common_rec(port, version=3)
828                 elif tag == _rec_tag_common_v2:
829                     result._load_common_rec(port, version=2)
830                 elif tag == _rec_tag_symlink_target:
831                     result._load_symlink_target_rec(port)
832                 elif tag == _rec_tag_hardlink_target:
833                     result._load_hardlink_target_rec(port)
834                 elif tag == _rec_tag_posix1e_acl:
835                     result._load_posix1e_acl_rec(port)
836                 elif tag == _rec_tag_linux_attr:
837                     result._load_linux_attr_rec(port)
838                 elif tag == _rec_tag_linux_xattr:
839                     result._load_linux_xattr_rec(port)
840                 elif tag == _rec_tag_end:
841                     return result
842                 elif tag == _rec_tag_common_v1: # Should be very rare.
843                     result._load_common_rec(port, version=1)
844                 else: # unknown record
845                     vint.skip_bvec(port)
846                 tag = vint.read_vuint(port)
847         except EOFError:
848             raise Exception("EOF while reading Metadata")
849
850     def isdir(self):
851         return stat.S_ISDIR(self.mode)
852
853     def create_path(self, path, create_symlinks=True):
854         self._create_via_common_rec(path, create_symlinks=create_symlinks)
855
856     def apply_to_path(self, path=None, restore_numeric_ids=False):
857         # apply metadata to path -- file must exist
858         if not path:
859             path = self.path
860         if not path:
861             raise Exception('Metadata.apply_to_path() called with no path')
862         if not self._recognized_file_type():
863             add_error('not applying metadata to "%s"' % path_msg(path)
864                       + ' with unrecognized mode "0x%x"\n' % self.mode)
865             return
866         num_ids = restore_numeric_ids
867         for apply_metadata in (self._apply_common_rec,
868                                self._apply_posix1e_acl_rec,
869                                self._apply_linux_attr_rec,
870                                self._apply_linux_xattr_rec):
871             try:
872                 apply_metadata(path, restore_numeric_ids=num_ids)
873             except ApplyError as e:
874                 add_error(e)
875
876     def same_file(self, other):
877         """Compare this to other for equivalency.  Return true if
878         their information implies they could represent the same file
879         on disk, in the hardlink sense.  Assume they're both regular
880         files."""
881         return self._same_common(other) \
882             and self._same_hardlink_target(other) \
883             and self._same_posix1e_acl(other) \
884             and self._same_linux_attr(other) \
885             and self._same_linux_xattr(other)
886
887
888 def from_path(path, statinfo=None, archive_path=None,
889               save_symlinks=True, hardlink_target=None,
890               normalized=False):
891     # This function is also a test hook; see test-save-errors
892     """Return the metadata associated with the path.  When normalized is
893     true, return the metadata appropriate for a typical save, which
894     may or may not be all of it."""
895     result = Metadata()
896     result.path = archive_path
897     st = statinfo or xstat.lstat(path)
898     result._add_common(path, st)
899     if save_symlinks:
900         result._add_symlink_target(path, st)
901     result._add_hardlink_target(hardlink_target)
902     result._add_posix1e_acl(path, st)
903     result._add_linux_attr(path, st)
904     result._add_linux_xattr(path, st)
905     if normalized:
906         # Only store sizes for regular files and symlinks for now.
907         if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
908             result.size = None
909     return result
910
911
912 def save_tree(output_file, paths,
913               recurse=False,
914               write_paths=True,
915               save_symlinks=True,
916               xdev=False):
917
918     # Issue top-level rewrite warnings.
919     for path in paths:
920         safe_path = _clean_up_path_for_archive(path)
921         if safe_path != path:
922             log('archiving "%s" as "%s"\n'
923                 % (path_msg(path), path_msg(safe_path)))
924
925     if not recurse:
926         for p in paths:
927             safe_path = _clean_up_path_for_archive(p)
928             st = xstat.lstat(p)
929             if stat.S_ISDIR(st.st_mode):
930                 safe_path += b'/'
931             m = from_path(p, statinfo=st, archive_path=safe_path,
932                           save_symlinks=save_symlinks)
933             if verbose:
934                 print(m.path, file=sys.stderr)
935             m.write(output_file, include_path=write_paths)
936     else:
937         start_dir = os.getcwd()
938         try:
939             for (p, st) in recursive_dirlist(paths, xdev=xdev):
940                 dirlist_dir = os.getcwd()
941                 os.chdir(start_dir)
942                 safe_path = _clean_up_path_for_archive(p)
943                 m = from_path(p, statinfo=st, archive_path=safe_path,
944                               save_symlinks=save_symlinks)
945                 if verbose:
946                     print(m.path, file=sys.stderr)
947                 m.write(output_file, include_path=write_paths)
948                 os.chdir(dirlist_dir)
949         finally:
950             os.chdir(start_dir)
951
952
953 def _set_up_path(meta, create_symlinks=True):
954     # Allow directories to exist as a special case -- might have
955     # been created by an earlier longer path.
956     if meta.isdir():
957         mkdirp(meta.path)
958     else:
959         parent = os.path.dirname(meta.path)
960         if parent:
961             mkdirp(parent)
962         meta.create_path(meta.path, create_symlinks=create_symlinks)
963
964
965 all_fields = frozenset(['path',
966                         'mode',
967                         'link-target',
968                         'rdev',
969                         'size',
970                         'uid',
971                         'gid',
972                         'user',
973                         'group',
974                         'atime',
975                         'mtime',
976                         'ctime',
977                         'linux-attr',
978                         'linux-xattr',
979                         'posix1e-acl'])
980
981
982 def summary_bytes(meta, numeric_ids = False, classification = None,
983                   human_readable = False):
984     """Return bytes containing the "ls -l" style listing for meta.
985     Classification may be "all", "type", or None."""
986     user_str = group_str = size_or_dev_str = b'?'
987     symlink_target = None
988     if meta:
989         name = meta.path
990         mode_str = xstat.mode_str(meta.mode).encode('ascii')
991         symlink_target = meta.symlink_target
992         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
993         mtime_str = strftime('%Y-%m-%d %H:%M',
994                              time.localtime(mtime_secs)).encode('ascii')
995         if meta.user and not numeric_ids:
996             user_str = meta.user
997         elif meta.uid != None:
998             user_str = str(meta.uid).encode()
999         if meta.group and not numeric_ids:
1000             group_str = meta.group
1001         elif meta.gid != None:
1002             group_str = str(meta.gid).encode()
1003         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1004             if meta.rdev:
1005                 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1006                                               os.minor(meta.rdev))).encode()
1007         elif meta.size != None:
1008             if human_readable:
1009                 size_or_dev_str = format_filesize(meta.size).encode()
1010             else:
1011                 size_or_dev_str = str(meta.size).encode()
1012         else:
1013             size_or_dev_str = b'-'
1014         if classification:
1015             classification_str = \
1016                 xstat.classification_str(meta.mode,
1017                                          classification == 'all').encode()
1018     else:
1019         mode_str = b'?' * 10
1020         mtime_str = b'????-??-?? ??:??'
1021         classification_str = b'?'
1022
1023     name = name or b''
1024     if classification:
1025         name += classification_str
1026     if symlink_target:
1027         name += b' -> ' + meta.symlink_target
1028
1029     return b'%-10s %-11s %11s %16s %s' % (mode_str,
1030                                           user_str + b'/' + group_str,
1031                                           size_or_dev_str,
1032                                           mtime_str,
1033                                           name)
1034
1035
1036 def detailed_bytes(meta, fields = None):
1037     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1038     # 0", "link-target:", etc.
1039     if not fields:
1040         fields = all_fields
1041
1042     result = []
1043     if 'path' in fields:
1044         path = meta.path or b''
1045         result.append(b'path: ' + path)
1046     if 'mode' in fields:
1047         result.append(b'mode: %o (%s)'
1048                       % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1049     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1050         result.append(b'link-target: ' + meta.symlink_target)
1051     if 'rdev' in fields:
1052         if meta.rdev:
1053             result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1054                                             os.minor(meta.rdev)))
1055         else:
1056             result.append(b'rdev: 0')
1057     if 'size' in fields and meta.size is not None:
1058         result.append(b'size: %d' % meta.size)
1059     if 'uid' in fields:
1060         result.append(b'uid: %d' % meta.uid)
1061     if 'gid' in fields:
1062         result.append(b'gid: %d' % meta.gid)
1063     if 'user' in fields:
1064         result.append(b'user: ' + meta.user)
1065     if 'group' in fields:
1066         result.append(b'group: ' + meta.group)
1067     if 'atime' in fields:
1068         # If we don't have xstat.lutime, that means we have to use
1069         # utime(), and utime() has no way to set the mtime/atime of a
1070         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
1071         # so let's not report it.  (That way scripts comparing
1072         # before/after won't trigger.)
1073         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1074             result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1075         else:
1076             result.append(b'atime: 0')
1077     if 'mtime' in fields:
1078         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1079             result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1080         else:
1081             result.append(b'mtime: 0')
1082     if 'ctime' in fields:
1083         result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1084     if 'linux-attr' in fields and meta.linux_attr:
1085         result.append(b'linux-attr: %x' % meta.linux_attr)
1086     if 'linux-xattr' in fields and meta.linux_xattr:
1087         for name, value in meta.linux_xattr:
1088             result.append(b'linux-xattr: %s -> %s' % (name, value))
1089     if 'posix1e-acl' in fields and meta.posix1e_acl:
1090         acl = meta.posix1e_acl[0]
1091         result.append(b'posix1e-acl: ' + acl + b'\n')
1092         if stat.S_ISDIR(meta.mode):
1093             def_acl = meta.posix1e_acl[2]
1094             result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1095     return b'\n'.join(result)
1096
1097
1098 class _ArchiveIterator:
1099     def __next__(self):
1100         try:
1101             return Metadata.read(self._file)
1102         except EOFError:
1103             raise StopIteration()
1104
1105     next = __next__
1106
1107     def __iter__(self):
1108         return self
1109
1110     def __init__(self, file):
1111         self._file = file
1112
1113
1114 def display_archive(file, out):
1115     if verbose > 1:
1116         first_item = True
1117         for meta in _ArchiveIterator(file):
1118             if not first_item:
1119                 out.write(b'\n')
1120             out.write(detailed_bytes(meta))
1121             out.write(b'\n')
1122             first_item = False
1123     elif verbose > 0:
1124         for meta in _ArchiveIterator(file):
1125             out.write(summary_bytes(meta))
1126             out.write(b'\n')
1127     elif verbose == 0:
1128         for meta in _ArchiveIterator(file):
1129             if not meta.path:
1130                 log('bup: no metadata path, but asked to only display path'
1131                     ' (increase verbosity?)')
1132                 sys.exit(1)
1133             out.write(meta.path)
1134             out.write(b'\n')
1135
1136
1137 def start_extract(file, create_symlinks=True):
1138     for meta in _ArchiveIterator(file):
1139         if not meta: # Hit end record.
1140             break
1141         if verbose:
1142             print(path_msg(meta.path), file=sys.stderr)
1143         xpath = _clean_up_extract_path(meta.path)
1144         if not xpath:
1145             add_error(Exception('skipping risky path "%s"'
1146                                 % path_msg(meta.path)))
1147         else:
1148             meta.path = xpath
1149             _set_up_path(meta, create_symlinks=create_symlinks)
1150
1151
1152 def finish_extract(file, restore_numeric_ids=False):
1153     all_dirs = []
1154     for meta in _ArchiveIterator(file):
1155         if not meta: # Hit end record.
1156             break
1157         xpath = _clean_up_extract_path(meta.path)
1158         if not xpath:
1159             add_error(Exception('skipping risky path "%s"'
1160                                 % path_msg(meta.path)))
1161         else:
1162             if os.path.isdir(meta.path):
1163                 all_dirs.append(meta)
1164             else:
1165                 if verbose:
1166                     print(path_msg(meta.path), file=sys.stderr)
1167                 meta.apply_to_path(path=xpath,
1168                                    restore_numeric_ids=restore_numeric_ids)
1169     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1170     for dir in all_dirs:
1171         # Don't need to check xpath -- won't be in all_dirs if not OK.
1172         xpath = _clean_up_extract_path(dir.path)
1173         if verbose:
1174             print(path_msg(dir.path), file=sys.stderr)
1175         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1176
1177
1178 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1179     # For now, just store all the directories and handle them last,
1180     # longest first.
1181     all_dirs = []
1182     for meta in _ArchiveIterator(file):
1183         if not meta: # Hit end record.
1184             break
1185         xpath = _clean_up_extract_path(meta.path)
1186         if not xpath:
1187             add_error(Exception('skipping risky path "%s"'
1188                                 % path_msg(meta.path)))
1189         else:
1190             meta.path = xpath
1191             if verbose:
1192                 print('+', path_msg(meta.path), file=sys.stderr)
1193             _set_up_path(meta, create_symlinks=create_symlinks)
1194             if os.path.isdir(meta.path):
1195                 all_dirs.append(meta)
1196             else:
1197                 if verbose:
1198                     print('=', path_msg(meta.path), file=sys.stderr)
1199                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1200     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1201     for dir in all_dirs:
1202         # Don't need to check xpath -- won't be in all_dirs if not OK.
1203         xpath = _clean_up_extract_path(dir.path)
1204         if verbose:
1205             print('=', path_msg(xpath), file=sys.stderr)
1206         # Shouldn't have to check for risky paths here (omitted above).
1207         dir.apply_to_path(path=dir.path,
1208                           restore_numeric_ids=restore_numeric_ids)