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