diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 20:10:01 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 20:10:01 +0200 |
| commit | aa24cfb344b1b3eaef7217d996359023cd72ba28 (patch) | |
| tree | ee0d981d3add58451a83de13ac56be4a8b165eea | |
| parent | 00aae60378982902b871a87850e0ed427b28a347 (diff) | |
| download | krino-aa24cfb344b1b3eaef7217d996359023cd72ba28.tar.gz krino-aa24cfb344b1b3eaef7217d996359023cd72ba28.zip | |
plan 8: fuzz decoders; fold ẞ and invalid UTF-8 correctly, refuse non-UTF-8 paths in krino new
| -rw-r--r-- | CHANGELOG.md | 9 | ||||
| -rw-r--r-- | Makefile | 11 | ||||
| -rw-r--r-- | docs/design.md | 5 | ||||
| -rw-r--r-- | internal/config/fuzz_test.go | 51 | ||||
| -rw-r--r-- | internal/config/skel.go | 6 | ||||
| -rw-r--r-- | internal/config/skel_test.go | 4 | ||||
| -rw-r--r-- | internal/engine/match.go | 10 | ||||
| -rw-r--r-- | internal/extract/fuzz_test.go | 56 | ||||
| -rw-r--r-- | internal/ignore/fuzz_test.go | 44 | ||||
| -rw-r--r-- | internal/kwcache/fuzz_test.go | 50 | ||||
| -rw-r--r-- | internal/norm/fuzz_test.go | 30 | ||||
| -rw-r--r-- | internal/norm/norm.go | 14 | ||||
| -rw-r--r-- | internal/norm/norm_test.go | 2 | ||||
| -rw-r--r-- | internal/norm/testdata/fuzz/FuzzTextIdempotent/d26a0358dee954b3 | 2 | ||||
| -rw-r--r-- | internal/plan/fuzz_test.go | 33 | ||||
| -rw-r--r-- | internal/sexp/fuzz_test.go | 28 | ||||
| -rw-r--r-- | man/krino.conf.5 | 2 |
17 files changed, 347 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ff1e3..b856f3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +- Folding maps the capital sharp s (ẞ) to "SS", as the other capitals + already were: "STRAẞE" in a document now matches the keyword "straße". +- Folding replaces invalid UTF-8 with U+FFFD first, so a stray invalid byte + no longer stops the letter after it from losing its accent. +- `krino new` refuses a directory whose path is not valid UTF-8 with that + reason, instead of reporting a broken template. +- The keyword cache is also discarded when normalisation changes, so answers + cached before these fixes are recomputed once. + - Builds need Go 1.25 or newer and use the Go 1.26.8 toolchain, which an older `go` downloads itself. `golang.org/x/text` is updated to v0.41.0. This fixes an infinite loop a crafted file's text could cause @@ -87,7 +87,16 @@ FUZZ_TARGETS = \ internal/journal:FuzzParseLine \ internal/journal:FuzzEntryRoundTrip \ internal/trash:FuzzPercentRoundTrip \ - internal/trash:FuzzParsePath + internal/trash:FuzzParsePath \ + internal/sexp:FuzzQuoteRoundTrip \ + internal/config:FuzzParseSize \ + internal/config:FuzzParseDuration \ + internal/norm:FuzzTextIdempotent \ + internal/plan:FuzzExpand \ + internal/ignore:FuzzMatch \ + internal/kwcache:FuzzLoad \ + internal/extract:FuzzStripMarkup \ + internal/extract:FuzzZipText fuzz: ## run every fuzz target for FUZZTIME each (default 20s); a crasher is saved under testdata/fuzz @for t in $(FUZZ_TARGETS); do \ diff --git a/docs/design.md b/docs/design.md index 411eeba..11e859e 100644 --- a/docs/design.md +++ b/docs/design.md @@ -233,7 +233,8 @@ quantifiers, and inline flags `(?i)` `(?-i)`; no backreferences or lookaround. data and the pattern. `type` is always case-insensitive. A regex can override `case` locally with `(?i)` or `(?-i)`. Folding decomposes letters (Unicode NFD), drops combining marks, and maps the letters that do not -decompose (ł ø đ ħ ß æ œ and their capitals) to ASCII. +decompose (ł ø đ ħ ß æ œ and their capitals, ẞ included) to ASCII. Invalid +UTF-8 is replaced by U+FFFD before folding. ### 5.4 Evaluation @@ -348,7 +349,7 @@ appear in the config. Failures (unreadable, tool missing, timeout) are never cached. - The cache is discarded whole when the extractor fingerprint changes: a tool installed, removed or replaced, or krino's extraction code changing - (`extract.Version`). + (`extract.Version`), or its text normalisation changing (`norm.Version`). - Planning a run (`-n` included) reads the cache and writes it back holding only files still in the directory, under the directory's lock, via a temporary file renamed into place; the directory is 0700, the file 0600. diff --git a/internal/config/fuzz_test.go b/internal/config/fuzz_test.go new file mode 100644 index 0000000..32926d6 --- /dev/null +++ b/internal/config/fuzz_test.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package config + +import ( + "fmt" + "strconv" + "testing" + "time" +) + +// FuzzParseSize: a size either fails to parse or is a non-negative number +// of bytes that reads back the same written as plain digits - no overflow +// ever wraps a huge size negative. +func FuzzParseSize(f *testing.F) { + for _, s := range []string{"50M", "0", "1T", "9223372036854775807", "8388608T", "8388607T", "-1", "1.5G", "", "G", "007K"} { + f.Add(s) + } + f.Fuzz(func(t *testing.T, s string) { + v, err := ParseSize(s) + if err != nil { + return + } + if v < 0 { + t.Fatalf("ParseSize(%q) = %d, negative", s, v) + } + if back, err := ParseSize(strconv.FormatInt(v, 10)); err != nil || back != v { + t.Fatalf("ParseSize(%q) = %d, which reads back as %d, %v", s, v, back, err) + } + }) +} + +// FuzzParseDuration: a duration either fails to parse or is a non-negative +// whole number of seconds that reads back the same written in seconds. +func FuzzParseDuration(f *testing.F) { + for _, s := range []string{"30d", "0s", "2m", "1w", "15250284452w", "9223372036s", "d", "", "-1d", "1.5h"} { + f.Add(s) + } + f.Fuzz(func(t *testing.T, s string) { + d, err := ParseDuration(s) + if err != nil { + return + } + if d < 0 || d%time.Second != 0 { + t.Fatalf("ParseDuration(%q) = %v", s, d) + } + if back, err := ParseDuration(fmt.Sprintf("%ds", d/time.Second)); err != nil || back != d { + t.Fatalf("ParseDuration(%q) = %v, which reads back as %v, %v", s, d, back, err) + } + }) +} diff --git a/internal/config/skel.go b/internal/config/skel.go index 65d9f95..ca914e3 100644 --- a/internal/config/skel.go +++ b/internal/config/skel.go @@ -10,6 +10,7 @@ import ( "io/fs" "os" "path/filepath" + "unicode/utf8" "krino/internal/sexp" "krino/internal/xdg" @@ -66,6 +67,11 @@ func NewDir(mainFile, name, path string) (string, error) { if err != nil { return "", err } + // The path is written into the new file, and config files are UTF-8 + // text (the reader refuses anything else). + if !utf8.ValidString(abs) { + return "", fmt.Errorf("%q is not valid UTF-8; krino's config is UTF-8 text, so rename the directory first", abs) + } if fi, err := os.Stat(abs); err != nil || !fi.IsDir() { return "", fmt.Errorf("%s is not a directory", abs) } diff --git a/internal/config/skel_test.go b/internal/config/skel_test.go index 476af65..3815336 100644 --- a/internal/config/skel_test.go +++ b/internal/config/skel_test.go @@ -5,6 +5,7 @@ package config import ( "bytes" "errors" + "fmt" "io/fs" "os" "path/filepath" @@ -242,7 +243,7 @@ func TestNewDirErrors(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) writeFiles(t, home, map[string]string{ - "krino.conf": `(include "a")`, "dirs/a.conf": `(path "~")`, "x/.keep": "", "f": "", + "krino.conf": `(include "a")`, "dirs/a.conf": `(path "~")`, "x/.keep": "", "f": "", "lat\xe9n/.keep": "", }) main := filepath.Join(home, "krino.conf") tests := []struct{ name, path, want string }{ @@ -251,6 +252,7 @@ func TestNewDirErrors(t *testing.T) { {"b", "~/f", filepath.Join(home, "f") + " is not a directory"}, {"b", "~/missing", filepath.Join(home, "missing") + " is not a directory"}, {"a", "~/x", `"a" is already included`}, + {"b", "~/lat\xe9n", fmt.Sprintf("%q is not valid UTF-8; krino's config is UTF-8 text, so rename the directory first", filepath.Join(home, "lat\xe9n"))}, } for _, tt := range tests { if _, err := NewDir(main, tt.name, tt.path); err == nil || err.Error() != tt.want { diff --git a/internal/engine/match.go b/internal/engine/match.go index 1a8077a..b922829 100644 --- a/internal/engine/match.go +++ b/internal/engine/match.go @@ -17,6 +17,7 @@ import ( "krino/internal/cond" "krino/internal/config" "krino/internal/kwcache" + "krino/internal/norm" "krino/internal/plan" "krino/internal/scan" "krino/internal/xdg" @@ -296,6 +297,13 @@ func (e *Engine) Explain(ctx context.Context, path string) (*Explanation, error) return &Explanation{Dir: d, File: sf, Skip: skip, Excludes: excludes, Excluded: excluded, Rules: rules}, nil } +// cacheFingerprint identifies what a cached keyword answer depends on +// besides the file: the extractor (its version and tools) and the +// normalisation version. A cache written under any other is discarded. +func (e *Engine) cacheFingerprint() string { + return fmt.Sprintf("%s norm%d", e.Extract.Fingerprint(), norm.Version) +} + // cacheFile is d's keyword cache file. func (e *Engine) cacheFile(d *Dir) string { return filepath.Join(e.CacheDir, d.Name+".cache") @@ -308,7 +316,7 @@ func (e *Engine) openCache(run *matchRun) []string { if e.CacheDir == "" || len(run.d.ContentKeywords) == 0 { return nil } - c, err := kwcache.Load(e.cacheFile(run.d), e.Extract.Fingerprint()) + c, err := kwcache.Load(e.cacheFile(run.d), e.cacheFingerprint()) run.cache = c if err != nil { return []string{"cache: " + err.Error() + " (starting a new one)"} diff --git a/internal/extract/fuzz_test.go b/internal/extract/fuzz_test.go new file mode 100644 index 0000000..5f707d2 --- /dev/null +++ b/internal/extract/fuzz_test.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package extract + +import ( + "archive/zip" + "bytes" + "context" + "os" + "path/filepath" + "testing" +) + +// FuzzStripMarkup: decoding and stripping any bytes as HTML never panics. +func FuzzStripMarkup(f *testing.F) { + for _, s := range []string{"<p>a & b</p>", "<script>x</script>y", "<!-- c", "<", "�", "<a href='>'>t</a>", "\xff\xfe<\x00p\x00>\x00", "\xef\xbb\xbf\xff"} { + f.Add([]byte(s)) + } + f.Fuzz(func(t *testing.T, data []byte) { + stripMarkup(decode(data)) + }) +} + +// FuzzZipText: reading any bytes as a .docx never panics and never returns +// more text than the budget allows - each entry adds at most one separating +// newline beyond it, and an entry takes more than one byte of the archive. +func FuzzZipText(f *testing.F) { + var buf bytes.Buffer + zw := zip.NewWriter(&buf) + w, err := zw.Create("word/document.xml") + if err != nil { + f.Fatal(err) + } + if _, err := w.Write([]byte(`<w:document><w:body><w:p><w:r><w:t>acme ltd</w:t></w:r></w:p></w:body></w:document>`)); err != nil { + f.Fatal(err) + } + if err := zw.Close(); err != nil { + f.Fatal(err) + } + f.Add(buf.Bytes()) + f.Add([]byte("PK\x03\x04")) + f.Add([]byte{}) + old := zipBudget + zipBudget = 4 << 10 + f.Cleanup(func() { zipBudget = old }) + f.Fuzz(func(t *testing.T, data []byte) { + p := filepath.Join(t.TempDir(), "f.docx") + if err := os.WriteFile(p, data, 0o600); err != nil { + t.Fatal(err) + } + text, err := zipText(context.Background(), p, "docx", 0) + if err == nil && int64(len(text)) > zipBudget+int64(len(data)) { + t.Fatalf("%d bytes of text from a %d-byte archive, budget %d", len(text), len(data), zipBudget) + } + }) +} diff --git a/internal/ignore/fuzz_test.go b/internal/ignore/fuzz_test.go new file mode 100644 index 0000000..0e9f0a3 --- /dev/null +++ b/internal/ignore/fuzz_test.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package ignore + +import ( + "strings" + "testing" +) + +// FuzzMatch: any pattern either fails to compile or matches any walked +// path without panicking, and gives the same answer twice. The oracle test +// checks answers against git; this checks everything else. +func FuzzMatch(f *testing.F) { + for _, p := range []string{"*.log", "/a.txt", "build/", "**/readme.md", "[!a]*.txt", "[[:digit:]]*", `\`, "[", "a[/]b", "!", "**/**/**", "*a*a*a*b"} { + f.Add(p, "dir/sub/a.txt") + } + f.Fuzz(func(t *testing.T, pattern, rel string) { + m, err := New([]string{pattern}) + if err != nil { + return + } + rel = walkedRel(rel) + if rel == "" { + return + } + if m.Match(rel, false) != m.Match(rel, false) { + t.Fatalf("Match(%q) under %q is not deterministic", rel, pattern) + } + m.Match(rel, true) + }) +} + +// walkedRel turns any string into a path of the shape Match accepts, as a +// directory walk produces it: slash-separated, with no empty, "." or ".." +// components. +func walkedRel(s string) string { + var parts []string + for _, p := range strings.Split(s, "/") { + if p != "" && p != "." && p != ".." { + parts = append(parts, p) + } + } + return strings.Join(parts, "/") +} diff --git a/internal/kwcache/fuzz_test.go b/internal/kwcache/fuzz_test.go new file mode 100644 index 0000000..0b0eced --- /dev/null +++ b/internal/kwcache/fuzz_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package kwcache + +import ( + "os" + "path/filepath" + "testing" +) + +// FuzzLoad: a cache file of any content loads as a usable cache - never +// nil, never a panic - and one that reports an error answers nothing. Its +// answers can then be saved again. +func FuzzLoad(f *testing.F) { + seed := New("fp") + seed.Store(a, map[string]bool{"k:acme": true, "k:x": false}) + path := filepath.Join(f.TempDir(), "seed.cache") + if err := seed.Save(path, []ID{a}); err != nil { + f.Fatal(err) + } + good, err := os.ReadFile(path) + if err != nil { + f.Fatal(err) + } + f.Add(good) + f.Add([]byte(`{"version":1,"fingerprint":"fp","keywords":[["b","a"]],"files":[{"dev":1,"ino":10,"size":100,"mtime":1000,"keywords":0,"hits":[5]}]}`)) + f.Add([]byte(`{"version":1,"fingerprint":"fp","keywords":[],"files":[{"dev":1,"ino":10,"size":100,"mtime":1000,"keywords":-1,"hits":[]}]}`)) + f.Add([]byte("{}")) + f.Add([]byte("null")) + f.Fuzz(func(t *testing.T, data []byte) { + p := filepath.Join(t.TempDir(), "dl.cache") + if err := os.WriteFile(p, data, 0o600); err != nil { + t.Fatal(err) + } + c, err := Load(p, "fp") + if c == nil { + t.Fatal("Load returned a nil cache") + } + hits, ok := c.Lookup(a, []string{"k:acme", "k:x"}) + if ok && len(hits) != 2 { + t.Fatalf("Lookup answered %d of 2 keywords", len(hits)) + } + if err != nil && ok { + t.Fatalf("a cache that failed to load (%v) answered", err) + } + if err := c.Save(filepath.Join(t.TempDir(), "out.cache"), []ID{a, b}); err != nil { + t.Fatal(err) + } + }) +} diff --git a/internal/norm/fuzz_test.go b/internal/norm/fuzz_test.go new file mode 100644 index 0000000..fde62a0 --- /dev/null +++ b/internal/norm/fuzz_test.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package norm + +import "testing" + +// FuzzTextIdempotent: normalising text twice changes nothing more than +// normalising it once, under every case and fold setting, and so for names. +// Keywords are normalised at load and text at match time by the same +// functions; a second pass that still changed something would mean the two +// could disagree about what one normalised string is. +func FuzzTextIdempotent(f *testing.F) { + for _, s := range []string{"Spółka Z O.O.", "ẞ straße", "a\u0301", " tabs\tand\nlines ", "\xff", "İstanbul", "Dž", "Æsir Œuvre"} { + f.Add(s) + } + f.Fuzz(func(t *testing.T, s string) { + for _, opt := range [][2]bool{{false, false}, {true, false}, {false, true}, {true, true}} { + once := Text(s, opt[0], opt[1]) + if twice := Text(once, opt[0], opt[1]); twice != once { + t.Fatalf("Text(%q, %v, %v) = %q, again %q", s, opt[0], opt[1], once, twice) + } + } + for _, fold := range []bool{false, true} { + once := Name(s, fold) + if twice := Name(once, fold); twice != once { + t.Fatalf("Name(%q, %v) = %q, again %q", s, fold, once, twice) + } + } + }) +} diff --git a/internal/norm/norm.go b/internal/norm/norm.go index e7933ec..77bb604 100644 --- a/internal/norm/norm.go +++ b/internal/norm/norm.go @@ -11,6 +11,11 @@ import ( unorm "golang.org/x/text/unicode/norm" ) +// Version identifies what Text and Name produce. Bump it whenever a change +// could make any string normalise differently: keyword answers cached under +// another version are discarded (spec §6.1). +const Version = 1 + // special holds the letters that do not decompose under Unicode NFD, so // Fold maps them explicitly. var special = map[rune]string{ @@ -18,15 +23,17 @@ var special = map[rune]string{ 'ø': "o", 'Ø': "O", 'đ': "d", 'Đ': "D", 'ħ': "h", 'Ħ': "H", - 'ß': "ss", + 'ß': "ss", 'ẞ': "SS", 'æ': "ae", 'Æ': "AE", 'œ': "oe", 'Œ': "OE", 'ı': "i", } // Fold strips diacritics: Unicode NFD, drop combining marks (category Mn), -// then map the letters that do not decompose. ASCII input is returned -// unchanged without allocating. +// then map the letters that do not decompose. Invalid UTF-8 becomes U+FFFD +// first: left in, an incomplete sequence can keep NFD from decomposing the +// letter after it, and folding the result again would change it. ASCII +// input is returned unchanged without allocating. func Fold(s string) string { ascii := true for i := 0; i < len(s); i++ { @@ -39,6 +46,7 @@ func Fold(s string) string { return s } + s = strings.ToValidUTF8(s, "\ufffd") var b strings.Builder b.Grow(len(s)) for _, r := range unorm.NFD.String(s) { diff --git a/internal/norm/norm_test.go b/internal/norm/norm_test.go index 2fc35b6..885c6f7 100644 --- a/internal/norm/norm_test.go +++ b/internal/norm/norm_test.go @@ -12,6 +12,8 @@ func TestFold(t *testing.T) { "Łódź": "Lodz", "ZAŻÓŁĆ GĘŚLĄ JAŹŃ": "ZAZOLC GESLA JAZN", "Straße": "Strasse", + "STRAẞE": "STRASSE", + "\xf3Á": "\ufffdA", // an invalid byte must not stop the next letter folding "Øresund": "Oresund", "Ærø": "AEro", "œuvre": "oeuvre", diff --git a/internal/norm/testdata/fuzz/FuzzTextIdempotent/d26a0358dee954b3 b/internal/norm/testdata/fuzz/FuzzTextIdempotent/d26a0358dee954b3 new file mode 100644 index 0000000..4b3fbfe --- /dev/null +++ b/internal/norm/testdata/fuzz/FuzzTextIdempotent/d26a0358dee954b3 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("\xf3Á") diff --git a/internal/plan/fuzz_test.go b/internal/plan/fuzz_test.go new file mode 100644 index 0000000..fb68f94 --- /dev/null +++ b/internal/plan/fuzz_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package plan + +import ( + "testing" + "time" +) + +// FuzzExpand: expanding any template against any file name never panics, +// gives the same answer twice, and never succeeds with a {N} beyond the +// capture groups there are - MaxIndex and Expand read placeholders alike. +func FuzzExpand(f *testing.F) { + for _, s := range []string{"{name}", "{stem}{ext}", "{mtime:%Y/%m}", "{1}_{2}", "{{literal}}", "{", "}", "{now:%", "{0}", "{9}", "{99999999999999999999}"} { + f.Add(s, "a.b.pdf") + } + f.Fuzz(func(t *testing.T, tmpl, name string) { + facts := Facts{ + Name: name, + Captures: []string{name, "x", ".."}, + ModTime: time.Date(2026, 8, 15, 12, 0, 0, 0, time.UTC), + Now: time.Date(2026, 9, 14, 0, 0, 0, 0, time.UTC), + } + a, errA := Expand(tmpl, facts) + b, errB := Expand(tmpl, facts) + if a != b || (errA == nil) != (errB == nil) { + t.Fatalf("Expand(%q) differs between two calls: %q/%v, %q/%v", tmpl, a, errA, b, errB) + } + if n, err := MaxIndex(tmpl); err == nil && n > 2 && errA == nil { + t.Fatalf("Expand(%q) = %q, though it uses {%d} and only 2 groups exist", tmpl, a, n) + } + }) +} diff --git a/internal/sexp/fuzz_test.go b/internal/sexp/fuzz_test.go index 01d436b..c64145a 100644 --- a/internal/sexp/fuzz_test.go +++ b/internal/sexp/fuzz_test.go @@ -2,7 +2,10 @@ package sexp -import "testing" +import ( + "testing" + "unicode/utf8" +) // FuzzParse checks that Parse never panics and that every node it returns // spans valid bytes, with lists pointing at their own parentheses. @@ -25,3 +28,26 @@ func FuzzParse(f *testing.F) { }) }) } + +// FuzzQuoteRoundTrip: Quote gives one string atom that parses back to +// exactly the text quoted, for any valid UTF-8 - quotes, backslashes and +// newlines included. krino new writes a directory's path into its config +// with Quote; config files are UTF-8 text, so krino new refuses a path that +// is not, and invalid UTF-8 is out of this property's scope. +func FuzzQuoteRoundTrip(f *testing.F) { + for _, s := range []string{"plain", `with "quotes"`, `back\slash`, "new\nline", "zażółć", "", "\xff", `\bacme\b`} { + f.Add(s) + } + f.Fuzz(func(t *testing.T, s string) { + if !utf8.ValidString(s) { + return + } + nodes, err := Parse("f", []byte(Quote(s))) + if err != nil { + t.Fatalf("Quote(%q) = %s does not parse: %v", s, Quote(s), err) + } + if len(nodes) != 1 || nodes[0].Kind != String || nodes[0].Text != s { + t.Fatalf("Quote(%q) = %s reads back as %+v", s, Quote(s), nodes) + } + }) +} diff --git a/man/krino.conf.5 b/man/krino.conf.5 index c89fbd9..cd16ada 100644 --- a/man/krino.conf.5 +++ b/man/krino.conf.5 @@ -539,7 +539,7 @@ Failures are never cached, and a file above .Ic max-read is refused before the cache is read. The whole cache is discarded when an extraction tool is installed, removed -or replaced. +or replaced, or when krino's extraction or normalisation changes. Each run keeps entries only for files still in the directory. A file edited in place with its size and modification time preserved keeps its old answers; deleting the cache resets everything. |
