diff --git a/doc/manpages/qvm-backup-restore.rst b/doc/manpages/qvm-backup-restore.rst index 0407174..f3315f2 100644 --- a/doc/manpages/qvm-backup-restore.rst +++ b/doc/manpages/qvm-backup-restore.rst @@ -72,6 +72,13 @@ Options this limit and restore such (broken, or potentially malicious) backup anyway. +.. option:: --compression-filter, -Z + + Force specific compression filter, instead of the one named in the backup + header. The compression filter is a command that accepts ``-d`` option to + decompress data on stdin and output it to stdout. This can be used to + override built-in protection against uncommon compression. + .. option:: --dest-vm=APPVM, -d APPVM Restore from a backup located in a specific AppVM diff --git a/qubesadmin/backup/restore.py b/qubesadmin/backup/restore.py index 0bee08d..0a33c31 100644 --- a/qubesadmin/backup/restore.py +++ b/qubesadmin/backup/restore.py @@ -192,6 +192,11 @@ class BackupHeader(object): else: raise QubesException("Unrecognized header type") if not header.validator(value): + if key == 'compression-filter': + raise QubesException( + "Unusual compression filter '{f}' found. Use " + "--compression-filter={f} to use it anyway.".format( + f=value)) raise QubesException("Invalid value for header: {}".format(key)) setattr(self, header.field, value) @@ -902,7 +907,8 @@ class BackupRestore(object): self.subdir = subdir self.username = os.path.basename(subdir) - def __init__(self, app, backup_location, backup_vm, passphrase): + def __init__(self, app, backup_location, backup_vm, passphrase, + force_compression_filter=None): super(BackupRestore, self).__init__() #: qubes.Qubes instance @@ -919,6 +925,10 @@ class BackupRestore(object): #: backup path, inside VM pointed by :py:attr:`backup_vm` self.backup_location = backup_location + #: force using specific application for (de)compression, instead of + #: the one named in the backup header + self.force_compression_filter = force_compression_filter + #: passphrase protecting backup integrity and optionally decryption self.passphrase = passphrase @@ -1252,7 +1262,9 @@ class BackupRestore(object): "failed). Is the password correct?") filename = os.path.join(self.tmpdir, filename) with open(filename, 'rb') as f_header: - header_data = BackupHeader(f_header.read()) + header_data = BackupHeader( + f_header.read(), + compression_filter=self.force_compression_filter) os.unlink(filename) return header_data diff --git a/qubesadmin/tests/tools/qvm_backup_restore.py b/qubesadmin/tests/tools/qvm_backup_restore.py index 534fb10..b07304c 100644 --- a/qubesadmin/tests/tools/qvm_backup_restore.py +++ b/qubesadmin/tests/tools/qvm_backup_restore.py @@ -58,7 +58,8 @@ class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase): mock_handle_broken.assert_called_once_with( self.app, mock.ANY, mock_restore_info) mock_backup.assert_called_once_with( - self.app, '/some/path', None, 'testpass') + self.app, '/some/path', None, 'testpass', + force_compression_filter=None) self.assertAllCalled() @mock.patch('qubesadmin.tools.qvm_backup_restore.input', create=True) @@ -92,7 +93,8 @@ class TC_00_qvm_backup_restore(qubesadmin.tests.QubesTestCase): qubesadmin.tools.qvm_backup_restore.main(['/some/path', 'test-vm'], app=self.app) mock_backup.assert_called_once_with( - self.app, '/some/path', None, 'testpass') + self.app, '/some/path', None, 'testpass', + force_compression_filter=None) self.assertEqual(mock_backup.return_value.options.exclude, ['test-vm2']) self.assertAllCalled() diff --git a/qubesadmin/tools/qvm_backup_restore.py b/qubesadmin/tools/qvm_backup_restore.py index 4c7a1db..d09df4d 100644 --- a/qubesadmin/tools/qvm_backup_restore.py +++ b/qubesadmin/tools/qvm_backup_restore.py @@ -72,6 +72,11 @@ parser.add_argument("--ignore-size-limit", action="store_true", dest="ignore_size_limit", default=False, help="Ignore size limit calculated from backup metadata") +parser.add_argument("--compression-filter", "-Z", action="store", + dest="compression", + help="Force specific compression filter program, " + "instead of the one from the backup header") + parser.add_argument("-d", "--dest-vm", action="store", dest="appvm", help="Specify VM containing the backup to be restored") @@ -213,7 +218,8 @@ def main(args=None, app=None): try: backup = BackupRestore(args.app, args.backup_location, - appvm, passphrase) + appvm, passphrase, + force_compression_filter=args.compression) except qubesadmin.exc.QubesException as e: parser.error_runtime(str(e)) # unreachable - error_runtime will raise SystemExit