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