aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common/tests/test_paths.py
blob: 223a2a65b5547dac33c434ce11a2bcf13caff7ff (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""
Name rules that have to give the same answer on every platform.

Most people share from an external drive formatted exFAT or NTFS, on Windows, so
these are not compatibility niceties — they decide whether two index entries are
one file, and whether a member can save what they downloaded.
"""

import pytest
from meshbay_common.paths import (
    find_fold_collisions,
    fold,
    fold_path,
    is_portable_name,
    nfc,
    portable_name_problem,
    sanitize_for_download,
)

# ── Same file or not ─────────────────────────────────────────────────────────

def test_case_differences_fold_together():
    assert fold("README.TXT") == fold("readme.txt")


def test_folding_is_not_lowercasing():
    """`casefold` handles what `lower()` misses, and those are the pairs that
    arrive as a bug report rather than a test failure."""
    assert fold("STRASSE") == fold("straße")
    assert "STRASSE".lower() != "straße".lower()


def test_the_two_unicode_spellings_of_an_accent_are_one_name():
    """
    `Café.mkv` written on macOS (NFD) and on Windows (NFC) are different byte
    strings naming the same file. For French filenames this is routine.
    """
    nfc_form = "Café.mkv"          # é precomposed
    nfd_form = "Café.mkv"         # e + combining acute
    assert nfc_form != nfd_form
    assert nfc(nfd_form) == nfc_form
    assert fold(nfd_form) == fold(nfc_form)


def test_distinct_names_stay_distinct():
    assert fold("film.mkv") != fold("film2.mkv")


def test_folding_a_path_keeps_its_separators():
    assert fold_path("Films/2024/A.MKV") == "films/2024/a.mkv"


def test_collisions_are_found_and_grouped():
    found = find_fold_collisions(
        ["README.txt", "readme.TXT", "notes.md", "Café.mkv", "Café.mkv"])
    groups = {frozenset(v) for v in found.values()}
    assert len(groups) == 2
    assert frozenset({"README.txt", "readme.TXT"}) in groups
    # The other pair is the two Unicode spellings of the same accented name,
    # which are different byte strings — so compare by size, not by literal.
    assert any(len(g) == 2 and all(f.endswith('.mkv') for f in g)
               for g in groups)
    assert all("notes.md" not in v for v in found.values())


def test_no_collision_means_no_report():
    assert find_fold_collisions(["a.txt", "b.txt"]) == {}


# ── What Windows refuses ─────────────────────────────────────────────────────

@pytest.mark.parametrize("name", ["CON", "PRN", "AUX", "NUL", "COM1", "LPT9",
                                  "aux", "Aux.txt", "COM3.tar.gz"])
def test_windows_reserved_names_are_flagged(name):
    """`AUX.txt` is as impossible as `AUX` — the extension does not help."""
    assert portable_name_problem(name) is not None


@pytest.mark.parametrize("name", ['a<b', 'a>b', 'a:b', 'a"b', "a/b", "a\\b",
                                  "a|b", "a?b", "a*b", "a\x00b", "a\tb"])
def test_reserved_characters_are_flagged(name):
    assert portable_name_problem(name) is not None


@pytest.mark.parametrize("name", ["trailing ", "trailing.", "  "])
def test_a_trailing_space_or_dot_is_flagged(name):
    """Windows strips them silently, so `file .` and `file` come back the same."""
    assert portable_name_problem(name) is not None


@pytest.mark.parametrize("name", ["film.mkv", "Café.mkv", "rapport (1).pdf",
                                  "été.txt", "COMET.txt", "AUXILIARY.doc",
                                  "日本語.txt"])
def test_ordinary_names_are_portable(name):
    assert is_portable_name(name), portable_name_problem(name)


def test_empty_and_dot_names_are_refused():
    assert portable_name_problem("") is not None
    assert portable_name_problem(".") is not None
    assert portable_name_problem("..") is not None


# ── Saving a file whose name the platform refuses ────────────────────────────

def test_a_portable_name_is_returned_unchanged():
    """Identity in the common case, so a caller can tell whether it renamed
    anything by comparing."""
    assert sanitize_for_download("film.mkv") == "film.mkv"
    assert sanitize_for_download("Café (2024).mkv") == "Café (2024).mkv"


def test_sanitizing_produces_something_writable():
    for hostile in ["a<b>c.txt", "AUX", "trailing .", "with/slash.txt", "COM1.log"]:
        cleaned = sanitize_for_download(hostile)
        assert is_portable_name(cleaned), f"{hostile!r} → {cleaned!r} still refused"


def test_sanitizing_never_returns_nothing():
    assert sanitize_for_download("...") not in ("", None)
    assert sanitize_for_download("???") not in ("", None)