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
|
// SPDX-License-Identifier: GPL-3.0-or-later
// Package plan turns matched rules into concrete actions: placeholder
// expansion, chains, conflict resolution and the JSON plan representation.
package plan
import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// Facts are the values a placeholder can reference.
type Facts struct {
Name string // the file's current base name
Captures []string // [0] whole match, [1:] groups, from the rule's first true name test
ModTime time.Time
Now time.Time
}
// splitExt splits name on its last dot, which does not count when it is the
// first character: "a.tar.gz" -> "a.tar", ".gz"; ".bashrc" -> ".bashrc", "".
func splitExt(name string) (stem, ext string) {
i := strings.LastIndexByte(name, '.')
if i <= 0 {
return name, ""
}
return name[:i], name[i:]
}
// Expand replaces every placeholder in s. It returns an error naming the
// first placeholder it could not expand. It scans s once: "{{" and "}}"
// each emit one literal brace, and any other "{" opens a placeholder that
// runs to the next "}".
func Expand(s string, f Facts) (string, error) {
var b strings.Builder
for i := 0; i < len(s); {
switch {
case s[i] == '{' && i+1 < len(s) && s[i+1] == '{':
b.WriteByte('{')
i += 2
case s[i] == '}' && i+1 < len(s) && s[i+1] == '}':
b.WriteByte('}')
i += 2
case s[i] == '{':
end := strings.IndexByte(s[i+1:], '}')
if end < 0 {
return "", errors.New("unclosed placeholder")
}
body := s[i+1 : i+1+end]
val, err := expandOne(body, f)
if err != nil {
return "", err
}
// D2: val is appended with WriteString, after the scan has
// already moved past this placeholder - it is never re-passed
// through this loop, so a "}}" or "{{" inside a capture's own
// text is never collapsed the way one written in the template
// itself would be. A refactor to scan-then-replace would
// silently undo this; TestExpandCaptureNotRescanned pins it.
b.WriteString(val)
i += end + 2
default:
b.WriteByte(s[i])
i++
}
}
return b.String(), nil
}
// expandOne expands the body of a single {...} placeholder (without the
// braces) into its replacement text.
func expandOne(body string, f Facts) (string, error) {
switch body {
case "name":
return f.Name, nil
case "stem":
stem, _ := splitExt(f.Name)
return stem, nil
case "ext":
_, ext := splitExt(f.Name)
return ext, nil
}
if verb, format, ok := strings.Cut(body, ":"); ok {
switch verb {
case "mtime":
return strftime(verb, format, f.ModTime)
case "now":
return strftime(verb, format, f.Now)
}
return "", fmt.Errorf("unknown placeholder {%s}", body)
}
if n, err := strconv.Atoi(body); err == nil {
if n == 0 {
return "", errors.New("capture groups are numbered from 1")
}
// D3: spec §7.3 defines the syntax as {1}...{9}, the same window
// MaxIndex enforces; without this check {10} and up bypass
// checkCaptures entirely (MaxIndex never sees them as capture
// uses) and fail only here, at expansion time.
if n > 9 {
return "", fmt.Errorf("unknown placeholder {%s}", body)
}
if n < 0 || n >= len(f.Captures) {
return "", fmt.Errorf("no capture group %d", n)
}
return f.Captures[n], nil
}
return "", fmt.Errorf("unknown placeholder {%s}", body)
}
// strftime renders a strftime subset (%Y %m %d %H %M %S %j %%) of t. verb is
// the placeholder's own verb ("mtime" or "now"), named in error messages so
// they point at what the config author actually wrote (B3) instead of
// hardcoding "mtime" for a {now:...} format error.
func strftime(verb, format string, t time.Time) (string, error) {
var b strings.Builder
for i := 0; i < len(format); i++ {
c := format[i]
if c != '%' {
b.WriteByte(c)
continue
}
i++
if i >= len(format) {
return "", fmt.Errorf("unknown time format %%%c in {%s:...}", format[i-1], verb)
}
switch format[i] {
case 'Y':
fmt.Fprintf(&b, "%04d", t.Year())
case 'm':
fmt.Fprintf(&b, "%02d", int(t.Month()))
case 'd':
fmt.Fprintf(&b, "%02d", t.Day())
case 'H':
fmt.Fprintf(&b, "%02d", t.Hour())
case 'M':
fmt.Fprintf(&b, "%02d", t.Minute())
case 'S':
fmt.Fprintf(&b, "%02d", t.Second())
case 'j':
fmt.Fprintf(&b, "%03d", t.YearDay())
case '%':
b.WriteByte('%')
default:
return "", fmt.Errorf("unknown time format %%%c in {%s:...}", format[i], verb)
}
}
return b.String(), nil
}
|