aboutsummaryrefslogtreecommitdiff
path: root/internal/engine/engine.go
blob: eec9a60b51977b5dc41435dc6f9ce9edac902e51 (plain) (blame)
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
// SPDX-License-Identifier: GPL-3.0-or-later

// Package engine is what every krino front end calls: it loads and compiles
// the configuration, matches files against rules, and (from plan 3) plans
// and applies actions. It returns data; front ends only render it.
package engine

import (
	"os"
	"time"

	"krino/internal/cond"
	"krino/internal/config"
	"krino/internal/extract"
	"krino/internal/ignore"
)

// Engine holds a loaded, compiled configuration: everything a front end
// needs to check, match and (from plan 3) act.
type Engine struct {
	Config   *config.Config
	Dirs     []*Dir
	Extract  *extract.Extractor
	Now      func() time.Time // time.Now; tests replace it
	MainFile string
}

// Dir is one configured directory, with its ignore matcher and rules
// compiled.
type Dir struct {
	Name     string
	Root     string // absolute
	Conf     *config.Dir
	Settings config.Resolved // built-in, then defaults, then the directory
	Ignore   *ignore.Matcher
	Rules    []*Rule

	// ContentVariants is the distinct (IgnoreCase, Fold) pairs any of
	// Rules' content tests evaluate under, in first-seen order. B2: when
	// this holds exactly one variant, facts.Content releases a file's raw
	// extracted text once that variant's normalised copy exists, since no
	// other variant will ever be asked for; with more than one, both must
	// stay memoised, as before.
	ContentVariants []cond.Options
}

// Rule is one directory's rule, with its condition compiled.
type Rule struct {
	Name     string
	Conf     *config.Rule
	Settings config.Resolved // the rule's own settings over its directory's
	Cond     *cond.Cond
}

// Load reads and compiles everything. Any problem anywhere returns a nil
// Engine and every diagnostic: krino never acts on a configuration it only
// partly understood. Duplicate names are ignored after their first use.
func Load(mainFile string, names ...string) (*Engine, []*config.Diag) {
	cfg, errs := config.Load(mainFile, dedupeNames(names)...)
	if cfg == nil {
		return nil, errs
	}

	var dirs []*Dir
	for _, d := range cfg.Dirs {
		dir := &Dir{
			Name:     d.Name,
			Root:     d.Path,
			Conf:     d,
			Settings: cfg.Resolved(d),
		}
		if m, err := ignore.New(d.Ignore); err != nil {
			errs = append(errs, &config.Diag{File: d.File, Msg: err.Error()})
		} else {
			dir.Ignore = m
		}
		for _, r := range d.Rules {
			rs := r.Settings.Over(dir.Settings)
			c, cerrs := cond.Compile(d.File, r.When, cond.Options{
				IgnoreCase: rs.Case == config.CaseIgnore,
				Fold:       rs.Fold,
			})
			if len(cerrs) > 0 {
				errs = append(errs, cerrs...)
				continue
			}
			dir.Rules = append(dir.Rules, &Rule{Name: r.Name, Conf: r, Settings: rs, Cond: c})
		}
		dir.ContentVariants = contentVariants(dir.Rules)
		dirs = append(dirs, dir)
	}

	if len(errs) > 0 {
		return nil, errs
	}
	return &Engine{
		Config:   cfg,
		Dirs:     dirs,
		Extract:  extract.New(),
		Now:      time.Now,
		MainFile: mainFile,
	}, nil
}

// dedupeNames returns names with every repeat after its first occurrence
// removed, order preserved.
func dedupeNames(names []string) []string {
	var out []string
	seen := map[string]bool{}
	for _, n := range names {
		if seen[n] {
			continue
		}
		seen[n] = true
		out = append(out, n)
	}
	return out
}

// contentVariants returns the distinct (IgnoreCase, Fold) pairs any of
// rules' content tests evaluate under, in first-seen order — B2's per-Dir
// ContentVariants. A rule whose condition has no content test at all
// (Cond.UsesContent false) never calls facts.Content, so its resolved
// case/fold settings contribute no variant here.
func contentVariants(rules []*Rule) []cond.Options {
	var out []cond.Options
	seen := map[cond.Options]bool{}
	for _, r := range rules {
		if !r.Cond.UsesContent {
			continue
		}
		opt := cond.Options{IgnoreCase: r.Settings.Case == config.CaseIgnore, Fold: r.Settings.Fold}
		if seen[opt] {
			continue
		}
		seen[opt] = true
		out = append(out, opt)
	}
	return out
}

// Report is what Check reports: the files involved and each directory's
// state.
type Report struct {
	MainFile string
	LogFile  string
	Dirs     []DirReport
	Tools    []extract.Tool
}

// DirReport is one directory's state in a Report.
type DirReport struct {
	Dir     *Dir
	Missing bool // the root is not a directory right now
}

// Check reports the engine's configuration files, each directory's current
// state and the external tools found for content extraction.
func (e *Engine) Check() Report {
	r := Report{
		MainFile: e.MainFile,
		LogFile:  e.Config.LogFile(),
		Tools:    e.Extract.Tools(),
	}
	for _, d := range e.Dirs {
		fi, err := os.Stat(d.Root)
		r.Dirs = append(r.Dirs, DirReport{Dir: d, Missing: err != nil || !fi.IsDir()})
	}
	return r
}