diff --git a/install.py b/install.py index 35dc45189..664a57d62 100644 --- a/install.py +++ b/install.py @@ -4305,7 +4305,17 @@ def closed_folder(path: Path) -> tuple[Path, str] | None: except OSError as exc: if exc.errno in (errno.ENOENT, errno.ENOTDIR): return None - return folder.parent, exc.strerror or str(exc) + why = exc.strerror or str(exc) + if exc.errno == errno.ELOOP: + # The link that loops, not the open folder holding it. + return folder, why + real = Path(os.path.realpath(folder)) + if real != folder: + # ~/Emulation linked into a closed card: the closed folder + # is under the link's target, and naming the link's + # parent told the user they cannot open their own HOME. + return closed_folder(real) or (real.parent, why) + return folder.parent, why return probe, failure.strerror or str(failure) diff --git a/tests/test_install_experience.py b/tests/test_install_experience.py index 4cba5a56a..f396a8351 100644 --- a/tests/test_install_experience.py +++ b/tests/test_install_experience.py @@ -8,6 +8,7 @@ honours Retry-After, and large files that resume where they stopped. from __future__ import annotations import contextlib +import errno import hashlib import http.server import io @@ -978,5 +979,45 @@ class WindowsMandatoryLock(unittest.TestCase): self.assertEqual((ok, reason), (False, install.NOT_WRITABLE)) +@unittest.skipIf(os.name == "nt" or os.geteuid() == 0, + "a folder without search permission is POSIX") +class AClosedFolderBehindALink(unittest.TestCase): + """~/Emulation linked into a card at 000: the line named the link's + parent, telling the user they cannot open their own HOME. A link + that loops was named by its open parent.""" + + def setUp(self): + self.base = Path(tempfile.mkdtemp(dir=REPO_ROOT / "tmp")) + self.addCleanup(shutil.rmtree, self.base, True) + self.home = self.base / "home" + self.home.mkdir() + self.card = self.base / "card" + (self.card / "Emulation" / "bios").mkdir(parents=True) + + def closed(self, path: Path): + os.chmod(self.card, 0o000) + try: + return install.closed_folder(path) + finally: + os.chmod(self.card, 0o755) + + def test_a_link_into_the_closed_folder_names_that_folder(self): + (self.home / "Emulation").symlink_to(self.card / "Emulation") + self.assertEqual(self.closed(self.home / "Emulation" / "bios"), + (self.card, "Permission denied")) + + def test_a_link_to_the_closed_folder_itself_names_it(self): + (self.home / "Emulation").symlink_to(self.card) + self.assertEqual(self.closed(self.home / "Emulation" / "Emulation" / "bios"), + (self.card, "Permission denied")) + + def test_a_loop_names_the_link(self): + loop = self.base / "loop" + loop.symlink_to("loop") + folder, why = install.closed_folder(loop / "system") + self.assertEqual(folder, loop) + self.assertEqual(why, os.strerror(errno.ELOOP)) + + if __name__ == "__main__": unittest.main()