]> arthur.barton.de Git - bup.git/blob - lib/bup/metadata.py
metadata: mention that from_path is also used as a test hook
[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         except OSError as e:
483             add_error('readlink: %s' % e)
484
485     def _encode_symlink_target(self):
486         return self.symlink_target
487
488     def _load_symlink_target_rec(self, port):
489         target = vint.read_bvec(port)
490         self.symlink_target = target
491         if self.size is None:
492             self.size = len(target)
493         else:
494             assert(self.size == len(target))
495
496
497     ## Hardlink targets
498
499     def _add_hardlink_target(self, target):
500         self.hardlink_target = target
501
502     def _same_hardlink_target(self, other):
503         """Return true or false to indicate similarity in the hardlink sense."""
504         return self.hardlink_target == other.hardlink_target
505
506     def _encode_hardlink_target(self):
507         return self.hardlink_target
508
509     def _load_hardlink_target_rec(self, port):
510         self.hardlink_target = vint.read_bvec(port)
511
512
513     ## POSIX1e ACL records
514
515     # Recorded as a list:
516     #   [txt_id_acl, num_id_acl]
517     # or, if a directory:
518     #   [txt_id_acl, num_id_acl, txt_id_default_acl, num_id_default_acl]
519     # The numeric/text distinction only matters when reading/restoring
520     # a stored record.
521     def _add_posix1e_acl(self, path, st):
522         if not read_acl:
523             return
524         if not stat.S_ISLNK(st.st_mode):
525             isdir = 1 if stat.S_ISDIR(st.st_mode) else 0
526             self.posix1e_acl = read_acl(path, isdir)
527
528     def _same_posix1e_acl(self, other):
529         """Return true or false to indicate similarity in the hardlink sense."""
530         return self.posix1e_acl == other.posix1e_acl
531
532     def _encode_posix1e_acl(self):
533         # Encode as two strings (w/default ACL string possibly empty).
534         if self.posix1e_acl:
535             acls = self.posix1e_acl
536             if len(acls) == 2:
537                 return vint.pack('ssss', acls[0], acls[1], b'', b'')
538             return vint.pack('ssss', acls[0], acls[1], acls[2], acls[3])
539         else:
540             return None
541
542     def _load_posix1e_acl_rec(self, port):
543         acl_rep = vint.unpack('ssss', vint.read_bvec(port))
544         if acl_rep[2] == b'':
545             acl_rep = acl_rep[:2]
546         self.posix1e_acl = acl_rep
547
548     def _apply_posix1e_acl_rec(self, path, restore_numeric_ids=False):
549         if not self.posix1e_acl:
550             return
551
552         if not apply_acl:
553             add_error("%s: can't restore ACLs; posix1e support missing.\n"
554                       % path_msg(path))
555             return
556
557         try:
558             acls = self.posix1e_acl
559             offs = 1 if restore_numeric_ids else 0
560             if len(acls) > 2:
561                 apply_acl(path, acls[offs], acls[offs + 2])
562             else:
563                 apply_acl(path, acls[offs])
564         except IOError as e:
565             if e.errno == errno.EINVAL:
566                 # libacl returns with errno set to EINVAL if a user
567                 # (or group) doesn't exist
568                 raise ApplyError("POSIX1e ACL: can't create %r for %r"
569                                  % (acls, path_msg(path)))
570             elif e.errno == errno.EPERM or e.errno == errno.EOPNOTSUPP:
571                 raise ApplyError('POSIX1e ACL applyto: %s' % e)
572             else:
573                 raise
574
575
576     ## Linux attributes (lsattr(1), chattr(1))
577
578     def _add_linux_attr(self, path, st):
579         check_linux_file_attr_api()
580         if not get_linux_file_attr: return
581         if stat.S_ISREG(st.st_mode) or stat.S_ISDIR(st.st_mode):
582             try:
583                 attr = get_linux_file_attr(path)
584                 if attr != 0:
585                     self.linux_attr = attr
586             except OSError as e:
587                 if e.errno == errno.EACCES:
588                     add_error('read Linux attr: %s' % e)
589                 elif e.errno in (ENOTTY, ENOSYS, EOPNOTSUPP):
590                     # Assume filesystem doesn't support attrs.
591                     return
592                 elif e.errno == EINVAL:
593                     global _warned_about_attr_einval
594                     if not _warned_about_attr_einval:
595                         log("Ignoring attr EINVAL;"
596                             + " if you're not using ntfs-3g, please report: "
597                             + path_msg(path) + '\n')
598                         _warned_about_attr_einval = True
599                     return
600                 else:
601                     raise
602
603     def _same_linux_attr(self, other):
604         """Return true or false to indicate similarity in the hardlink sense."""
605         return self.linux_attr == other.linux_attr
606
607     def _encode_linux_attr(self):
608         if self.linux_attr:
609             return vint.pack('V', self.linux_attr)
610         else:
611             return None
612
613     def _load_linux_attr_rec(self, port):
614         data = vint.read_bvec(port)
615         self.linux_attr = vint.unpack('V', data)[0]
616
617     def _apply_linux_attr_rec(self, path, restore_numeric_ids=False):
618         if self.linux_attr:
619             check_linux_file_attr_api()
620             if not set_linux_file_attr:
621                 add_error("%s: can't restore linuxattrs: "
622                           "linuxattr support missing.\n" % path_msg(path))
623                 return
624             try:
625                 set_linux_file_attr(path, self.linux_attr)
626             except OSError as e:
627                 if e.errno in (EACCES, ENOTTY, EOPNOTSUPP, ENOSYS):
628                     raise ApplyError('Linux chattr: %s (0x%s)'
629                                      % (e, hex(self.linux_attr)))
630                 elif e.errno == EINVAL:
631                     msg = "if you're not using ntfs-3g, please report"
632                     raise ApplyError('Linux chattr: %s (0x%s) (%s)'
633                                      % (e, hex(self.linux_attr), msg))
634                 else:
635                     raise
636
637
638     ## Linux extended attributes (getfattr(1), setfattr(1))
639
640     def _add_linux_xattr(self, path, st):
641         if not xattr: return
642         try:
643             self.linux_xattr = xattr.get_all(path, nofollow=True)
644         except EnvironmentError as e:
645             if e.errno != errno.EOPNOTSUPP:
646                 raise
647
648     def _same_linux_xattr(self, other):
649         """Return true or false to indicate similarity in the hardlink sense."""
650         return self.linux_xattr == other.linux_xattr
651
652     def _encode_linux_xattr(self):
653         if self.linux_xattr:
654             result = vint.pack('V', len(self.linux_xattr))
655             for name, value in self.linux_xattr:
656                 result += vint.pack('ss', name, value)
657             return result
658         else:
659             return None
660
661     def _load_linux_xattr_rec(self, file):
662         data = vint.read_bvec(file)
663         memfile = BytesIO(data)
664         result = []
665         for i in range(vint.read_vuint(memfile)):
666             key = vint.read_bvec(memfile)
667             value = vint.read_bvec(memfile)
668             result.append((key, value))
669         self.linux_xattr = result
670
671     def _apply_linux_xattr_rec(self, path, restore_numeric_ids=False):
672         if not xattr:
673             if self.linux_xattr:
674                 add_error("%s: can't restore xattr; xattr support missing.\n"
675                           % path_msg(path))
676             return
677         if not self.linux_xattr:
678             return
679         try:
680             existing_xattrs = set(xattr.list(path, nofollow=True))
681         except IOError as e:
682             if e.errno == errno.EACCES:
683                 raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
684             else:
685                 raise
686         for k, v in self.linux_xattr:
687             if k not in existing_xattrs \
688                     or v != xattr.get(path, k, nofollow=True):
689                 try:
690                     xattr.set(path, k, v, nofollow=True)
691                 except IOError as e:
692                     if e.errno == errno.EPERM \
693                             or e.errno == errno.EOPNOTSUPP:
694                         raise ApplyError('xattr.set %r: %s' % (path_msg(path), e))
695                     else:
696                         raise
697             existing_xattrs -= frozenset([k])
698         for k in existing_xattrs:
699             try:
700                 xattr.remove(path, k, nofollow=True)
701             except IOError as e:
702                 if e.errno in (errno.EPERM, errno.EACCES):
703                     raise ApplyError('xattr.remove %r: %s' % (path_msg(path), e))
704                 else:
705                     raise
706
707     def __init__(self):
708         self.mode = self.uid = self.gid = self.user = self.group = None
709         self.atime = self.mtime = self.ctime = None
710         # optional members
711         self.path = None
712         self.size = None
713         self.symlink_target = None
714         self.hardlink_target = None
715         self.linux_attr = None
716         self.linux_xattr = None
717         self.posix1e_acl = None
718
719     def __eq__(self, other):
720         if not isinstance(other, Metadata): return False
721         if self.mode != other.mode: return False
722         if self.mtime != other.mtime: return False
723         if self.ctime != other.ctime: return False
724         if self.atime != other.atime: return False
725         if self.path != other.path: return False
726         if self.uid != other.uid: return False
727         if self.gid != other.gid: return False
728         if self.size != other.size: return False
729         if self.user != other.user: return False
730         if self.group != other.group: return False
731         if self.symlink_target != other.symlink_target: return False
732         if self.hardlink_target != other.hardlink_target: return False
733         if self.linux_attr != other.linux_attr: return False
734         if self.posix1e_acl != other.posix1e_acl: return False
735         return True
736
737     def __ne__(self, other):
738         return not self.__eq__(other)
739
740     def __hash__(self):
741         return hash((self.mode,
742                      self.mtime,
743                      self.ctime,
744                      self.atime,
745                      self.path,
746                      self.uid,
747                      self.gid,
748                      self.size,
749                      self.user,
750                      self.group,
751                      self.symlink_target,
752                      self.hardlink_target,
753                      self.linux_attr,
754                      self.posix1e_acl))
755
756     def __repr__(self):
757         result = ['<%s instance at %s' % (self.__class__, hex(id(self)))]
758         if self.path is not None:
759             result += ' path:' + repr(self.path)
760         if self.mode is not None:
761             result += ' mode: %o (%s)' % (self.mode, xstat.mode_str(self.mode))
762         if self.uid is not None:
763             result += ' uid:' + str(self.uid)
764         if self.gid is not None:
765             result += ' gid:' + str(self.gid)
766         if self.user is not None:
767             result += ' user:' + repr(self.user)
768         if self.group is not None:
769             result += ' group:' + repr(self.group)
770         if self.size is not None:
771             result += ' size:' + repr(self.size)
772         for name, val in (('atime', self.atime),
773                           ('mtime', self.mtime),
774                           ('ctime', self.ctime)):
775             if val is not None:
776                 result += ' %s:%r (%d)' \
777                           % (name,
778                              strftime('%Y-%m-%d %H:%M %z',
779                                       gmtime(xstat.fstime_floor_secs(val))),
780                              val)
781         result += '>'
782         return ''.join(result)
783
784     def write(self, port, include_path=True):
785         port.write(self.encode(include_path=include_path))
786
787     def encode(self, include_path=True):
788         ret = []
789         records = include_path and [(_rec_tag_path, self._encode_path())] or []
790         records.extend([(_rec_tag_common_v3, self._encode_common()),
791                         (_rec_tag_symlink_target,
792                          self._encode_symlink_target()),
793                         (_rec_tag_hardlink_target,
794                          self._encode_hardlink_target()),
795                         (_rec_tag_posix1e_acl, self._encode_posix1e_acl()),
796                         (_rec_tag_linux_attr, self._encode_linux_attr()),
797                         (_rec_tag_linux_xattr, self._encode_linux_xattr())])
798         for tag, data in records:
799             if data:
800                 ret.extend((vint.encode_vuint(tag),
801                             vint.encode_bvec(data)))
802         ret.append(vint.encode_vuint(_rec_tag_end))
803         return b''.join(ret)
804
805     def copy(self):
806         return deepcopy(self)
807
808     @staticmethod
809     def read(port):
810         # This method should either return a valid Metadata object,
811         # return None if there was no information at all (just a
812         # _rec_tag_end), throw EOFError if there was nothing at all to
813         # read, or throw an Exception if a valid object could not be
814         # read completely.
815         tag = vint.read_vuint(port)
816         if tag == _rec_tag_end:
817             return None
818         try: # From here on, EOF is an error.
819             result = Metadata()
820             while True: # only exit is error (exception) or _rec_tag_end
821                 if tag == _rec_tag_path:
822                     result._load_path_rec(port)
823                 elif tag == _rec_tag_common_v3:
824                     result._load_common_rec(port, version=3)
825                 elif tag == _rec_tag_common_v2:
826                     result._load_common_rec(port, version=2)
827                 elif tag == _rec_tag_symlink_target:
828                     result._load_symlink_target_rec(port)
829                 elif tag == _rec_tag_hardlink_target:
830                     result._load_hardlink_target_rec(port)
831                 elif tag == _rec_tag_posix1e_acl:
832                     result._load_posix1e_acl_rec(port)
833                 elif tag == _rec_tag_linux_attr:
834                     result._load_linux_attr_rec(port)
835                 elif tag == _rec_tag_linux_xattr:
836                     result._load_linux_xattr_rec(port)
837                 elif tag == _rec_tag_end:
838                     return result
839                 elif tag == _rec_tag_common_v1: # Should be very rare.
840                     result._load_common_rec(port, version=1)
841                 else: # unknown record
842                     vint.skip_bvec(port)
843                 tag = vint.read_vuint(port)
844         except EOFError:
845             raise Exception("EOF while reading Metadata")
846
847     def isdir(self):
848         return stat.S_ISDIR(self.mode)
849
850     def create_path(self, path, create_symlinks=True):
851         self._create_via_common_rec(path, create_symlinks=create_symlinks)
852
853     def apply_to_path(self, path=None, restore_numeric_ids=False):
854         # apply metadata to path -- file must exist
855         if not path:
856             path = self.path
857         if not path:
858             raise Exception('Metadata.apply_to_path() called with no path')
859         if not self._recognized_file_type():
860             add_error('not applying metadata to "%s"' % path_msg(path)
861                       + ' with unrecognized mode "0x%x"\n' % self.mode)
862             return
863         num_ids = restore_numeric_ids
864         for apply_metadata in (self._apply_common_rec,
865                                self._apply_posix1e_acl_rec,
866                                self._apply_linux_attr_rec,
867                                self._apply_linux_xattr_rec):
868             try:
869                 apply_metadata(path, restore_numeric_ids=num_ids)
870             except ApplyError as e:
871                 add_error(e)
872
873     def same_file(self, other):
874         """Compare this to other for equivalency.  Return true if
875         their information implies they could represent the same file
876         on disk, in the hardlink sense.  Assume they're both regular
877         files."""
878         return self._same_common(other) \
879             and self._same_hardlink_target(other) \
880             and self._same_posix1e_acl(other) \
881             and self._same_linux_attr(other) \
882             and self._same_linux_xattr(other)
883
884
885 def from_path(path, statinfo=None, archive_path=None,
886               save_symlinks=True, hardlink_target=None,
887               normalized=False):
888     # This function is also a test hook; see test-save-errors
889     """Return the metadata associated with the path.  When normalized is
890     true, return the metadata appropriate for a typical save, which
891     may or may not be all of it."""
892     result = Metadata()
893     result.path = archive_path
894     st = statinfo or xstat.lstat(path)
895     result._add_common(path, st)
896     if save_symlinks:
897         result._add_symlink_target(path, st)
898     result._add_hardlink_target(hardlink_target)
899     result._add_posix1e_acl(path, st)
900     result._add_linux_attr(path, st)
901     result._add_linux_xattr(path, st)
902     if normalized:
903         # Only store sizes for regular files and symlinks for now.
904         if not (stat.S_ISREG(result.mode) or stat.S_ISLNK(result.mode)):
905             result.size = None
906     return result
907
908
909 def save_tree(output_file, paths,
910               recurse=False,
911               write_paths=True,
912               save_symlinks=True,
913               xdev=False):
914
915     # Issue top-level rewrite warnings.
916     for path in paths:
917         safe_path = _clean_up_path_for_archive(path)
918         if safe_path != path:
919             log('archiving "%s" as "%s"\n'
920                 % (path_msg(path), path_msg(safe_path)))
921
922     if not recurse:
923         for p in paths:
924             safe_path = _clean_up_path_for_archive(p)
925             st = xstat.lstat(p)
926             if stat.S_ISDIR(st.st_mode):
927                 safe_path += b'/'
928             m = from_path(p, statinfo=st, archive_path=safe_path,
929                           save_symlinks=save_symlinks)
930             if verbose:
931                 print(m.path, file=sys.stderr)
932             m.write(output_file, include_path=write_paths)
933     else:
934         start_dir = os.getcwd()
935         try:
936             for (p, st) in recursive_dirlist(paths, xdev=xdev):
937                 dirlist_dir = os.getcwd()
938                 os.chdir(start_dir)
939                 safe_path = _clean_up_path_for_archive(p)
940                 m = from_path(p, statinfo=st, archive_path=safe_path,
941                               save_symlinks=save_symlinks)
942                 if verbose:
943                     print(m.path, file=sys.stderr)
944                 m.write(output_file, include_path=write_paths)
945                 os.chdir(dirlist_dir)
946         finally:
947             os.chdir(start_dir)
948
949
950 def _set_up_path(meta, create_symlinks=True):
951     # Allow directories to exist as a special case -- might have
952     # been created by an earlier longer path.
953     if meta.isdir():
954         mkdirp(meta.path)
955     else:
956         parent = os.path.dirname(meta.path)
957         if parent:
958             mkdirp(parent)
959         meta.create_path(meta.path, create_symlinks=create_symlinks)
960
961
962 all_fields = frozenset(['path',
963                         'mode',
964                         'link-target',
965                         'rdev',
966                         'size',
967                         'uid',
968                         'gid',
969                         'user',
970                         'group',
971                         'atime',
972                         'mtime',
973                         'ctime',
974                         'linux-attr',
975                         'linux-xattr',
976                         'posix1e-acl'])
977
978
979 def summary_bytes(meta, numeric_ids = False, classification = None,
980                   human_readable = False):
981     """Return bytes containing the "ls -l" style listing for meta.
982     Classification may be "all", "type", or None."""
983     user_str = group_str = size_or_dev_str = b'?'
984     symlink_target = None
985     if meta:
986         name = meta.path
987         mode_str = xstat.mode_str(meta.mode).encode('ascii')
988         symlink_target = meta.symlink_target
989         mtime_secs = xstat.fstime_floor_secs(meta.mtime)
990         mtime_str = strftime('%Y-%m-%d %H:%M',
991                              time.localtime(mtime_secs)).encode('ascii')
992         if meta.user and not numeric_ids:
993             user_str = meta.user
994         elif meta.uid != None:
995             user_str = str(meta.uid).encode()
996         if meta.group and not numeric_ids:
997             group_str = meta.group
998         elif meta.gid != None:
999             group_str = str(meta.gid).encode()
1000         if stat.S_ISCHR(meta.mode) or stat.S_ISBLK(meta.mode):
1001             if meta.rdev:
1002                 size_or_dev_str = ('%d,%d' % (os.major(meta.rdev),
1003                                               os.minor(meta.rdev))).encode()
1004         elif meta.size != None:
1005             if human_readable:
1006                 size_or_dev_str = format_filesize(meta.size).encode()
1007             else:
1008                 size_or_dev_str = str(meta.size).encode()
1009         else:
1010             size_or_dev_str = b'-'
1011         if classification:
1012             classification_str = \
1013                 xstat.classification_str(meta.mode,
1014                                          classification == 'all').encode()
1015     else:
1016         mode_str = b'?' * 10
1017         mtime_str = b'????-??-?? ??:??'
1018         classification_str = b'?'
1019
1020     name = name or b''
1021     if classification:
1022         name += classification_str
1023     if symlink_target:
1024         name += b' -> ' + meta.symlink_target
1025
1026     return b'%-10s %-11s %11s %16s %s' % (mode_str,
1027                                           user_str + b'/' + group_str,
1028                                           size_or_dev_str,
1029                                           mtime_str,
1030                                           name)
1031
1032
1033 def detailed_bytes(meta, fields = None):
1034     # FIXME: should optional fields be omitted, or empty i.e. "rdev:
1035     # 0", "link-target:", etc.
1036     if not fields:
1037         fields = all_fields
1038
1039     result = []
1040     if 'path' in fields:
1041         path = meta.path or b''
1042         result.append(b'path: ' + path)
1043     if 'mode' in fields:
1044         result.append(b'mode: %o (%s)'
1045                       % (meta.mode, xstat.mode_str(meta.mode).encode('ascii')))
1046     if 'link-target' in fields and stat.S_ISLNK(meta.mode):
1047         result.append(b'link-target: ' + meta.symlink_target)
1048     if 'rdev' in fields:
1049         if meta.rdev:
1050             result.append(b'rdev: %d,%d' % (os.major(meta.rdev),
1051                                             os.minor(meta.rdev)))
1052         else:
1053             result.append(b'rdev: 0')
1054     if 'size' in fields and meta.size is not None:
1055         result.append(b'size: %d' % meta.size)
1056     if 'uid' in fields:
1057         result.append(b'uid: %d' % meta.uid)
1058     if 'gid' in fields:
1059         result.append(b'gid: %d' % meta.gid)
1060     if 'user' in fields:
1061         result.append(b'user: ' + meta.user)
1062     if 'group' in fields:
1063         result.append(b'group: ' + meta.group)
1064     if 'atime' in fields:
1065         # If we don't have xstat.lutime, that means we have to use
1066         # utime(), and utime() has no way to set the mtime/atime of a
1067         # symlink.  Thus, the mtime/atime of a symlink is meaningless,
1068         # so let's not report it.  (That way scripts comparing
1069         # before/after won't trigger.)
1070         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1071             result.append(b'atime: ' + xstat.fstime_to_sec_bytes(meta.atime))
1072         else:
1073             result.append(b'atime: 0')
1074     if 'mtime' in fields:
1075         if xstat.lutime or not stat.S_ISLNK(meta.mode):
1076             result.append(b'mtime: ' + xstat.fstime_to_sec_bytes(meta.mtime))
1077         else:
1078             result.append(b'mtime: 0')
1079     if 'ctime' in fields:
1080         result.append(b'ctime: ' + xstat.fstime_to_sec_bytes(meta.ctime))
1081     if 'linux-attr' in fields and meta.linux_attr:
1082         result.append(b'linux-attr: %x' % meta.linux_attr)
1083     if 'linux-xattr' in fields and meta.linux_xattr:
1084         for name, value in meta.linux_xattr:
1085             result.append(b'linux-xattr: %s -> %s' % (name, value))
1086     if 'posix1e-acl' in fields and meta.posix1e_acl:
1087         acl = meta.posix1e_acl[0]
1088         result.append(b'posix1e-acl: ' + acl + b'\n')
1089         if stat.S_ISDIR(meta.mode):
1090             def_acl = meta.posix1e_acl[2]
1091             result.append(b'posix1e-acl-default: ' + def_acl + b'\n')
1092     return b'\n'.join(result)
1093
1094
1095 class _ArchiveIterator:
1096     def __next__(self):
1097         try:
1098             return Metadata.read(self._file)
1099         except EOFError:
1100             raise StopIteration()
1101
1102     next = __next__
1103
1104     def __iter__(self):
1105         return self
1106
1107     def __init__(self, file):
1108         self._file = file
1109
1110
1111 def display_archive(file, out):
1112     if verbose > 1:
1113         first_item = True
1114         for meta in _ArchiveIterator(file):
1115             if not first_item:
1116                 out.write(b'\n')
1117             out.write(detailed_bytes(meta))
1118             out.write(b'\n')
1119             first_item = False
1120     elif verbose > 0:
1121         for meta in _ArchiveIterator(file):
1122             out.write(summary_bytes(meta))
1123             out.write(b'\n')
1124     elif verbose == 0:
1125         for meta in _ArchiveIterator(file):
1126             if not meta.path:
1127                 log('bup: no metadata path, but asked to only display path'
1128                     ' (increase verbosity?)')
1129                 sys.exit(1)
1130             out.write(meta.path)
1131             out.write(b'\n')
1132
1133
1134 def start_extract(file, create_symlinks=True):
1135     for meta in _ArchiveIterator(file):
1136         if not meta: # Hit end record.
1137             break
1138         if verbose:
1139             print(path_msg(meta.path), file=sys.stderr)
1140         xpath = _clean_up_extract_path(meta.path)
1141         if not xpath:
1142             add_error(Exception('skipping risky path "%s"'
1143                                 % path_msg(meta.path)))
1144         else:
1145             meta.path = xpath
1146             _set_up_path(meta, create_symlinks=create_symlinks)
1147
1148
1149 def finish_extract(file, restore_numeric_ids=False):
1150     all_dirs = []
1151     for meta in _ArchiveIterator(file):
1152         if not meta: # Hit end record.
1153             break
1154         xpath = _clean_up_extract_path(meta.path)
1155         if not xpath:
1156             add_error(Exception('skipping risky path "%s"'
1157                                 % path_msg(meta.path)))
1158         else:
1159             if os.path.isdir(meta.path):
1160                 all_dirs.append(meta)
1161             else:
1162                 if verbose:
1163                     print(path_msg(meta.path), file=sys.stderr)
1164                 meta.apply_to_path(path=xpath,
1165                                    restore_numeric_ids=restore_numeric_ids)
1166     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1167     for dir in all_dirs:
1168         # Don't need to check xpath -- won't be in all_dirs if not OK.
1169         xpath = _clean_up_extract_path(dir.path)
1170         if verbose:
1171             print(path_msg(dir.path), file=sys.stderr)
1172         dir.apply_to_path(path=xpath, restore_numeric_ids=restore_numeric_ids)
1173
1174
1175 def extract(file, restore_numeric_ids=False, create_symlinks=True):
1176     # For now, just store all the directories and handle them last,
1177     # longest first.
1178     all_dirs = []
1179     for meta in _ArchiveIterator(file):
1180         if not meta: # Hit end record.
1181             break
1182         xpath = _clean_up_extract_path(meta.path)
1183         if not xpath:
1184             add_error(Exception('skipping risky path "%s"'
1185                                 % path_msg(meta.path)))
1186         else:
1187             meta.path = xpath
1188             if verbose:
1189                 print('+', path_msg(meta.path), file=sys.stderr)
1190             _set_up_path(meta, create_symlinks=create_symlinks)
1191             if os.path.isdir(meta.path):
1192                 all_dirs.append(meta)
1193             else:
1194                 if verbose:
1195                     print('=', path_msg(meta.path), file=sys.stderr)
1196                 meta.apply_to_path(restore_numeric_ids=restore_numeric_ids)
1197     all_dirs.sort(key = lambda x : len(x.path), reverse=True)
1198     for dir in all_dirs:
1199         # Don't need to check xpath -- won't be in all_dirs if not OK.
1200         xpath = _clean_up_extract_path(dir.path)
1201         if verbose:
1202             print('=', path_msg(xpath), file=sys.stderr)
1203         # Shouldn't have to check for risky paths here (omitted above).
1204         dir.apply_to_path(path=dir.path,
1205                           restore_numeric_ids=restore_numeric_ids)