diff --git a/tests/test_restore/cmd/test_restore_home_trash_on_separate_volume.py b/tests/test_restore/cmd/test_restore_home_trash_on_separate_volume.py new file mode 100644 index 00000000..64ad84cf --- /dev/null +++ b/tests/test_restore/cmd/test_restore_home_trash_on_separate_volume.py @@ -0,0 +1,56 @@ +from six import StringIO + +from trashcli.lib.my_input import HardCodedInput +from trashcli.restore.file_system import FakeReadCwd, FileReader +from trashcli.restore.restore_cmd import RestoreCmd +from trashcli.restore.trashed_files import TrashedFiles + +HOME = '/home/user' +INFO = '/home/user/.local/share/Trash/info/report.trashinfo' + + +class FakeReader(FileReader): + def __init__(self, contents): + self.contents = contents + + def contents_of(self, path): + return self.contents + + +class FakeSearcher: + # the home trash lives on the /home volume, not on "/" + def __init__(self, volume): + self.volume = volume + + def all_file_in_info_dir(self, trash_dir_from_cli): + from collections import namedtuple + InfoFile = namedtuple('InfoFile', 'path type volume') + yield InfoFile(INFO, 'trashinfo', self.volume) + + +class NullLogger: + def warning(self, message): + pass + + +class TestRestoreHomeTrashOnSeparateVolume: + def run_restore_from(self, path, trashinfo, volume): + stdout = StringIO() + trashed_files = TrashedFiles(NullLogger(), FakeReader(trashinfo), + FakeSearcher(volume)) + cmd = RestoreCmd.make( + stdout=stdout, stderr=StringIO(), exit=lambda _: None, + input=HardCodedInput(''), version="0.0.0", + trashed_files=trashed_files, read_fs=None, write_fs=None, + read_cwd=FakeReadCwd(path)) + cmd.run(['trash-restore', path]) + return stdout.getvalue() + + def test_an_absolute_home_entry_on_a_separate_home_volume_is_listed(self): + trashinfo = ('[Trash Info]\n' + 'Path=/home/user/report.txt\n' + 'DeletionDate=2026-01-01T00:00:00\n') + + output = self.run_restore_from(HOME, trashinfo, volume='/home') + + assert '/home/user/report.txt' in output diff --git a/tests/test_restore/cmd/test_restore_rejects_out_of_volume_path.py b/tests/test_restore/cmd/test_restore_rejects_out_of_volume_path.py index acf7b7d0..ad0375d3 100644 --- a/tests/test_restore/cmd/test_restore_rejects_out_of_volume_path.py +++ b/tests/test_restore/cmd/test_restore_rejects_out_of_volume_path.py @@ -59,10 +59,12 @@ def test_a_relative_path_inside_the_volume_is_restorable(self): self.assertEqual(['good.trashinfo'], self._restorable()) - def test_an_absolute_path_is_not_restorable(self): - self._add('evil', '/etc/passwd') + def test_an_absolute_path_is_restorable(self): + # the home trash stores absolute paths by design, and the volume + # that contains it is not necessarily "/" (e.g. a /home partition) + self._add('home-entry', '/home/user/document.txt') - self.assertEqual([], self._restorable()) + self.assertEqual(['home-entry.trashinfo'], self._restorable()) def test_a_path_escaping_the_volume_is_not_restorable(self): self._add('evil', '../../../etc/shadow') diff --git a/trashcli/parse_trashinfo/parse_original_location.py b/trashcli/parse_trashinfo/parse_original_location.py index 8b17947e..eb480d60 100644 --- a/trashcli/parse_trashinfo/parse_original_location.py +++ b/trashcli/parse_trashinfo/parse_original_location.py @@ -8,12 +8,11 @@ def parse_original_location(contents, volume_path): path = parse_path(contents) + if os.path.isabs(path): + # the home trash legitimately stores absolute paths, on any volume + return os.path.normpath(path) resolved = os.path.normpath(os.path.join(volume_path, path)) - if volume_path != os.path.sep: - # A volume trash must record a location that is relative to and inside that volume. - if os.path.isabs(path): - raise ParseError("Path= must be relative for volume trashes") - rel = os.path.relpath(resolved, os.path.normpath(volume_path)) - if rel == os.pardir or rel.startswith(os.pardir + os.sep): - raise ParseError("Path= escapes the volume root") + rel = os.path.relpath(resolved, os.path.normpath(volume_path)) + if rel == os.pardir or rel.startswith(os.pardir + os.sep): + raise ParseError("Path= escapes the volume root") return resolved