diff --git a/src/algo/algo.go b/src/algo/algo.go index 45433c03..c3a4ca7d 100644 --- a/src/algo/algo.go +++ b/src/algo/algo.go @@ -303,7 +303,9 @@ func bonusAt(input *util.Chars, idx int) int16 { } func normalizeRune(r rune) rune { - if r < 0x00C0 || r > 0xFF61 { + // Every key of the map folds to ASCII, so a rune the bitmap rejects cannot + // be in it. TestNormalizedKeysAreFlagged pins that. + if !util.MayFoldToAscii(r) { return r } diff --git a/src/algo/runeprefilter_test.go b/src/algo/runeprefilter_test.go index 8277eb40..59c2d283 100644 --- a/src/algo/runeprefilter_test.go +++ b/src/algo/runeprefilter_test.go @@ -226,6 +226,35 @@ func TestRunePrefilterEquivalenceV1(t *testing.T) { disableRunePrefilter = false } +// normalizeRune skips the map when util.MayFoldToAscii rejects the rune. That +// is only sound if every key of the map is flagged, since a flagged-false rune +// is returned unchanged. +func TestNormalizedKeysAreFlagged(t *testing.T) { + for k := range normalized { + if !util.MayFoldToAscii(k) { + t.Errorf("normalized key U+%04X (%c) is not flagged by MayFoldToAscii", k, k) + } + } +} + +// Guarding normalizeRune must not change what it returns, for any rune. +func TestNormalizeRuneUnchangedByGuard(t *testing.T) { + for r := rune(0); r <= unicode.MaxRune; r++ { + if r >= 0xD800 && r <= 0xDFFF { + continue + } + exp := r + if r >= 0x00C0 && r <= 0xFF61 { + if n := normalized[r]; n > 0 { + exp = n + } + } + if got := normalizeRune(r); got != exp { + t.Fatalf("normalizeRune(U+%04X) = U+%04X, expected U+%04X", r, got, exp) + } + } +} + // preparePattern mirrors what pattern.go guarantees the algo functions: // lowercased when case-insensitive, normalized when normalize is on. func preparePattern(pat string, caseSensitive, normalize bool) []rune {