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