require "spec" private def assert_file_matches(pattern, path : String, *, file = __FILE__, line = __LINE__) File.match?(pattern, path).should be_true, file: file, line: line File.match?(pattern, Path.posix(path)).should be_true, file: file, line: line File.match?(pattern, Path.posix(path).to_windows).should be_true, file: file, line: line end private def refute_file_matches(pattern, path : String, *, file = __FILE__, line = __LINE__) File.match?(pattern, path).should be_false, file: file, line: line File.match?(pattern, Path.posix(path)).should be_false, file: file, line: line File.match?(pattern, Path.posix(path).to_windows).should be_false, file: file, line: line end describe File do describe ".match?" do it "matches basics" do assert_file_matches "abc", "abc" assert_file_matches "*", "abc" assert_file_matches "*c", "abc" assert_file_matches "a*", "a" assert_file_matches "a*", "abc" assert_file_matches "a*/b", "abc/b" assert_file_matches "*x", "xxx" assert_file_matches "*.x", "a.x" assert_file_matches "a/b/*.x", "a/b/c.x" refute_file_matches "*.x", "a/b/c.x" refute_file_matches "c.x", "a/b/c.x" refute_file_matches "b/*.x", "a/b/c.x" end it "matches multiple expansions" do assert_file_matches "a*b*c*d*e*/f", "axbxcxdxe/f" assert_file_matches "a*b*c*d*e*/f", "axbxcxdxexxx/f" assert_file_matches "a*b?c*x", "abxbbxdbxebxczzx" refute_file_matches "a*b?c*x", "abxbbxdbxebxczzy" end describe "multibyte" do it "single-character match" do assert_file_matches "a?b", "a☺b" refute_file_matches "a???b", "a☺b" end it "character sets" do assert_file_matches "[🐶🐱🐰].jpg", "🐶.jpg" refute_file_matches "[🐶🐱🐰].jpg", "🐯.jpg" refute_file_matches "[🐶🐱🐰].jpg", "x.jpg" assert_file_matches "[🐶🐱🐰x].jpg", "🐶.jpg" refute_file_matches "[🐶🐱🐰x].jpg", "🐯.jpg" assert_file_matches "[🐶🐱🐰x].jpg", "x.jpg" assert_file_matches "[^🐶🐱🐰].jpg", "🐯.jpg" refute_file_matches "[^🐶🐱🐰].jpg", "🐶.jpg" assert_file_matches "[^🐶🐱🐰].jpg", "x.jpg" end it "character ranges" do assert_file_matches "[α-ω].doc", "β.doc" refute_file_matches "[α-ω].doc", "Ω.doc" assert_file_matches "[Α-Ω].pdf", "Θ.pdf" assert_file_matches "[🥇-🥉].png", "🥈.png" refute_file_matches "[🥇-🥉].png", "🏆.png" refute_file_matches "[🥇-🥉].png", "2.png" assert_file_matches "[α-ω🥇-🥉].doc", "β.doc" assert_file_matches "[α-ω🥇-🥉].doc", "🥈.doc" refute_file_matches "[α-ω🥇-🥉].doc", "Ω.doc" refute_file_matches "[α-ω🥇-🥉].doc", "🏆.doc" assert_file_matches "[Α-Ω🥇-🥉].pdf", "Θ.pdf" end it "braces" do assert_file_matches "{café,restaurant}.png", "café.png" assert_file_matches "{🐶,🐱,🐰}.log", "🐶.log" refute_file_matches "{🐶,🐱,🐰}.log", "🐯.log" end it "wildcard" do assert_file_matches "重要/*/中.txt", "重要/子文件夹/中.txt" refute_file_matches "重要/*/中.txt", "重要/子文/件夹/中.txt" end it "globstar" do assert_file_matches "重要/**/中.txt", "重要/子文件夹/中.txt" assert_file_matches "重要/**/中.txt", "重要/子文/件夹/中.txt" end it "NFC and NFD are disparate" do assert_file_matches "café.txt", "café.txt" # NFC refute_file_matches "café.txt", "cafe\u0301.txt" # NFD refute_file_matches "cafe*.txt", "café.txt" # NFC assert_file_matches "cafe*.txt", "cafe\u0301.txt" # NFD end end describe "invalid byte sequences" do it "single-character with invalid path" do assert_file_matches "?.txt", "\xC3.txt" # Invalid byte sequence refute_file_matches "?.txt", "\xC3\x28.txt" # Invalid byte sequence refute_file_matches "?.txt", "\xED\xA0\x80.txt" # Lone surrogate assert_file_matches "?.txt", "\uFFFF.txt" # Noncharacter codepoint end it "single-character with invalid pattern" do refute_file_matches "\xC3\x28.txt", "a.txt" # Invalid byte sequence refute_file_matches "\xED\xA0\x80.txt", "b.txt" # Lone surrogate end it "character set with invalid path" do refute_file_matches "[a-z].txt", "\xF0\x28\x8C\x28.txt" # Invalid byte sequence refute_file_matches "[A-Z].txt", "\xED\xA0\x80.txt" # Lone surrogate end it "character set with invalid pattern" do refute_file_matches "[\xC3\x28].txt", "m.txt" # Invalid byte sequence refute_file_matches "[\xED\xA0\x80].txt", "A.txt" # Lone surrogate end it "character range with invalid path" do refute_file_matches "[a-z].txt", "\xED\xA0\x80.txt" # Invalid byte sequence refute_file_matches "[α-ω].txt", "\xED\xBF\xBF.txt" # Lone surrogate refute_file_matches "[😀-🙏].png", "\xFF\xFE\xFD.png" # Invalid byte sequence end it "character range with invalid pattern" do refute_file_matches "[\xF0\x28\x8C\x28].txt", "o.txt" # Corrupt range refute_file_matches "[\xED\xA0\x80-\xED\xBD\xBF].csv", "X.csv" # Invalid range of surrogates end it "invalid pattern and path" do assert_file_matches "[\xED\xA0\x80-α]?.log", "\xC3\x28.log" # Lone surrogate in pattern, bad in path refute_file_matches "[😀-\uFFFF]?.json", "\xF0\x90\x28\xBC.json" # Invalid range with corrupt UTF-8 refute_file_matches "[\xED\xA0\x80-\uFFFF]?", "\xED\xBD\xBF.txt" # Invalid pattern range and lone low surrogate in path end end it "* don't match path separator" do refute_file_matches "a*", "ab/c" refute_file_matches "a*/b", "a/c/b" refute_file_matches "a*b*c*d*e*/f", "axbxcxdxe/xxx/f" refute_file_matches "a*b*c*d*e*/f", "axbxcxdxexxx/fff" end it "**" do assert_file_matches "a/b/**", "a/b/c.x" assert_file_matches "a/**", "a/b/c.x" assert_file_matches "a/**/d.x", "a/b/c/d.x" refute_file_matches "a/**b/d.x", "a/bb/c/d.x" refute_file_matches "a/b**/*", "a/bb/c/d.x" end it "** bugs (#15319)" do assert_file_matches "a/**/*", "a/b/c/d.x" refute_file_matches "a/b**/d.x", "a/bb/c/d.x" assert_file_matches "**/*.x", "a/b/c.x" refute_file_matches "**.x", "a/b/c.x" end it "** matches path separator" do refute_file_matches "a**", "ab/c" refute_file_matches "a**/b", "a/c/b" refute_file_matches "a*b*c*d*e**/f", "axbxcxdxe/xxx/f" assert_file_matches "a*b*c*d*e**/f", "axbxcxdxexxx/f" refute_file_matches "a*b*c*d*e**/f", "axbxcxdxexxx/fff" end it "classes" do assert_file_matches "ab[c]", "abc" assert_file_matches "ab[b-d]", "abc" refute_file_matches "ab[d-b]", "abc" refute_file_matches "ab[e-g]", "abc" assert_file_matches "ab[e-gc]", "abc" refute_file_matches "ab[^c]", "abc" refute_file_matches "ab[^b-d]", "abc" assert_file_matches "ab[^e-g]", "abc" assert_file_matches "a[^a]b", "a☺b" refute_file_matches "a[^a][^a][^a]b", "a☺b" assert_file_matches "[a-ζ]*", "α" refute_file_matches "*[a-ζ]", "A" end it "escape" do # NOTE: `*` is forbidden in Windows paths File.match?("a\\*b", "a*b").should be_true refute_file_matches "a\\*b", "ab" File.match?("a\\**b", "a*bb").should be_true refute_file_matches "a\\**b", "abb" File.match?("a*\\*b", "ab*b").should be_true refute_file_matches "a*\\*b", "abb" assert_file_matches "a\\[b\\]", "a[b]" refute_file_matches "a\\[b\\]", "ab" assert_file_matches "a\\[bb\\]", "a[bb]" refute_file_matches "a\\[bb\\]", "abb" assert_file_matches "a[b]\\[b\\]", "ab[b]" refute_file_matches "a[b]\\[b\\]", "abb" end it "special chars" do refute_file_matches "a?b", "a/b" refute_file_matches "a*b", "a/b" end it "classes escapes" do assert_file_matches "[\\]a]", "]" assert_file_matches "[\\-]", "-" assert_file_matches "[x\\-]", "x" assert_file_matches "[x\\-]", "-" refute_file_matches "[x\\-]", "z" assert_file_matches "[\\-x]", "x" assert_file_matches "[\\-x]", "-" refute_file_matches "[\\-x]", "a" assert_file_matches "[]a]", "]" assert_file_matches "[-]", "-" assert_file_matches "[x-]", "x" assert_file_matches "[-x]", "x" expect_raises(File::BadPatternError, "Empty escape character") do File.match?("\\", "a") end assert_file_matches "[a-b-c]", "a" expect_raises(File::BadPatternError, "unterminated character set") do File.match?("[", "a") end expect_raises(File::BadPatternError, "unterminated character set") do File.match?("[^", "a") end expect_raises(File::BadPatternError, "unterminated character set") do File.match?("[^bc", "a") end refute_file_matches "a[", "a" end it "alternates" do assert_file_matches "{abc,def}", "abc" assert_file_matches "ab{c,}", "abc" assert_file_matches "ab{c,}", "ab" refute_file_matches "ab{d,e}", "abc" assert_file_matches "ab{*,/cde}", "abcde" assert_file_matches "ab{*,/cde}", "ab/cde" assert_file_matches "ab{?,/}de", "abcde" assert_file_matches "ab{?,/}de", "ab/de" assert_file_matches "ab{{c,d}ef,}", "ab" assert_file_matches "ab{{c,d}ef,}", "abcef" assert_file_matches "ab{{c,d}ef,}", "abdef" end describe "brace stack" do it "allows up to 10 levels" do assert_file_matches "{{{{{{{{{{a},b},b},b},b},b},b},b},b},b}", "a" end it "raises at more than 10 levels" do expect_raises File::BadPatternError, "Brace nesting too deep: must not exceed 10 levels" do File.match? "{{{{{{{{{{{a},b},b},b},b},b},b},b},b},b},b}", "b" end end end end it "fuzz tests" do # https://github.com/devongovett/glob-match/issues/1 s = "{*{??*{??**,Uz*zz}w**{*{**a,z***b*[!}w??*azzzzzzzz*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!z[za,z&zz}w**z*z*}" refute_file_matches s, s s = "**** *{*{??*{??***\u{5} *{*{??*{??***\u{5},\0U\0}]*****\u{1},\0***\0,\0\0}w****,\0U\0}]*****\u{1},\0***\0,\0\0}w*****\u{1}***{}*.*\0\0*\0" # Must use `String` overload here because `Path` raises on null byte File.match?(s, s).should be_false end end