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