aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:53:54 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-14 23:53:54 +0200
commit0b155e8df3205aebf129091b517f4116665a5adc (patch)
tree2215aa05b1d703f9b37badd96aa94a95fd2b14bf
parent1f1a303c617057f149b4f0d748ee0a195484642c (diff)
downloadkrino-0b155e8df3205aebf129091b517f4116665a5adc.tar.gz
krino-0b155e8df3205aebf129091b517f4116665a5adc.zip
main excludes checked with the defaults' case and fold; Latin-1 decoding memory; hardlink election documented
-rw-r--r--CHANGELOG.md3
-rw-r--r--docs/design.md5
-rw-r--r--internal/engine/engine.go6
-rw-r--r--internal/engine/exclude_test.go7
-rw-r--r--internal/extract/plain.go12
-rw-r--r--internal/extract/plain_test.go16
6 files changed, 42 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39ab7fb..37a9f50 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,9 @@
could change its answer; `explain` shows `?`. A document read only in part
answers the keywords found in what was read, and leaves the others
unknown.
+- krino.conf's excludes are checked with the defaults' `case` and `fold`
+ even while no directory is included; Latin-1 text is decoded in a third
+ of the memory.
- `-v` lists the rule destinations inside a directory that were not
scanned, so files there are no longer invisible.
- Plan and table widths count terminal columns: a name in a wide script
diff --git a/docs/design.md b/docs/design.md
index c9ebe50..ef52d7e 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -279,7 +279,10 @@ by size. Only groups of two or more are hashed: first the first and last
64 KiB, then SHA-256 of the whole file. Empty files are never duplicates.
Two names for the same file — the same device and inode, as a hardlink
creates — are never duplicates of each other; they may still both be
-duplicates of a separate identical file.
+duplicates of a separate identical file. The original is elected once for
+all identical content, so a scanned file whose hardlink under a DIR is
+elected is that original, and not a duplicate, even when the DIR also
+holds a separate copy.
The **original** in each group is, in order of preference: a file under one
of the given DIRs, then the oldest by mtime, then the shortest name, then
diff --git a/internal/engine/engine.go b/internal/engine/engine.go
index 7a83ccd..00a249d 100644
--- a/internal/engine/engine.go
+++ b/internal/engine/engine.go
@@ -93,8 +93,12 @@ func Load(mainFile string, names ...string) (*Engine, []*config.Diag) {
// With no directory to compile them for, krino.conf's excludes are
// still checked, so a mistake is reported before one is included
// (review cli F10).
+ // Compiled with the defaults' case and fold, as a directory without
+ // settings of its own would compile them (triage 28g).
+ def := cfg.Main.Defaults.Over(config.Builtin())
+ opt := cond.Options{IgnoreCase: def.Case == config.CaseIgnore, Fold: def.Fold}
for _, x := range cfg.Main.Excludes {
- _, cerrs := cond.Compile(cfg.Main.File, x.When, cond.Options{IgnoreCase: true, Fold: true})
+ _, cerrs := cond.Compile(cfg.Main.File, x.When, opt)
errs = append(errs, cerrs...)
}
}
diff --git a/internal/engine/exclude_test.go b/internal/engine/exclude_test.go
index cd1cc4b..8308a82 100644
--- a/internal/engine/exclude_test.go
+++ b/internal/engine/exclude_test.go
@@ -261,6 +261,13 @@ func TestLoadChecksMainExcludesWithoutDirectories(t *testing.T) {
if _, errs := Load(main); len(errs) == 0 {
t.Error("a broken krino.conf exclude was not reported")
}
+ // Checked with the defaults' case and fold, as a directory would compile
+ // them (triage 28g): a keyword of a lone combining mark is empty only
+ // when folded.
+ main = writeConfig(t, h, "(include)\n(defaults (fold no))\n(exclude (content \"\u0301\"))\n", nil)
+ if _, errs := Load(main); len(errs) != 0 {
+ t.Errorf("checked with fold on, though the defaults say no: %v", errs)
+ }
}
// TestNoTextFormatIsNoMatch: a file whose format has no text cannot contain
diff --git a/internal/extract/plain.go b/internal/extract/plain.go
index 3f04c7e..ce9dedc 100644
--- a/internal/extract/plain.go
+++ b/internal/extract/plain.go
@@ -142,13 +142,15 @@ func decodeUTF16(b []byte, order binary.ByteOrder) string {
}
// decodeLatin1 decodes b as Latin-1: each byte is its own Unicode code
-// point.
+// point. Built directly as UTF-8 (at most two bytes per input byte), not
+// through a []rune of four bytes per input byte (triage 28d).
func decodeLatin1(b []byte) string {
- r := make([]rune, len(b))
- for i, c := range b {
- r[i] = rune(c)
+ var s strings.Builder
+ s.Grow(len(b) * 2)
+ for _, c := range b {
+ s.WriteRune(rune(c))
}
- return string(r)
+ return s.String()
}
// stripMarkup turns decoded HTML/XML/SVG text into plain text: a small
diff --git a/internal/extract/plain_test.go b/internal/extract/plain_test.go
index 5752bf8..c8e1008 100644
--- a/internal/extract/plain_test.go
+++ b/internal/extract/plain_test.go
@@ -3,6 +3,7 @@
package extract
import (
+ "bytes"
"context"
"errors"
"os"
@@ -228,3 +229,18 @@ func TestSniffRuneStraddlingSampleBoundary(t *testing.T) {
t.Errorf("got tail %q, want it to end in %q", got[len(got)-8:], want)
}
}
+
+// TestLatin1DecodingMemory: decoding Latin-1 allocates about what the UTF-8
+// result needs (at most two bytes per input byte), not a four-byte rune per
+// input byte on top of it (triage 28d).
+func TestLatin1DecodingMemory(t *testing.T) {
+ data := bytes.Repeat([]byte("Gr\xfc\xdfe "), 16000)
+ r := testing.Benchmark(func(b *testing.B) {
+ for range b.N {
+ decodeLatin1(data)
+ }
+ })
+ if got, limit := r.AllocedBytesPerOp(), int64(3*len(data)); got > limit {
+ t.Errorf("decoding %d bytes allocates %d bytes, over %d", len(data), got, limit)
+ }
+}