Skip to main content

pacenotes_shared/
lookup_key.rs

1/// Is this a usable game id or access directory name: `[a-z0-9_-]+`?
2///
3/// Both end up in a URL and on disk, and this alphabet can neither widen into
4/// a path nor collide case-insensitively on Windows.
5pub fn valid_id(id: &str) -> bool {
6    !id.is_empty()
7        && id
8            .bytes()
9            .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
10}
11
12/// Is this a usable `PackageInfo::name`: `/`-separated segments, none empty,
13/// none `.` or `..`, and no `\` or control character anywhere?
14///
15/// Both sides call it, because a manifest can come from a server the user does not run.
16pub fn valid_name(name: &str) -> bool {
17    if name.is_empty() || name.contains('\\') {
18        return false;
19    }
20    // A name becomes text the installer writes, such as `sounds=` in an ini,
21    // so a control character would let a server inject whole lines.
22    if name.chars().any(|c| c.is_ascii_control()) {
23        return false;
24    }
25    name.split('/')
26        .all(|segment| !segment.is_empty() && segment != "." && segment != "..")
27}
28
29#[cfg(test)]
30mod tests {
31    #[test]
32    fn valid_id_is_the_rule_for_games_and_directories() {
33        assert!(super::valid_id("public"));
34        assert!(super::valid_id("rbr"));
35        assert!(super::valid_id("premium-voices_2"));
36        assert!(!super::valid_id(""));
37        assert!(!super::valid_id("Public"));
38        assert!(!super::valid_id(".."));
39        assert!(!super::valid_id("a/b"));
40        assert!(!super::valid_id("a\\b"));
41        assert!(!super::valid_id("a.b"));
42    }
43
44    #[test]
45    fn a_name_can_never_widen_into_a_filesystem_path() {
46        for good in [
47            "basic",
48            "style/Descriptive",
49            "codriver/polish/male/Kamil",
50            "language/english",
51        ] {
52            assert!(super::valid_name(good), "{good:?} should be valid");
53        }
54        for bad in [
55            "",
56            "..",
57            "codriver/../../etc",
58            "codriver/./Kamil",
59            "/codriver/Kamil",
60            "codriver/Kamil/",
61            "codriver//Kamil",
62            "codriver\\polish",
63        ] {
64            assert!(!super::valid_name(bad), "{bad:?} should be rejected");
65        }
66    }
67
68    #[test]
69    fn a_name_can_never_carry_a_control_character() {
70        for bad in [
71            "codriver/Kamil\nmuteReplayPacenotes=1",
72            "codriver/Kamil\r\nenableGUI=0",
73            "codriver/\tKamil",
74            "codriver/Ka\0mil",
75            "codriver/Kamil\u{7f}",
76        ] {
77            assert!(!super::valid_name(bad), "{bad:?} should be rejected");
78        }
79        assert!(super::valid_name("style/Numeric Standard"));
80        assert!(super::valid_name("codriver/polish/male/MiƂosz"));
81    }
82}