1# Copyright 2003 Ben Escoto
2#
3# This file is part of rdiff-backup.
4#
5# rdiff-backup is free software; you can redistribute it and/or modify
6# under the terms of the GNU General Public License as published by the
7# Free Software Foundation; either version 2 of the License, or (at your
8# option) any later version.
9#
10# rdiff-backup is distributed in the hope that it will be useful, but
11# WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13# General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with rdiff-backup; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18# 02110-1301, USA
19"""Determine the capabilities of given file system
20
21rdiff-backup needs to read and write to file systems with varying
22abilities.  For instance, some file systems and not others have ACLs,
23are case-sensitive, or can store ownership information.  The code in
24this module tests the file system for various features, and returns an
25FSAbilities object describing it.
26
27"""
28
29import errno
30import os
31from . import Globals, log, TempFile, selection, robust, SetConnections, \
32    FilenameMapping, win_acls, Time
33
34
35class FSAbilities:
36    """Store capabilities of given file system"""
37    extended_filenames = None  # True if filenames can have non-ASCII chars
38    win_reserved_filenames = None  # True if filenames can't have ",*,: etc.
39    case_sensitive = None  # True if "foobar" and "FoObAr" are different files
40    ownership = None  # True if chown works on this filesystem
41    acls = None  # True if access control lists supported
42    eas = None  # True if extended attributes supported
43    win_acls = None  # True if windows access control lists supported
44    hardlinks = None  # True if hard linking supported
45    fsync_dirs = None  # True if directories can be fsync'd
46    dir_inc_perms = None  # True if regular files can have full permissions
47    resource_forks = None  # True if system supports resource forks
48    carbonfile = None  # True if Mac Carbon file data is supported.
49    name = None  # Short string, not used for any technical purpose
50    read_only = None  # True if capabilities were determined non-destructively
51    high_perms = None  # True if suid etc perms are (read/write) supported
52    escape_dos_devices = None  # True if dos device files can't be created (e.g.,
53    # aux, con, com1, etc)
54    escape_trailing_spaces = None  # True if trailing spaces or periods at the
55    # end of filenames aren't preserved
56    symlink_perms = None  # True if symlink perms are affected by umask
57
58    def __init__(self, name=None):
59        """FSAbilities initializer.  name is only used in logging"""
60        self.name = name
61
62    def __str__(self):
63        """Return pretty printable version of self"""
64        assert self.read_only == 0 or self.read_only == 1, self.read_only
65        s = ['-' * 65]
66
67        def addline(desc, val_text):
68            """Add description line to s"""
69            s.append('  %s%s%s' % (desc, ' ' * (45 - len(desc)), val_text))
70
71        def add_boolean_list(pair_list):
72            """Add lines from list of (desc, boolean) pairs"""
73            for desc, boolean in pair_list:
74                if boolean:
75                    val_text = 'On'
76                elif boolean is None:
77                    val_text = 'N/A'
78                else:
79                    assert boolean == 0
80                    val_text = 'Off'
81                addline(desc, val_text)
82
83        def get_title_line():
84            """Add the first line, mostly for decoration"""
85            read_string = self.read_only and "read only" or "read/write"
86            if self.name:
87                return ('Detected abilities for %s (%s) file system:' %
88                        (self.name, read_string))
89            else:
90                return (
91                    'Detected abilities for %s file system' % (read_string, ))
92
93        s.append(get_title_line())
94        if not self.read_only:
95            add_boolean_list([
96                ('Ownership changing', self.ownership),
97                ('Hard linking', self.hardlinks),
98                ('fsync() directories', self.fsync_dirs),
99                ('Directory inc permissions', self.dir_inc_perms),
100                ('High-bit permissions', self.high_perms),
101                ('Symlink permissions', self.symlink_perms),
102                ('Extended filenames', self.extended_filenames),
103                ('Windows reserved filenames', self.win_reserved_filenames),
104            ])
105        add_boolean_list(
106            [('Access control lists', self.acls),
107             ('Extended attributes', self.eas),
108             ('Windows access control lists', self.win_acls),
109             ('Case sensitivity', self.case_sensitive),
110             ('Escape DOS devices', self.escape_dos_devices),
111             ('Escape trailing spaces', self.escape_trailing_spaces),
112             ('Mac OS X style resource forks', self.resource_forks),
113             ('Mac OS X Finder information', self.carbonfile)])
114        s.append(s[0])
115        return '\n'.join(s)
116
117    def init_readonly(self, rp):
118        """Set variables using fs tested at RPath rp.  Run locally.
119
120        This method does not write to the file system at all, and
121        should be run on the file system when the file system will
122        only need to be read.
123
124        Only self.acls and self.eas are set.
125
126        """
127        assert rp.conn is Globals.local_connection
128        self.root_rp = rp
129        self.read_only = 1
130        self.set_eas(rp, 0)
131        self.set_acls(rp)
132        self.set_win_acls(rp, 0)
133        self.set_resource_fork_readonly(rp)
134        self.set_carbonfile()
135        self.set_case_sensitive_readonly(rp)
136        self.set_escape_dos_devices(rp)
137        self.set_escape_trailing_spaces_readonly(rp)
138        return self
139
140    def init_readwrite(self, rbdir):
141        """Set variables using fs tested at rp_base.  Run locally.
142
143        This method creates a temp directory in rp_base and writes to
144        it in order to test various features.  Use on a file system
145        that will be written to.
146
147        """
148        assert rbdir.conn is Globals.local_connection
149        if not rbdir.isdir():
150            assert not rbdir.lstat(), (rbdir.path, rbdir.lstat())
151            rbdir.mkdir()
152        self.root_rp = rbdir
153        self.read_only = 0
154        subdir = TempFile.new_in_dir(rbdir)
155        subdir.mkdir()
156
157        self.set_extended_filenames(subdir)
158        self.set_win_reserved_filenames(subdir)
159        self.set_case_sensitive_readwrite(subdir)
160        self.set_ownership(subdir)
161        self.set_hardlinks(subdir)
162        self.set_fsync_dirs(subdir)
163        self.set_eas(subdir, 1)
164        self.set_acls(subdir)
165        self.set_win_acls(subdir, 1)
166        self.set_dir_inc_perms(subdir)
167        self.set_resource_fork_readwrite(subdir)
168        self.set_carbonfile()
169        self.set_high_perms_readwrite(subdir)
170        self.set_symlink_perms(subdir)
171        self.set_escape_dos_devices(subdir)
172        self.set_escape_trailing_spaces_readwrite(subdir)
173
174        subdir.delete()
175        return self
176
177    def set_ownership(self, testdir):
178        """Set self.ownership to true iff testdir's ownership can be changed"""
179        tmp_rp = testdir.append("foo")
180        tmp_rp.touch()
181        uid, gid = tmp_rp.getuidgid()
182        try:
183            tmp_rp.chown(uid // 2 + 1, gid // 2 + 1)  # just choose random uid/gid
184            tmp_rp.chown(0, 0)
185        except (IOError, OSError, AttributeError):
186            self.ownership = 0
187        else:
188            self.ownership = 1
189        tmp_rp.delete()
190
191    def set_hardlinks(self, testdir):
192        """Set self.hardlinks to true iff hard linked files can be made"""
193        hl_source = testdir.append("hardlinked_file1")
194        hl_dir = testdir.append("hl")
195        hl_dir.mkdir()
196        hl_dest = hl_dir.append("hardlinked_file2")
197        hl_source.touch()
198        try:
199            hl_dest.hardlink(hl_source.path)
200            if hl_source.getinode() != hl_dest.getinode():
201                raise IOError(errno.EOPNOTSUPP, "Hard links don't compare")
202        except (IOError, OSError, AttributeError):
203            if Globals.preserve_hardlinks != 0:
204                log.Log(
205                    "Warning: hard linking not supported by filesystem "
206                    "at %s" % self.root_rp.get_safepath(), 3)
207            self.hardlinks = None
208        else:
209            self.hardlinks = 1
210
211    def set_fsync_dirs(self, testdir):
212        """Set self.fsync_dirs if directories can be fsync'd"""
213        assert testdir.conn is Globals.local_connection
214        try:
215            testdir.fsync()
216        except (IOError, OSError):
217            log.Log(
218                "Directories on file system at %s are not fsyncable.\n"
219                "Assuming it's unnecessary." % testdir.get_safepath(), 4)
220            self.fsync_dirs = 0
221        else:
222            self.fsync_dirs = 1
223
224    def set_extended_filenames(self, subdir):
225        """Set self.extended_filenames by trying to write a path"""
226        assert not self.read_only
227
228        # Make sure ordinary filenames ok
229        ordinary_filename = b'5-_ a.snapshot.gz'
230        ord_rp = subdir.append(ordinary_filename)
231        ord_rp.touch()
232        assert ord_rp.lstat()
233        ord_rp.delete()
234
235        # Try path with UTF-8 encoded character
236        extended_filename = (
237            'uni' + chr(225) + chr(132) + chr(137)).encode('utf-8')
238        ext_rp = None
239        try:
240            ext_rp = subdir.append(extended_filename)
241            ext_rp.touch()
242        except (IOError, OSError):
243            if ext_rp:
244                assert not ext_rp.lstat()
245            self.extended_filenames = 0
246        else:
247            assert ext_rp.lstat()
248            try:
249                ext_rp.delete()
250            except (IOError, OSError):
251                # Broken CIFS setups will sometimes create UTF-8 files
252                # and even stat them, but not let us perform file operations
253                # on them. Test file cannot be deleted. UTF-8 chars not in the
254                # underlying codepage get translated to '?'
255                log.Log.FatalError(
256                    "Could not delete extended filenames test "
257                    "file. If you are using a CIFS share, please"
258                    " see the FAQ entry about characters being "
259                    "transformed to a '?'")
260            self.extended_filenames = 1
261
262    def set_win_reserved_filenames(self, subdir):
263        """Set self.win_reserved_filenames by trying to write a path"""
264        assert not self.read_only
265
266        # Try Windows reserved characters
267        win_reserved_filename = ':\\"'
268        win_rp = None
269        try:
270            win_rp = subdir.append(win_reserved_filename)
271            win_rp.touch()
272        except (IOError, OSError):
273            if win_rp:
274                assert not win_rp.lstat()
275            self.win_reserved_filenames = 1
276        else:
277            assert win_rp.lstat()
278            try:
279                win_rp.delete()
280            except (IOError, OSError):
281                self.win_reserved_filenames = 1
282            else:
283                self.win_reserved_filenames = 0
284
285    def set_acls(self, rp):
286        """Set self.acls based on rp.  Does not write.  Needs to be local"""
287        assert Globals.local_connection is rp.conn
288        assert rp.lstat()
289        if Globals.acls_active == 0:
290            log.Log(
291                "POSIX ACLs test skipped. rdiff-backup run "
292                "with --no-acls option.", 4)
293            self.acls = 0
294            return
295
296        try:
297            import posix1e
298        except ImportError:
299            log.Log(
300                "Unable to import module posix1e from pylibacl "
301                "package.\nPOSIX ACLs not supported on filesystem at %s" %
302                rp.get_safepath(), 4)
303            self.acls = 0
304            return
305
306        try:
307            posix1e.ACL(file=rp.path)
308        except IOError:
309            log.Log(
310                "POSIX ACLs not supported by filesystem at %s" %
311                rp.get_safepath(), 4)
312            self.acls = 0
313        else:
314            self.acls = 1
315
316    def set_case_sensitive_readwrite(self, subdir):
317        """Determine if directory at rp is case sensitive by writing"""
318        assert not self.read_only
319        upper_a = subdir.append("A")
320        upper_a.touch()
321        lower_a = subdir.append("a")
322        if lower_a.lstat():
323            lower_a.delete()
324            upper_a.setdata()
325            if upper_a.lstat():
326                # we know that (fuse-)exFAT 1.3.0 takes 1sec to register the
327                # deletion (July 2020)
328                log.Log.FatalError(
329                    "We're sorry but the target file system at '%s' isn't "
330                    "deemed reliable enough for a backup. It takes too long "
331                    "or doesn't register case insensitive deletion of files."
332                    % subdir.get_safepath())
333            self.case_sensitive = 0
334        else:
335            upper_a.delete()
336            self.case_sensitive = 1
337
338    def set_case_sensitive_readonly(self, rp):
339        """Determine if directory at rp is case sensitive without writing"""
340
341        def find_letter(subdir):
342            """Find a (subdir_rp, dirlist) with a letter in it, or None
343
344            Recurse down the directory, looking for any file that has
345            a letter in it.  Return the pair (rp, [list of filenames])
346            where the list is of the directory containing rp.
347
348            """
349            files_list = robust.listrp(subdir)
350            for filename in files_list:
351                if filename != filename.swapcase():
352                    return (subdir, files_list, filename)
353            for filename in files_list:
354                dir_rp = subdir.append(filename)
355                if dir_rp.isdir():
356                    subsearch = find_letter(dir_rp)
357                    if subsearch:
358                        return subsearch
359            return None
360
361        def test_triple(dir_rp, dirlist, filename):
362            """Return 1 if filename shows system case sensitive"""
363            try:
364                letter_rp = dir_rp.append(filename)
365            except OSError:
366                return 0
367            assert letter_rp.lstat(), letter_rp
368            swapped = filename.swapcase()
369            if swapped in dirlist:
370                return 1
371
372            swapped_rp = dir_rp.append(swapped)
373            if swapped_rp.lstat():
374                return 0
375            return 1
376
377        triple = find_letter(rp)
378        if not triple:
379            log.Log(
380                "Warning: could not determine case sensitivity of "
381                "source directory at\n  %s\n"
382                "because we can't find any files with letters in them.\n"
383                "It will be treated as case sensitive." % rp.get_safepath(), 2)
384            self.case_sensitive = 1
385            return
386
387        self.case_sensitive = test_triple(*triple)
388
389    def set_eas(self, rp, write):
390        """Set extended attributes from rp. Tests writing if write is true."""
391        assert Globals.local_connection is rp.conn
392        assert rp.lstat()
393        if Globals.eas_active == 0:
394            log.Log(
395                "Extended attributes test skipped. rdiff-backup run "
396                "with --no-eas option.", 4)
397            self.eas = 0
398            return
399        try:
400            import xattr.pyxattr_compat as xattr
401        except ImportError:
402            try:
403                import xattr
404            except ImportError:
405                log.Log(
406                    "Unable to import module (py)xattr.\nExtended attributes not "
407                    "supported on filesystem at %s" % (rp.get_safepath(), ), 4)
408                self.eas = 0
409                return
410
411        try:
412            xattr.list(rp.path)
413            if write:
414                xattr.set(rp.path, b"user.test", b"test val")
415                assert xattr.get(rp.path, b"user.test") == b"test val"
416        except IOError:
417            log.Log(
418                "Extended attributes not supported by "
419                "filesystem at %s" % (rp.get_safepath(), ), 4)
420            self.eas = 0
421        except AssertionError:
422            log.Log(
423                "Extended attributes support is broken on filesystem at "
424                "%s.\nPlease upgrade the filesystem driver, contact the "
425                "developers,\nor use the --no-eas option to disable "
426                "extended attributes\nsupport and suppress this message." %
427                (rp.get_safepath(), ), 1)
428            self.eas = 0
429        else:
430            self.eas = 1
431
432    def set_win_acls(self, dir_rp, write):
433        """Test if windows access control lists are supported"""
434        assert Globals.local_connection is dir_rp.conn
435        assert dir_rp.lstat()
436        if Globals.win_acls_active == 0:
437            log.Log(
438                "Windows ACLs test skipped. rdiff-backup run "
439                "with --no-acls option.", 4)
440            self.win_acls = 0
441            return
442
443        try:
444            import win32security
445            import pywintypes
446        except ImportError:
447            log.Log(
448                "Unable to import win32security module. Windows ACLs\n"
449                "not supported by filesystem at %s" % dir_rp.get_safepath(), 4)
450            self.win_acls = 0
451            return
452        try:
453            sd = win32security.GetNamedSecurityInfo(
454                os.fsdecode(dir_rp.path), win32security.SE_FILE_OBJECT,
455                win32security.OWNER_SECURITY_INFORMATION
456                | win32security.GROUP_SECURITY_INFORMATION
457                | win32security.DACL_SECURITY_INFORMATION)
458            acl = sd.GetSecurityDescriptorDacl()
459            acl.GetAceCount()  # to verify that it works
460            if write:
461                win32security.SetNamedSecurityInfo(
462                    os.fsdecode(dir_rp.path), win32security.SE_FILE_OBJECT,
463                    win32security.OWNER_SECURITY_INFORMATION
464                    | win32security.GROUP_SECURITY_INFORMATION
465                    | win32security.DACL_SECURITY_INFORMATION,
466                    sd.GetSecurityDescriptorOwner(),
467                    sd.GetSecurityDescriptorGroup(),
468                    sd.GetSecurityDescriptorDacl(), None)
469        except (OSError, AttributeError, pywintypes.error):
470            log.Log(
471                "Unable to load a Windows ACL.\nWindows ACLs not supported "
472                "by filesystem at %s" % dir_rp.get_safepath(), 4)
473            self.win_acls = 0
474            return
475
476        try:
477            win_acls.init_acls()
478        except (OSError, AttributeError, pywintypes.error):
479            log.Log(
480                "Unable to init win_acls.\nWindows ACLs not supported by "
481                "filesystem at %s" % dir_rp.get_safepath(), 4)
482            self.win_acls = 0
483            return
484        self.win_acls = 1
485
486    def set_dir_inc_perms(self, rp):
487        """See if increments can have full permissions like a directory"""
488        test_rp = rp.append('dir_inc_check')
489        test_rp.touch()
490        try:
491            test_rp.chmod(0o7777, 4)
492        except OSError:
493            test_rp.delete()
494            self.dir_inc_perms = 0
495            return
496        test_rp.setdata()
497        assert test_rp.isreg()
498        if test_rp.getperms() == 0o7777 or test_rp.getperms() == 0o6777:
499            self.dir_inc_perms = 1
500        else:
501            self.dir_inc_perms = 0
502        test_rp.delete()
503
504    def set_carbonfile(self):
505        """Test for support of the Mac Carbon library.  This library
506        can be used to obtain Finder info (creator/type)."""
507        try:
508            import Carbon.File
509        except (ImportError, AttributeError):
510            self.carbonfile = 0
511            return
512
513        try:
514            Carbon.File.FSSpec('.')  # just to verify that it works
515        except BaseException:
516            self.carbonfile = 0
517            return
518
519        self.carbonfile = 1
520
521    def set_resource_fork_readwrite(self, dir_rp):
522        """Test for resource forks by writing to regular_file/..namedfork/rsrc"""
523        assert dir_rp.conn is Globals.local_connection
524        reg_rp = dir_rp.append('regfile')
525        reg_rp.touch()
526
527        s = b'test string---this should end up in resource fork'
528        try:
529            fp_write = open(
530                os.path.join(reg_rp.path, b'..namedfork', b'rsrc'), 'wb')
531            fp_write.write(s)
532            assert not fp_write.close()
533
534            fp_read = open(
535                os.path.join(reg_rp.path, b'..namedfork', b'rsrc'), 'rb')
536            s_back = fp_read.read()
537            assert not fp_read.close()
538        except (OSError, IOError):
539            self.resource_forks = 0
540        else:
541            self.resource_forks = (s_back == s)
542        reg_rp.delete()
543
544    def set_resource_fork_readonly(self, dir_rp):
545        """Test for resource fork support by testing an regular file
546
547        Launches search for regular file in given directory.  If no
548        regular file is found, resource_fork support will be turned
549        off by default.
550
551        """
552        for rp in selection.Select(dir_rp).set_iter():
553            if rp.isreg():
554                try:
555                    rfork = rp.append(b'..namedfork', b'rsrc')
556                    fp = rfork.open('rb')
557                    fp.read()
558                    assert not fp.close()
559                except (OSError, IOError):
560                    self.resource_forks = 0
561                    return
562                self.resource_forks = 1
563                return
564        self.resource_forks = 0
565
566    def set_high_perms_readwrite(self, dir_rp):
567        """Test for writing high-bit permissions like suid"""
568        tmpf_rp = dir_rp.append(b"high_perms_file")
569        tmpf_rp.touch()
570        tmpd_rp = dir_rp.append(b"high_perms_dir")
571        tmpd_rp.touch()
572        try:
573            tmpf_rp.chmod(0o7000, 4)
574            tmpf_rp.chmod(0o7777, 4)
575            tmpd_rp.chmod(0o7000, 4)
576            tmpd_rp.chmod(0o7777, 4)
577        except (OSError, IOError):
578            self.high_perms = 0
579        else:
580            self.high_perms = 1
581        tmpf_rp.delete()
582        tmpd_rp.delete()
583
584    def set_symlink_perms(self, dir_rp):
585        """Test if symlink permissions are affected by umask"""
586        sym_source = dir_rp.append(b"symlinked_file1")
587        sym_source.touch()
588        sym_dest = dir_rp.append(b"symlinked_file2")
589        try:
590            sym_dest.symlink(b"symlinked_file1")
591        except (OSError, AttributeError):
592            self.symlink_perms = 0
593        else:
594            sym_dest.setdata()
595            assert sym_dest.issym()
596            if sym_dest.getperms() == 0o700:
597                self.symlink_perms = 1
598            else:
599                self.symlink_perms = 0
600            sym_dest.delete()
601        sym_source.delete()
602
603    def set_escape_dos_devices(self, subdir):
604        """Test if DOS device files can be used as filenames.
605
606        This test must detect if the underlying OS is Windows, whether we are
607        running under Cygwin or natively. Cygwin allows these special files to
608        be stat'd from any directory. Native Windows returns OSError (like
609        non-Cygwin POSIX), but we can check for that using os.name.
610
611        Note that 'con' and 'aux' have some unusual behaviors as shown below.
612
613        os.lstat()   |  con         aux         prn
614        -------------+-------------------------------------
615        Unix         |  OSError,2   OSError,2   OSError,2
616        Cygwin/NTFS  |  -success-   -success-   -success-
617        Cygwin/FAT32 |  -success-   -HANGS-
618        Native Win   |  WinError,2  WinError,87 WinError,87
619        """
620        if os.name == "nt":
621            self.escape_dos_devices = 1
622            return
623
624        try:
625            device_rp = subdir.append(b"con")
626            if device_rp.lstat():
627                self.escape_dos_devices = 1
628            else:
629                self.escape_dos_devices = 0
630        except (OSError):
631            self.escape_dos_devices = 1
632
633    def set_escape_trailing_spaces_readwrite(self, testdir):
634        """
635        Windows and Linux/FAT32 will not preserve trailing spaces or periods.
636        Linux/FAT32 behaves inconsistently: It will give an OSError,22 if
637        os.mkdir() is called on a directory name with a space at the end, but
638        will give an IOError("invalid mode") if you attempt to create a filename
639        with a space at the end. However, if a period is placed at the end of
640        the name, Linux/FAT32 is consistent with Cygwin and Native Windows.
641        """
642
643        period_rp = testdir.append("foo.")
644        assert not period_rp.lstat()
645
646        tmp_rp = testdir.append("foo")
647        tmp_rp.touch()
648        assert tmp_rp.lstat()
649
650        period_rp.setdata()
651        if period_rp.lstat():
652            self.escape_trailing_spaces = 1
653        else:
654            self.escape_trailing_spaces = 0
655
656        tmp_rp.delete()
657
658    def set_escape_trailing_spaces_readonly(self, rp):
659        """Determine if directory at rp permits filenames with trailing
660        spaces or periods without writing."""
661
662        def test_period(dir_rp, dirlist):
663            """Return 1 if trailing spaces and periods should be escaped"""
664            filename = dirlist[0]
665            try:
666                test_rp = dir_rp.append(filename)
667            except OSError:
668                return 0
669            assert test_rp.lstat(), test_rp
670            period = filename + b'.'
671            if period in dirlist:
672                return 0
673
674            return 0  # FIXME the following lines fail if filename is almost too long
675            period_rp = dir_rp.append(period)
676            if period_rp.lstat():
677                return 1
678            return 0
679
680        dirlist = robust.listrp(rp)
681        if len(dirlist):
682            self.escape_trailing_spaces = test_period(rp, dirlist)
683        else:
684            log.Log(
685                "Warning: could not determine if source directory at\n"
686                "  %s\npermits trailing spaces or periods in "
687                "filenames because we can't find any files.\n"
688                "It will be treated as permitting such files." %
689                rp.get_safepath(), 2)
690            self.escape_trailing_spaces = 0
691
692
693def get_readonly_fsa(desc_string, rp):
694    """Return an fsa with given description_string
695
696    Will be initialized read_only with given RPath rp.  We separate
697    this out into a separate function so the request can be vetted by
698    the security module.
699
700    """
701    if os.name == 'nt':
702        log.Log("Hardlinks disabled by default on Windows", 4)
703        SetConnections.UpdateGlobal('preserve_hardlinks', 0)
704    return FSAbilities(desc_string).init_readonly(rp)
705
706
707class SetGlobals:
708    """Various functions for setting Globals vars given FSAbilities above
709
710    Container for BackupSetGlobals and RestoreSetGlobals (don't use directly)
711
712    """
713
714    def __init__(self, in_conn, out_conn, src_fsa, dest_fsa):
715        """Just store some variables for use below"""
716        self.in_conn, self.out_conn = in_conn, out_conn
717        self.src_fsa, self.dest_fsa = src_fsa, dest_fsa
718
719    def set_eas(self):
720        self.update_triple(self.src_fsa.eas, self.dest_fsa.eas,
721                           ('eas_active', 'eas_write', 'eas_conn'))
722
723    def set_acls(self):
724        self.update_triple(self.src_fsa.acls, self.dest_fsa.acls,
725                           ('acls_active', 'acls_write', 'acls_conn'))
726        if Globals.never_drop_acls and not Globals.acls_active:
727            log.Log.FatalError("--never-drop-acls specified, but ACL support\n"
728                               "missing from source filesystem")
729
730    def set_win_acls(self):
731        self.update_triple(
732            self.src_fsa.win_acls, self.dest_fsa.win_acls,
733            ('win_acls_active', 'win_acls_write', 'win_acls_conn'))
734
735    def set_resource_forks(self):
736        self.update_triple(self.src_fsa.resource_forks,
737                           self.dest_fsa.resource_forks,
738                           ('resource_forks_active', 'resource_forks_write',
739                            'resource_forks_conn'))
740
741    def set_carbonfile(self):
742        self.update_triple(
743            self.src_fsa.carbonfile, self.dest_fsa.carbonfile,
744            ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn'))
745
746    def set_hardlinks(self):
747        if Globals.preserve_hardlinks != 0:
748            SetConnections.UpdateGlobal('preserve_hardlinks',
749                                        self.dest_fsa.hardlinks)
750
751    def set_fsync_directories(self):
752        SetConnections.UpdateGlobal('fsync_directories',
753                                    self.dest_fsa.fsync_dirs)
754
755    def set_change_ownership(self):
756        SetConnections.UpdateGlobal('change_ownership',
757                                    self.dest_fsa.ownership)
758
759    def set_high_perms(self):
760        if not self.dest_fsa.high_perms:
761            SetConnections.UpdateGlobal('permission_mask', 0o777)
762
763    def set_symlink_perms(self):
764        SetConnections.UpdateGlobal('symlink_perms',
765                                    self.dest_fsa.symlink_perms)
766
767    def set_compatible_timestamps(self):
768        if Globals.chars_to_quote.find(b":") > -1:
769            SetConnections.UpdateGlobal('use_compatible_timestamps', 1)
770            Time.setcurtime(
771                Time.curtime)  # update Time.curtimestr on all conns
772            log.Log("Enabled use_compatible_timestamps", 4)
773
774
775class BackupSetGlobals(SetGlobals):
776    """Functions for setting fsa related globals for backup session"""
777
778    def update_triple(self, src_support, dest_support, attr_triple):
779        """Many of the settings have a common form we can handle here"""
780        active_attr, write_attr, conn_attr = attr_triple
781        if Globals.get(active_attr) == 0:
782            return  # don't override 0
783        for attr in attr_triple:
784            SetConnections.UpdateGlobal(attr, None)
785        if not src_support:
786            return  # if source doesn't support, nothing
787        SetConnections.UpdateGlobal(active_attr, 1)
788        self.in_conn.Globals.set_local(conn_attr, 1)
789        if dest_support:
790            SetConnections.UpdateGlobal(write_attr, 1)
791            self.out_conn.Globals.set_local(conn_attr, 1)
792
793    def set_special_escapes(self, rbdir):
794        """Escaping DOS devices and trailing periods/spaces works like
795        regular filename escaping. If only the destination requires it,
796        then we do it. Otherwise, it is not necessary, since the files
797        couldn't have been created in the first place. We also record
798        whether we have done it in order to handle the case where a
799        volume which was escaped is later restored by an OS that does
800        not require it.
801
802        """
803
804        suggested_edd = (self.dest_fsa.escape_dos_devices
805                         and not self.src_fsa.escape_dos_devices)
806        suggested_ets = (self.dest_fsa.escape_trailing_spaces
807                         and not self.src_fsa.escape_trailing_spaces)
808
809        se_rp = rbdir.append("special_escapes")
810        if not se_rp.lstat():
811            actual_edd, actual_ets = suggested_edd, suggested_ets
812            se = ""
813            if actual_edd:
814                se = se + "escape_dos_devices\n"
815            if actual_ets:
816                se = se + "escape_trailing_spaces\n"
817            se_rp.write_string(se)
818        else:
819            se = se_rp.get_string().split("\n")
820            actual_edd = ("escape_dos_devices" in se)
821            actual_ets = ("escape_trailing_spaces" in se)
822
823            if actual_edd != suggested_edd and not suggested_edd:
824                log.Log(
825                    "Warning: System no longer needs DOS devices escaped, "
826                    "but we will retain for backwards compatibility.", 2)
827            if actual_ets != suggested_ets and not suggested_ets:
828                log.Log(
829                    "Warning: System no longer needs trailing spaces or "
830                    "periods escaped, but we will retain for backwards "
831                    "compatibility.", 2)
832
833        SetConnections.UpdateGlobal('escape_dos_devices', actual_edd)
834        log.Log("Backup: escape_dos_devices = %d" % actual_edd, 4)
835
836        SetConnections.UpdateGlobal('escape_trailing_spaces', actual_ets)
837        log.Log("Backup: escape_trailing_spaces = %d" % actual_ets, 4)
838
839    def set_chars_to_quote(self, rbdir, force):
840        """Set chars_to_quote setting for backup session
841
842        Unlike most other options, the chars_to_quote setting also
843        depends on the current settings in the rdiff-backup-data
844        directory, not just the current fs features.
845
846        """
847        (ctq, update) = self.compare_ctq_file(rbdir, self.get_ctq_from_fsas(),
848                                              force)
849
850        SetConnections.UpdateGlobal('chars_to_quote', ctq)
851        if Globals.chars_to_quote:
852            FilenameMapping.set_init_quote_vals()
853        return update
854
855    def get_ctq_from_fsas(self):
856        """Determine chars_to_quote just from filesystems, no ctq file"""
857        ctq = []
858
859        if self.src_fsa.case_sensitive and not self.dest_fsa.case_sensitive:
860            ctq.append(b"A-Z")  # Quote upper case
861        if not self.dest_fsa.extended_filenames:
862            ctq.append(b'\000-\037')  # Quote 0 - 31
863            ctq.append(b'\200-\377')  # Quote non-ASCII characters 0x80 - 0xFF
864        if self.dest_fsa.win_reserved_filenames:
865            if self.dest_fsa.extended_filenames:
866                ctq.append(b'\000-\037')  # Quote 0 - 31
867            # Quote ", *, /, :, <, >, ?, \, |, and 127 (DEL)
868            ctq.append(b'\"*/:<>?\\\\|\177')
869
870        # Quote quoting char if quoting anything
871        if ctq:
872            ctq.append(Globals.quoting_char)
873        return b"".join(ctq)
874
875    def compare_ctq_file(self, rbdir, suggested_ctq, force):
876        """Compare ctq file with suggested result, return actual ctq"""
877        ctq_rp = rbdir.append(b"chars_to_quote")
878        if not ctq_rp.lstat():
879            if Globals.chars_to_quote is None:
880                actual_ctq = suggested_ctq
881            else:
882                actual_ctq = Globals.chars_to_quote
883            ctq_rp.write_bytes(actual_ctq)
884            return (actual_ctq, None)
885
886        if Globals.chars_to_quote is None:
887            actual_ctq = ctq_rp.get_bytes()
888        else:
889            actual_ctq = Globals.chars_to_quote  # Globals override
890
891        if actual_ctq == suggested_ctq:
892            return (actual_ctq, None)
893        if suggested_ctq == b"":
894            log.Log(
895                "Warning: File system no longer needs quoting, "
896                "but we will retain for backwards compatibility.", 2)
897            return (actual_ctq, None)
898        if Globals.chars_to_quote is None:
899            if force:
900                log.Log(
901                    "Warning: migrating rdiff-backup repository from"
902                    "old quoting chars %r to new quoting chars %r" %
903                    (actual_ctq, suggested_ctq), 2)
904                ctq_rp.delete()
905                ctq_rp.write_bytes(suggested_ctq)
906                return (suggested_ctq, 1)
907            else:
908                log.Log.FatalError(
909                    """New quoting requirements!
910
911The quoting chars this session needs %r do not match
912the repository settings %r listed in
913
914%s
915
916This may be caused when you copy an rdiff-backup repository from a
917normal file system onto a windows one that cannot support the same
918characters, or if you backup a case-sensitive file system onto a
919case-insensitive one that previously only had case-insensitive ones
920backed up onto it.
921
922By specifying the --force option, rdiff-backup will migrate the
923repository from the old quoting chars to the new ones.""" %
924                    (suggested_ctq, actual_ctq, ctq_rp.get_safepath()))
925        return (actual_ctq, None)  # Maintain Globals override
926
927
928class RestoreSetGlobals(SetGlobals):
929    """Functions for setting fsa-related globals for restore session"""
930
931    def update_triple(self, src_support, dest_support, attr_triple):
932        """Update global settings for feature based on fsa results
933
934        This is slightly different from BackupSetGlobals.update_triple
935        because (using the mirror_metadata file) rpaths from the
936        source may have more information than the file system
937        supports.
938
939        """
940        active_attr, write_attr, conn_attr = attr_triple
941        if Globals.get(active_attr) == 0:
942            return  # don't override 0
943        for attr in attr_triple:
944            SetConnections.UpdateGlobal(attr, None)
945        if not dest_support:
946            return  # if dest doesn't support, do nothing
947        SetConnections.UpdateGlobal(active_attr, 1)
948        self.out_conn.Globals.set_local(conn_attr, 1)
949        self.out_conn.Globals.set_local(write_attr, 1)
950        if src_support:
951            self.in_conn.Globals.set_local(conn_attr, 1)
952
953    def set_special_escapes(self, rbdir):
954        """Set escape_dos_devices and escape_trailing_spaces from
955        rdiff-backup-data dir, just like chars_to_quote"""
956        se_rp = rbdir.append("special_escapes")
957        if se_rp.lstat():
958            se = se_rp.get_string().split("\n")
959            actual_edd = ("escape_dos_devices" in se)
960            actual_ets = ("escape_trailing_spaces" in se)
961        else:
962            log.Log(
963                "Warning: special_escapes file not found,\n"
964                "will assume need to escape DOS devices and trailing "
965                "spaces based on file systems.", 2)
966            if getattr(self, "src_fsa", None) is not None:
967                actual_edd = (self.src_fsa.escape_dos_devices
968                              and not self.dest_fsa.escape_dos_devices)
969                actual_ets = (self.src_fsa.escape_trailing_spaces
970                              and not self.dest_fsa.escape_trailing_spaces)
971            else:
972                # Single filesystem operation
973                actual_edd = self.dest_fsa.escape_dos_devices
974                actual_ets = self.dest_fsa.escape_trailing_spaces
975
976        SetConnections.UpdateGlobal('escape_dos_devices', actual_edd)
977        log.Log("Backup: escape_dos_devices = %d" % actual_edd, 4)
978
979        SetConnections.UpdateGlobal('escape_trailing_spaces', actual_ets)
980        log.Log("Backup: escape_trailing_spaces = %d" % actual_ets, 4)
981
982    def set_chars_to_quote(self, rbdir):
983        """Set chars_to_quote from rdiff-backup-data dir"""
984        if Globals.chars_to_quote is not None:
985            return  # already overridden
986
987        ctq_rp = rbdir.append(b"chars_to_quote")
988        if ctq_rp.lstat():
989            SetConnections.UpdateGlobal("chars_to_quote", ctq_rp.get_bytes())
990        else:
991            log.Log(
992                "Warning: chars_to_quote file not found,\n"
993                "assuming no quoting in backup repository.", 2)
994            SetConnections.UpdateGlobal("chars_to_quote", b"")
995
996
997class SingleSetGlobals(RestoreSetGlobals):
998    """For setting globals when dealing only with one filesystem"""
999
1000    def __init__(self, conn, fsa):
1001        self.conn = conn
1002        self.dest_fsa = fsa
1003
1004    def update_triple(self, fsa_support, attr_triple):
1005        """Update global vars from single fsa test"""
1006        active_attr, write_attr, conn_attr = attr_triple
1007        if Globals.get(active_attr) == 0:
1008            return  # don't override 0
1009        for attr in attr_triple:
1010            SetConnections.UpdateGlobal(attr, None)
1011        if not fsa_support:
1012            return
1013        SetConnections.UpdateGlobal(active_attr, 1)
1014        SetConnections.UpdateGlobal(write_attr, 1)
1015        self.conn.Globals.set_local(conn_attr, 1)
1016
1017    def set_eas(self):
1018        self.update_triple(self.dest_fsa.eas,
1019                           ('eas_active', 'eas_write', 'eas_conn'))
1020
1021    def set_acls(self):
1022        self.update_triple(self.dest_fsa.acls,
1023                           ('acls_active', 'acls_write', 'acls_conn'))
1024
1025    def set_win_acls(self):
1026        self.update_triple(
1027            self.src_fsa.win_acls, self.dest_fsa.win_acls,
1028            ('win_acls_active', 'win_acls_write', 'win_acls_conn'))
1029
1030    def set_resource_forks(self):
1031        self.update_triple(self.dest_fsa.resource_forks,
1032                           ('resource_forks_active', 'resource_forks_write',
1033                            'resource_forks_conn'))
1034
1035    def set_carbonfile(self):
1036        self.update_triple(
1037            self.dest_fsa.carbonfile,
1038            ('carbonfile_active', 'carbonfile_write', 'carbonfile_conn'))
1039
1040
1041def backup_set_globals(rpin, force):
1042    """Given rps for source filesystem and repository, set fsa globals
1043
1044    This should be run on the destination connection, because we may
1045    need to write a new chars_to_quote file.
1046
1047    """
1048    assert Globals.rbdir.conn is Globals.local_connection
1049    src_fsa = rpin.conn.fs_abilities.get_readonly_fsa('source', rpin)
1050    log.Log(str(src_fsa), 4)
1051    dest_fsa = FSAbilities('destination').init_readwrite(Globals.rbdir)
1052    log.Log(str(dest_fsa), 4)
1053
1054    bsg = BackupSetGlobals(rpin.conn, Globals.rbdir.conn, src_fsa, dest_fsa)
1055    bsg.set_eas()
1056    bsg.set_acls()
1057    bsg.set_win_acls()
1058    bsg.set_resource_forks()
1059    bsg.set_carbonfile()
1060    bsg.set_hardlinks()
1061    bsg.set_fsync_directories()
1062    bsg.set_change_ownership()
1063    bsg.set_high_perms()
1064    bsg.set_symlink_perms()
1065    update_quoting = bsg.set_chars_to_quote(Globals.rbdir, force)
1066    bsg.set_special_escapes(Globals.rbdir)
1067    bsg.set_compatible_timestamps()
1068
1069    if update_quoting and force:
1070        FilenameMapping.update_quoting(Globals.rbdir)
1071
1072
1073def restore_set_globals(rpout):
1074    """Set fsa related globals for restore session, given in/out rps"""
1075    assert rpout.conn is Globals.local_connection
1076    src_fsa = Globals.rbdir.conn.fs_abilities.get_readonly_fsa(
1077        'rdiff-backup repository', Globals.rbdir)
1078    log.Log(str(src_fsa), 4)
1079    dest_fsa = FSAbilities('restore target').init_readwrite(rpout)
1080    log.Log(str(dest_fsa), 4)
1081
1082    rsg = RestoreSetGlobals(Globals.rbdir.conn, rpout.conn, src_fsa, dest_fsa)
1083    rsg.set_eas()
1084    rsg.set_acls()
1085    rsg.set_win_acls()
1086    rsg.set_resource_forks()
1087    rsg.set_carbonfile()
1088    rsg.set_hardlinks()
1089    # No need to fsync anything when restoring
1090    rsg.set_change_ownership()
1091    rsg.set_high_perms()
1092    rsg.set_symlink_perms()
1093    rsg.set_chars_to_quote(Globals.rbdir)
1094    rsg.set_special_escapes(Globals.rbdir)
1095    rsg.set_compatible_timestamps()
1096
1097
1098def single_set_globals(rp, read_only=None):
1099    """Set fsa related globals for operation on single filesystem"""
1100    if read_only:
1101        fsa = rp.conn.fs_abilities.get_readonly_fsa(rp.path, rp)
1102    else:
1103        fsa = FSAbilities(rp.path).init_readwrite(rp)
1104    log.Log(str(fsa), 4)
1105
1106    ssg = SingleSetGlobals(rp.conn, fsa)
1107    ssg.set_eas()
1108    ssg.set_acls()
1109    ssg.set_resource_forks()
1110    ssg.set_carbonfile()
1111    if not read_only:
1112        ssg.set_hardlinks()
1113        ssg.set_change_ownership()
1114        ssg.set_high_perms()
1115        ssg.set_symlink_perms()
1116    ssg.set_chars_to_quote(Globals.rbdir)
1117    ssg.set_special_escapes(Globals.rbdir)
1118    ssg.set_compatible_timestamps()
1119