1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
// SPDX-License-Identifier: GPL-3.0-or-later
package engine
import (
"context"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"krino/internal/cond"
"krino/internal/dup"
"krino/internal/norm"
"krino/internal/plan"
"krino/internal/scan"
"krino/internal/xdg"
)
// matchRun holds the state shared by every file evaluated during one Match
// or Explain call: the directory being matched, the full set of scanned
// files (for duplicate detection) and the lazily built duplicate indexes,
// one per distinct set of resolved directories a (duplicate ...) test
// names. mu guards dupOnce, dupIdx and warn, the only fields any goroutine
// but the one that created the matchRun ever touches.
type matchRun struct {
e *Engine
d *Dir
ctx context.Context
now time.Time
files []scan.File
mu sync.Mutex
dupOnce map[string]*sync.Once
dupIdx map[string]*dup.Index
warn []string
}
// newMatchRun builds a matchRun over files, the set a (duplicate ...) test
// with no directories of its own checks against.
func newMatchRun(e *Engine, d *Dir, ctx context.Context, now time.Time, files []scan.File) *matchRun {
return &matchRun{
e: e,
d: d,
ctx: ctx,
now: now,
files: files,
dupOnce: make(map[string]*sync.Once),
dupIdx: make(map[string]*dup.Index),
}
}
// warnings returns the directory-level warnings collected so far (from
// building duplicate indexes), in the order they were recorded.
func (run *matchRun) warnings() []string {
run.mu.Lock()
defer run.mu.Unlock()
return append([]string(nil), run.warn...)
}
// drainDupErrors appends every duplicate index's candidate-hashing errors
// (A1: a candidate other than the file being looked up that could not be
// hashed) to run.warn, once matching is done and every index has seen every
// Lookup it is going to see. Candidate paths are abbreviated with
// xdg.Abbrev, as every other user-visible path is.
func (run *matchRun) drainDupErrors() {
run.mu.Lock()
defer run.mu.Unlock()
for _, idx := range run.dupIdx {
for _, ce := range idx.Errors() {
run.warn = append(run.warn, "duplicate: "+xdg.Abbrev(ce.Path)+": "+ce.Err.Error())
}
}
}
// dupIndex returns the shared *dup.Index for the resolved, sorted extra
// directories named by key, building it exactly once across every
// concurrent caller that asks for the same key.
func (run *matchRun) dupIndex(key string, dirs []string) *dup.Index {
run.mu.Lock()
once, ok := run.dupOnce[key]
if !ok {
once = &sync.Once{}
run.dupOnce[key] = once
}
run.mu.Unlock()
once.Do(func() {
idx, errs := dup.NewIndex(run.files, dirs)
run.mu.Lock()
run.dupIdx[key] = idx
for _, err := range errs {
run.warn = append(run.warn, "duplicate: "+err.Error())
}
run.mu.Unlock()
})
run.mu.Lock()
idx := run.dupIdx[key]
run.mu.Unlock()
return idx
}
// facts is one file's cond.Facts. It is used by exactly one goroutine, so
// its own memoised state (content, its normalised variants, and whether an
// earlier rule matched) needs no locking of its own; only the matchRun it
// points at is shared.
type facts struct {
run *matchRun
file scan.File
matched bool
contentDone bool
content string
contentErr error
normCache map[[2]bool]string
}
var _ cond.Facts = (*facts)(nil)
// newFacts builds the Facts for one scanned file.
func newFacts(run *matchRun, file scan.File) *facts {
return &facts{run: run, file: file, normCache: make(map[[2]bool]string)}
}
func (f *facts) Name() string { return f.file.Name }
func (f *facts) Rel() string { return f.file.Rel }
func (f *facts) Size() int64 { return f.file.Size }
func (f *facts) ModTime() time.Time { return f.file.ModTime }
func (f *facts) Now() time.Time { return f.run.now }
func (f *facts) Matched() bool { return f.matched }
// Content extracts the file's text once, then normalises it per
// (ignoreCase, fold) variant, memoising each. B2: when the directory's
// rules use exactly one variant (Dir.ContentVariants), the raw text is
// released as soon as that variant's normalised copy exists — no other
// variant will ever be asked for, so there is no reason to keep both the
// raw text and its normalised copy in memory at once. A directory using
// more than one variant keeps the raw text for as long as f lives, exactly
// as before.
func (f *facts) Content(ignoreCase, fold bool) (string, error) {
if !f.contentDone {
f.content, f.contentErr = f.run.e.Extract.Text(f.run.ctx, f.file.Path, f.file.Size, f.run.d.Settings.MaxRead)
f.contentDone = true
}
if f.contentErr != nil {
return "", f.contentErr
}
key := [2]bool{ignoreCase, fold}
if v, ok := f.normCache[key]; ok {
return v, nil
}
v := norm.Text(f.content, ignoreCase, fold)
f.normCache[key] = v
if len(f.run.d.ContentVariants) == 1 {
f.content = ""
}
return v, nil
}
// Duplicate resolves dirs against the directory's root, builds (or reuses)
// the shared duplicate index for that resolved, sorted set, and looks the
// file up in it.
func (f *facts) Duplicate(dirs []string) (string, bool, error) {
root := f.run.d.Root
resolved := make([]string, len(dirs))
for i, raw := range dirs {
resolved[i] = plan.ResolveDir(raw, root)
}
sorted := append([]string(nil), resolved...)
sort.Strings(sorted)
key := strings.Join(sorted, "\x00")
idx := f.run.dupIndex(key, sorted)
orig, isDup, err := idx.Lookup(f.file.Path)
if err != nil {
return "", false, err
}
if !isDup {
return "", false, nil
}
return displayOriginal(orig, root), true, nil
}
// displayOriginal reports orig relative to root when it lies inside root,
// else home-abbreviated (xdg.Abbrev), as every other user-visible path is.
func displayOriginal(orig, root string) string {
rel, err := filepath.Rel(root, orig)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return xdg.Abbrev(orig)
}
return filepath.ToSlash(rel)
}
|