package pathrule import ( "errors" "fmt" "strings" "testing" ) // The generated tables match their canonical text: nobody edited them. func TestTablesDigest(t *testing.T) { if got := digest(); got != TablesDigest { t.Fatalf("digest of the tables %s, TablesDigest %s", got, TablesDigest) } if UnicodeVersion != "18.0.0" { t.Fatalf("UnicodeVersion %q", UnicodeVersion) } if len(Sources) != 19 || len(bestFitTables) != 15 { t.Fatalf("%d sources, %d best-fit tables", len(Sources), len(bestFitTables)) } } func TestProperties(t *testing.T) { for _, c := range []struct { r rune assigned, ignorable bool }{ {'a', true, false}, {0x0378, false, false}, // unassigned {0x00AD, true, true}, // soft hyphen {0x200D, true, true}, // ZWJ {0xFE0F, true, true}, // VS16 {0x206A, true, true}, // deprecated format character {0xE0041, true, true}, // tag A {0xE0000, false, true}, // unassigned, still ignorable {0x2028, true, false}, // line separator {0xFFFE, false, false}, // noncharacter {0xF03A, true, false}, // private use {0x10FFFF, false, false}, } { if Assigned(c.r) != c.assigned || DefaultIgnorable(c.r) != c.ignorable { t.Errorf("U+%04X: assigned %v ignorable %v, want %v %v", c.r, Assigned(c.r), DefaultIgnorable(c.r), c.assigned, c.ignorable) } } } func TestNFD(t *testing.T) { for _, c := range []struct{ in, want string }{ {"é", "é"}, {"Å", "Å"}, // ANGSTROM SIGN, a singleton decomposition {"K", "K"}, // KELVIN SIGN {"한", "한"}, // Hangul, with a final jamo {"하", "하"}, // Hangul, without {"ạ́", "ạ́"}, // canonical order {"ΐ", "ΐ"}, {"ǖ", "ǖ"}, // recursive decomposition {"á‍̣", "á‍̣"}, // ZWJ is a starter } { if got := NFD(c.in); got != c.want { t.Errorf("NFD(%+q) = %+q, want %+q", c.in, got, c.want) } } } func TestKey(t *testing.T) { for _, same := range [][2]string{ {"A.txt", "a.txt"}, {"Straße", "STRASSE"}, {"K", "k"}, {"ı", "i"}, {"é", "é"}, {"ab", "a‌b"}, {"á‍̣", "ạ́"}, {".DateKeys-x", ".datekeys-x"}, } { if Key(same[0]) != Key(same[1]) { t.Errorf("Key(%+q) = %+q, Key(%+q) = %+q", same[0], Key(same[0]), same[1], Key(same[1])) } } if Key("a") == Key("b") { t.Error("distinct keys expected") } } // rule is the rule of a pathrule error, "" for nil. func rule(err error) string { var e *Error if errors.As(err, &e) { return e.Rule } if err != nil { return "?" + err.Error() } return "" } func TestCheckPath(t *testing.T) { const ( zwj = "‍" zwnj = "‌" vs16 = "️" ) scotland := "\U0001F3F4\U000E0067\U000E0062\U000E0073\U000E0063\U000E0074\U000E007F" for _, c := range []struct { path, rule string }{ // Accepted. {"nota.txt", ""}, {"fotos/playa.jpg", ""}, {"¿Qué es esto.jpg", ""}, // bestfit1250 maps ¿ to '?', which R6c no longer rejects {"Para ti ♥.jpg", ""}, // bestfit874 maps ♥ to 0x03 {"§ 3 contrato.pdf", ""}, // bestfit874 maps § to 0x15 {"Madrid → Lisboa", ""}, // bestfit1253 maps → to '>' {"❤" + vs16 + ".txt", ""}, // ❤️ {"\U0001F3F3" + vs16 + zwj + "\U0001F308", ""}, // rainbow flag {"\U0001F468" + zwj + "\U0001F469" + zwj + "\U0001F467", ""}, {"ab" + zwnj + "c", ""}, {"a/.datekeys-x", ""}, // only the first segment is reserved {"ABCDEFGHI~1", ""}, // longer than 8 before the dot {"a~1b", ""}, {"report~2023.txt", ""}, {strings.Repeat("a", 255), ""}, {strings.Repeat("ΐ", 85), ""}, // 170 bytes, 255 UTF-16 units after NFD {" a ", ""}, // no table maps U+00A0 to U+0020... checked below // R2. {"/a", "R2"}, {"a//b", "R2"}, {"a/", "R2"}, {strings.Repeat("a/", 32) + "a", "R2"}, // R3. {"..", "R3"}, {"a/./b", "R3"}, {zwj, "R4b"}, // R3 sees an empty stripped segment? No: R3 first. {strings.Repeat("a", 256), "R3"}, {strings.Repeat("ΐ", 127), "R3"}, // 254 bytes, 381 UTF-16 units after NFD // R4. {"a:b", "R4"}, {"a\\b", "R4"}, {"a?b", "R4"}, {"a\tb", "R4"}, {"a
b", "R4"}, {"a­b", "R4"}, {"ab", "R4"}, {"a‮b", "R4"}, {"informeanexo", "R4"}, {"a͸b", "R4"}, {"a￾b", "R4"}, {scotland, "R4"}, {"\U0001F600\U000E0100", "R4"}, // VS17 // R4b. {"a" + vs16, "R4b"}, {zwj + "a", "R4b"}, {"a" + zwj, "R4b"}, {"a" + zwj + zwj + "b", "R4b"}, {"a" + zwnj + zwj + "b", "R4b"}, // R5. {" a", "R5"}, {"a ", "R5"}, {"a.", "R5"}, // R6. {"CON.txt", "R6"}, {"con", "R6"}, {"Aux .log", "R6"}, {"COM¹", "R6"}, {"lpt9.doc", "R6"}, {"CONIN$", "R6"}, // R6b. {"ABCDEF~1", "R6b"}, {"ABCDEF~1.TXT", "R6b"}, {"~1", "R6b"}, {"Ä~1.txt", "R6b"}, // R6c. {"CON.txt", "R6c"}, // full-width CON {"a∖b", "R6c"}, // SET MINUS {"a∶b", "R6c"}, // RATIO {" a", "R6c"}, // bestfit1252 maps U+3000 to U+0020, which breaks R5 {"a/b", "R6c"}, // full-width solidus {"..", "R6c"}, // full-width dots project to ".." // R10. {".datekeys-x", "R10"}, {".DateKeys-abc/b", "R10"}, } { got := rule(CheckPath(c.path)) if c.path == zwj { // R3 comes before R4b in a segment: the stripped segment is empty. if got != "R3" { t.Errorf("CheckPath(%+q): %s, want R3", c.path, got) } continue } if c.path == " a " { // Whatever the tables give, it must be R6c or nothing (R5 is // about U+0020 only). if got != "" && got != "R6c" { t.Errorf("CheckPath(%+q): %s", c.path, got) } continue } if got != c.rule { t.Errorf("CheckPath(%+q): %s (%v), want %s", c.path, got, CheckPath(c.path), c.rule) } } } func TestCheckTree(t *testing.T) { for _, c := range []struct { paths []string rule string }{ {[]string{"a/b", "a/c", "d"}, ""}, {[]string{"A.txt", "a.txt"}, "R7"}, {[]string{"A", "a/b"}, "R7"}, {[]string{"a/b", "a/b/c"}, "R7"}, {[]string{"Fotos/b", "fotos/a"}, "R7"}, {[]string{"STRASSE", "Straße"}, "R7"}, {[]string{"ab", "a‌b"}, "R7"}, {[]string{"k", "K"}, "R7"}, } { if got := rule(CheckTree(c.paths)); got != c.rule { t.Errorf("CheckTree(%+q): %s, want %s", c.paths, got, c.rule) } } many := make([]string, MaxImplicitDirs+1) for i := range many { many[i] = fmt.Sprintf("d%05d/f", i) } if got := rule(CheckTree(many)); got != "R9" { t.Errorf("%d folders: %s, want R9", len(many), got) } if got := rule(CheckTree(many[:MaxImplicitDirs])); got != "" { t.Errorf("%d folders: %s", MaxImplicitDirs, got) } } func TestTexts(t *testing.T) { for _, c := range []struct { text string comment bool rule string }{ {"Hola\tmundo\nsegunda línea", true, ""}, {"❤️ para ti", true, ""}, {"Hola\r\n", true, "text"}, {"a‮b", true, "text"}, {"Hola\U000E0049\U000E0047", true, "text"}, // tag characters {"\U0001F600\U000E0100", true, "text"}, // VS17 {"a️", true, "text"}, // VS16 after a letter {"a‍\nb", true, "text"}, // ZWJ at the end of a line {"a￾", true, "text"}, {"a﷐", true, "text"}, {"a­b", true, "text"}, {"Ana López", false, ""}, {"Ana\tLópez", false, "text"}, {"Ana\nLópez", false, "text"}, {" Ana", false, "text"}, {"Ana ", false, "text"}, {"a‎b", false, "text"}, } { check := CheckAuthor if c.comment { check = CheckComment } if got := rule(check(c.text)); got != c.rule { t.Errorf("%+q (comment %v): %s, want %s", c.text, c.comment, got, c.rule) } } }