Skip to main content

trex/
rewrite.rs

1//! Rewrite and transform: match, render a replacement template per
2//! match, splice the renders into the input.
3//!
4//! A rewrite is `match -> render -> splice`. The match stage is the same
5//! structure-aware scan as everywhere else, so a rewrite expresses
6//! transforms a byte regex cannot reach safely: it substitutes spans
7//! found by typed-token, balanced, and bound matching, not by a greedy
8//! byte search. Renaming a balanced tag, reformatting a typed atom, or
9//! reordering a captured argument list are all just templates over the
10//! spans the scan finds and the registers they bound.
11//!
12//! ## Template syntax
13//!
14//! - `${name}` renders a named capture's value; for a register bound under
15//!   a repetition, its last binding. `${name[i]}` renders the i-th binding,
16//!   and `${name[*]}` every binding joined with a comma.
17//! - `${0}` renders the whole matched span.
18//! - `${1}`, `${2}` and so on render a capture by position: `${1}` is the
19//!   first name the pattern binds. trex's `(...)` is a logical group and
20//!   binds nothing, so what is numbered is the bindings in written order,
21//!   rather than a second anonymous kind of capture alongside `:name`.
22//! - `${name:acc}` applies an accessor, and `${name:a|b}` chains them.
23//!   Accessors are the transforms `upper` / `lower`, the slices `trim` /
24//!   `firstN` / `lastN`, and the typed sub-field extractors that slice a
25//!   captured atom by its known structure:
26//!   `octetN[-M]` (IPv4) / `groupN[-M]` (IPv6) on an `\I`, `scheme` / `host` /
27//!   `port` / `path` / `query` on a `\U`, `user` / `domain` on an `\E`,
28//!   `major` / `minor` / `patch` on a `\V`, `year` / `month` / `day` / `hour` /
29//!   `minute` / `second` on a `\T`, and `dir` / `name` / `ext` on a `\L`.
30//! - `$$` is a literal dollar sign; `\n`, `\t` and `\\` are a newline, a tab
31//!   and a backslash; every other byte is literal, and a backslash before
32//!   anything else is an error.
33//! - `${path}`, `${line}`, `${col}`, `${start}` and `${end}`, in a report's
34//!   template ([`Template::parse_report`]), are where the match stands: the
35//!   input's name, its line and column from one, and its byte offsets. They
36//!   take accessors too (`${path:name}`).
37//! - `${@axis}` is a reading `--explain` computes for the match: the kinds it
38//!   spans (`${@kind}`), the guard each guarded kind passed (`${@guard}`),
39//!   the rung that answered (`${@route}`), and every axis the pattern read -
40//!   `${@magnitude}`, `${@baseline}`, `${@spectral}`, `${@echo}`,
41//!   `${@order}`, `${@template}`, `${@nesting}`, `${@seam}`,
42//!   `${@ambiguous}`, `${@construct}`, `${@phase}`, `${@field}`,
43//!   `${@join}`. A dot names one value of the reading rather than the whole
44//!   sentence: `${@spectral.entropy}`, `${@template.rarity}`,
45//!   `${@construct.role}`, and the two depths the axes tell apart,
46//!   `${@nesting.depth}` for bracket nesting and `${@construct.depth}` for
47//!   the enclosing unit. An axis reads at every token the match spans, so
48//!   the reference renders them joined and an index picks one:
49//!   `${@magnitude[0]}`. Accessors apply as they do elsewhere. An axis the
50//!   pattern never read renders empty; an axis that does not exist is a
51//!   parse error. [`Template::reads_explanation`] is what tells a caller to
52//!   build the explainer, so a template naming none costs what it always
53//!   did.
54//!
55//! A template is validated against the pattern's capture names when it
56//! is parsed, so a reference to a name the pattern never binds is an
57//! error rather than a silent empty render. The `@` keeps the axes out of
58//! that namespace: a pattern may bind `:kind`, and `${kind}` is its
59//! register under every pattern, whether or not the pattern also reads an
60//! axis by that name.
61//!
62//! ## Redaction
63//!
64//! [`redactions`] masks each match and leaves the fields a [`Keep`] names
65//! where they stand, so the slicing accessors have a second reading: a kept
66//! field is the byte range an accessor locates rather than the text it
67//! renders. Every slicing accessor renders exactly the bytes it locates, so
68//! `${card:last4}` in a template and `card:last4` in a redaction are the
69//! same four characters.
70//!
71//! ## Callback
72//!
73//! [`rewrite_with`] and [`rewrite_n_with`] take a closure in a template's
74//! place: each match arrives as a [`Matched`], and [`Matched::get`] reads
75//! the reference a template writes inside `${...}` - `0`, a register, a
76//! typed slice such as `e:domain` - through the same [`Reference`], so a
77//! closure and a template read a match the same way.
78
79use std::borrow::Cow;
80use std::ops::Range;
81
82use crate::ast::Pattern;
83use crate::engine::{Match, Span, captures, captures_with_lists, scan};
84use crate::explain::Explanation;
85use crate::gpu::{Backend, BackendUsed, scan_gpu, scan_with_backend};
86use crate::token::TokenKind;
87
88/// One step applied to a captured value before it is rendered: a pure
89/// transform, or a typed sub-field extractor that decomposes a captured atom by
90/// its known structure - an IP into octets or IPv6 groups, a URL into host and
91/// path, an email into user and domain, a version into major/minor/patch, a
92/// timestamp into date fields, a path into dir/name/ext. Because trex captured
93/// a *typed* atom, the sub-field is a slice of a known shape, not a second
94/// match. Steps chain left to right with `|`.
95#[derive(Clone, Debug, PartialEq, Eq)]
96enum Accessor {
97    Upper,
98    Lower,
99    Trim,
100    /// The first `n` characters.
101    First(usize),
102    /// The last `n` characters.
103    Last(usize),
104    /// IPv4 octets `a..=b` (1-based inclusive), rejoined with `.`.
105    Octet(usize, usize),
106    /// IPv6 groups `a..=b` (1-based inclusive), rejoined with `:`.
107    Group(usize, usize),
108    UrlScheme,
109    UrlHost,
110    UrlPort,
111    UrlPath,
112    UrlQuery,
113    EmailUser,
114    EmailDomain,
115    VerMajor,
116    VerMinor,
117    VerPatch,
118    /// The i-th numeric field of a timestamp: year, month, day, hour, ...
119    TsField(usize),
120    PathDir,
121    PathName,
122    PathExt,
123    /// A quantity's number as written, sign included.
124    QtyValue,
125    /// A quantity's unit symbol as written.
126    QtyUnit,
127}
128
129/// A byte range within the text an accessor read.
130type ByteRange = Range<usize>;
131
132impl Accessor {
133    /// Whether the accessor reads a slice of its input rather than
134    /// transforming it, so its value has a place in the original text.
135    fn slices(&self) -> bool {
136        !matches!(self, Accessor::Upper | Accessor::Lower)
137    }
138
139    /// The byte range of the value within `v`: `None` for a transform, and
140    /// where the value is absent or empty. Every slicing accessor renders
141    /// exactly the bytes it locates, so [`Self::apply`] reads through here
142    /// and a kept field is the same bytes a template would render.
143    fn locate(&self, v: &str) -> Option<ByteRange> {
144        let r = match self {
145            Accessor::Upper | Accessor::Lower => return None,
146            Accessor::Trim => {
147                let start = v.len() - v.trim_start().len();
148                start..start + v.trim().len()
149            }
150            Accessor::First(n) => 0..v.char_indices().nth(*n).map_or(v.len(), |(i, _)| i),
151            Accessor::Last(n) => {
152                v.char_indices().rev().nth(n.checked_sub(1)?).map_or(0, |(i, _)| i)..v.len()
153            }
154            Accessor::Octet(a, b) => parts_range(v, '.', *a, *b)?,
155            Accessor::Group(a, b) => parts_range(v, ':', *a, *b)?,
156            Accessor::UrlScheme => url_range(v, UrlPart::Scheme),
157            Accessor::UrlHost => url_range(v, UrlPart::Host),
158            Accessor::UrlPort => url_range(v, UrlPart::Port),
159            Accessor::UrlPath => url_range(v, UrlPart::Path),
160            Accessor::UrlQuery => url_range(v, UrlPart::Query),
161            Accessor::EmailUser => 0..v.find('@')?,
162            Accessor::EmailDomain => v.find('@')? + 1..v.len(),
163            Accessor::VerMajor => ver_range(v, 0)?,
164            Accessor::VerMinor => ver_range(v, 1)?,
165            Accessor::VerPatch => ver_range(v, 2)?,
166            Accessor::TsField(i) => ts_range(v, *i)?,
167            Accessor::PathDir => path_range(v, PathPart::Dir),
168            Accessor::PathName => path_range(v, PathPart::Name),
169            Accessor::PathExt => path_range(v, PathPart::Ext),
170            Accessor::QtyValue => 0..crate::quantity::split(v)?.0.len(),
171            Accessor::QtyUnit => v.len() - crate::quantity::split(v)?.1.len()..v.len(),
172        };
173        (!r.is_empty()).then_some(r)
174    }
175
176    fn apply(&self, v: &str) -> String {
177        match self {
178            Accessor::Upper => v.to_uppercase(),
179            Accessor::Lower => v.to_lowercase(),
180            _ => self.locate(v).map_or_else(String::new, |r| v[r].to_string()),
181        }
182    }
183}
184
185/// The accessors a template writes after a capture of a `kind` token to read
186/// exactly `part` of the token's text `text`: the typed accessors the kind
187/// has first, then `firstN` where the part runs from the text's start or
188/// `lastN` where it runs to the end. Empty where no accessor reads exactly
189/// that part.
190pub(crate) fn accessors_locating(kind: TokenKind, text: &str, part: Range<usize>) -> Vec<String> {
191    let mut typed: Vec<(String, Accessor)> = Vec::new();
192    let named = |names: &[(&str, Accessor)]| -> Vec<(String, Accessor)> {
193        names.iter().map(|(n, a)| ((*n).to_string(), a.clone())).collect()
194    };
195    match kind {
196        TokenKind::Url => typed = named(&[
197            ("scheme", Accessor::UrlScheme),
198            ("host", Accessor::UrlHost),
199            ("port", Accessor::UrlPort),
200            ("path", Accessor::UrlPath),
201            ("query", Accessor::UrlQuery),
202        ]),
203        TokenKind::Email => typed = named(&[("user", Accessor::EmailUser), ("domain", Accessor::EmailDomain)]),
204        TokenKind::Version => typed = named(&[
205            ("major", Accessor::VerMajor),
206            ("minor", Accessor::VerMinor),
207            ("patch", Accessor::VerPatch),
208        ]),
209        TokenKind::Timestamp => {
210            for (i, n) in ["year", "month", "day", "hour", "minute", "second"].into_iter().enumerate() {
211                typed.push((n.to_string(), Accessor::TsField(i)));
212            }
213        }
214        TokenKind::Path => typed = named(&[
215            ("dir", Accessor::PathDir),
216            ("name", Accessor::PathName),
217            ("ext", Accessor::PathExt),
218        ]),
219        TokenKind::Quantity | TokenKind::ByteSize | TokenKind::Duration | TokenKind::Percent => {
220            typed = named(&[("value", Accessor::QtyValue), ("unit", Accessor::QtyUnit)]);
221        }
222        TokenKind::Ip => {
223            for a in 1..=8 {
224                for b in a..=8 {
225                    let span = if a == b { a.to_string() } else { format!("{a}-{b}") };
226                    if b <= 4 {
227                        typed.push((format!("octet{span}"), Accessor::Octet(a, b)));
228                    }
229                    typed.push((format!("group{span}"), Accessor::Group(a, b)));
230                }
231            }
232        }
233        _ => {}
234    }
235    let mut found: Vec<String> =
236        typed.into_iter().filter(|(_, a)| a.locate(text) == Some(part.clone())).map(|(n, _)| n).collect();
237    let chars = text.get(part.clone()).map_or(0, |p| p.chars().count());
238    if chars > 0 && part.start == 0 && part.end < text.len() {
239        found.push(format!("first{chars}"));
240    }
241    if chars > 0 && part.end == text.len() && part.start > 0 {
242        found.push(format!("last{chars}"));
243    }
244    found
245}
246
247/// What the accessor pipeline `accs`, as a template writes it after the
248/// capture's name and `:`, renders from `text`.
249///
250/// # Errors
251///
252/// `accs` names an accessor a template does not know.
253pub(crate) fn apply_named(accs: &str, text: &str) -> Result<String, String> {
254    match parse_accessors(accs, 0) {
255        Ok(parsed) => Ok(apply_all(&parsed, text)),
256        Err(e) => Err(e.msg),
257    }
258}
259
260/// Whether a report template reads `${name}` as a field of the match's
261/// report (its file, place, pattern or rule) rather than as a capture, so a
262/// capture of that name is reached only by its position.
263pub(crate) fn is_report_field(name: &str) -> bool {
264    ReportField::parse(name).is_some()
265}
266
267/// Apply an accessor pipeline to a value, left to right.
268fn apply_all(accs: &[Accessor], v: &str) -> String {
269    let mut s = v.to_string();
270    for a in accs {
271        s = a.apply(&s);
272    }
273    s
274}
275
276/// The byte range of an accessor pipeline's value within `v`, each accessor
277/// reading the slice the one before it located; `None` where any accessor
278/// transforms rather than slices, or finds nothing.
279fn locate_all(accs: &[Accessor], v: &str) -> Option<ByteRange> {
280    let mut r = 0..v.len();
281    for a in accs {
282        let sub = a.locate(&v[r.clone()])?;
283        r = r.start + sub.start..r.start + sub.end;
284    }
285    (!r.is_empty()).then_some(r)
286}
287
288/// The byte range of the 1-based inclusive run of parts `a..=b` of `v`
289/// split on `sep`, the separators between them included; `None` when the
290/// range is out of bounds.
291fn parts_range(v: &str, sep: char, a: usize, b: usize) -> Option<ByteRange> {
292    if a == 0 || b < a {
293        return None;
294    }
295    let mut parts: Vec<ByteRange> = Vec::new();
296    let mut start = 0;
297    for (i, _) in v.match_indices(sep) {
298        parts.push(start..i);
299        start = i + sep.len_utf8();
300    }
301    parts.push(start..v.len());
302    if b > parts.len() {
303        return None;
304    }
305    Some(parts[a - 1].start..parts[b - 1].end)
306}
307
308/// The part of a URL an accessor or a typed predicate reads.
309pub(crate) enum UrlPart {
310    Scheme,
311    Host,
312    Port,
313    Path,
314    Query,
315}
316
317/// The byte range of one part of `scheme://host[:port][/path][?query]`
318/// within `v`, empty where the part is absent.
319fn url_range(v: &str, part: UrlPart) -> ByteRange {
320    let (scheme_end, rest_start) = match v.find("://") {
321        Some(i) => (i, i + 3),
322        None => (0, 0),
323    };
324    let rest = &v[rest_start..];
325    let auth_end = rest.find(['/', '?']).unwrap_or(rest.len());
326    let authority = &rest[..auth_end];
327    let host_end = match authority.rsplit_once(':') {
328        Some((h, p)) if !p.is_empty() && p.bytes().all(|c| c.is_ascii_digit()) => h.len(),
329        _ => auth_end,
330    };
331    let after = &rest[auth_end..];
332    let path_end = after.find('?').unwrap_or(after.len());
333    let at = |i: usize| rest_start + i;
334    match part {
335        UrlPart::Scheme => 0..scheme_end,
336        UrlPart::Host => at(0)..at(host_end),
337        UrlPart::Port if host_end < auth_end => at(host_end + 1)..at(auth_end),
338        UrlPart::Path => at(auth_end)..at(auth_end + path_end),
339        UrlPart::Query if path_end < after.len() => at(auth_end + path_end + 1)..v.len(),
340        UrlPart::Port | UrlPart::Query => 0..0,
341    }
342}
343
344/// Decompose `scheme://host[:port][/path][?query]` into one part.
345pub(crate) fn url_part(v: &str, part: UrlPart) -> String {
346    v[url_range(v, part)].to_string()
347}
348
349/// The byte range of the i-th dotted field of `MAJOR.MINOR.PATCH`, before
350/// any `-pre` or `+build`.
351fn ver_range(v: &str, i: usize) -> Option<ByteRange> {
352    let core_end = v.find(['-', '+']).unwrap_or(v.len());
353    parts_range(&v[..core_end], '.', i + 1, i + 1)
354}
355
356/// The byte ranges of the runs of digits in `v`, in order.
357fn digit_runs(v: &str) -> Vec<ByteRange> {
358    let b = v.as_bytes();
359    let mut runs = Vec::new();
360    let mut j = 0;
361    while j < b.len() {
362        if !b[j].is_ascii_digit() {
363            j += 1;
364            continue;
365        }
366        let start = j;
367        while j < b.len() && b[j].is_ascii_digit() {
368            j += 1;
369        }
370        runs.push(start..j);
371    }
372    runs
373}
374
375/// The byte range of one calendar field of a timestamp - year, month, day,
376/// hour, minute, second, in that order - where the form the text is written
377/// in puts it.
378///
379/// Read from the form rather than by counting digit runs, because the runs
380/// stand in a different order in every form a log writes: `15/09/2026` opens
381/// with the day, Apache's `15/Sep/2026:10:00:00` writes the year third, and
382/// a bare clock writes no date at all, so its first run is the hour. A field
383/// the text does not write in digits - syslog's named month, the year it
384/// leaves out - has no range and renders empty.
385fn ts_range(v: &str, i: usize) -> Option<ByteRange> {
386    let b = v.as_bytes();
387    let runs = digit_runs(v);
388    // Which run holds the year, the month and the day, and which run the
389    // clock's hour stands at after them.
390    let (date, clock) = if crate::typed::month_abbrev(b).is_some() && b.get(3) == Some(&b' ') {
391        ((None, None, Some(0)), 1)
392    } else if b.get(2) == Some(&b'/')
393        && b.get(3..).is_some_and(|rest| crate::typed::month_abbrev(rest).is_some())
394    {
395        ((Some(1), None, Some(0)), 2)
396    } else if let Some(d) = crate::typed::slash_date(b, 0) {
397        let (year, month, day) = d.runs;
398        ((Some(year), Some(month), Some(day)), 3)
399    } else if runs.first().is_some_and(|r| r.len() == 4) && b.get(4) == Some(&b'-') {
400        ((Some(0), Some(1), Some(2)), 3)
401    } else {
402        ((None, None, None), 0)
403    };
404    let run = match i {
405        0 => date.0?,
406        1 => date.1?,
407        2 => date.2?,
408        _ => clock + i - 3,
409    };
410    runs.get(run).cloned()
411}
412
413/// The part of a filesystem path an accessor or a typed predicate reads.
414pub(crate) enum PathPart {
415    Dir,
416    Name,
417    Ext,
418}
419
420/// The byte range of one part of a filesystem path within `v`: its
421/// directory, basename, or extension, empty where the part is absent.
422fn path_range(v: &str, part: PathPart) -> ByteRange {
423    let sep = if v.contains('\\') { '\\' } else { '/' };
424    let name_start = v.rfind(sep).map_or(0, |i| i + 1);
425    match part {
426        PathPart::Dir => 0..name_start.saturating_sub(1),
427        PathPart::Name => name_start..v.len(),
428        PathPart::Ext => match v[name_start..].rfind('.') {
429            Some(dot) => name_start + dot + 1..v.len(),
430            None => 0..0,
431        },
432    }
433}
434
435/// Decompose a filesystem path into its directory, basename, or extension.
436pub(crate) fn path_field(v: &str, part: PathPart) -> String {
437    v[path_range(v, part)].to_string()
438}
439
440/// One piece of a parsed template.
441#[derive(Debug)]
442enum Part {
443    Literal(String),
444    /// `${0}`: the whole matched span, with an optional accessor pipeline.
445    WholeMatch(Vec<Accessor>),
446    /// `${name}`: a named capture, with an optional accessor pipeline.
447    Capture(String, Vec<Accessor>),
448    /// `${name[i]}`: the i-th binding of a register bound under a
449    /// repetition, counted from zero, with an optional accessor pipeline.
450    Item(String, usize, Vec<Accessor>),
451    /// `${name[*]}`: every binding of a register, each through the accessor
452    /// pipeline, joined with a comma.
453    All(String, Vec<Accessor>),
454    /// `${path}` and the rest: where the match stands, in a report's
455    /// template, with an optional accessor pipeline.
456    Where(ReportField, Vec<Accessor>),
457    /// `${@axis}`, `${@axis.piece}`, `${@axis[i]}`: a reading the explainer
458    /// computes at the tokens the match spans, with an optional accessor
459    /// pipeline.
460    Explain(ExplainRef, Vec<Accessor>),
461}
462
463/// What a `${@...}` reference names: an axis the explainer reads, the piece
464/// of its reading the reference asks for, and which token's reading where it
465/// carries an index.
466///
467/// The `@` is what keeps this apart from a register: a template is validated
468/// against the pattern's capture names, so a bare `${kind}` would name the
469/// register a pattern writing `:kind` binds, and the same template text
470/// would mean one thing under one pattern and another under the next with
471/// nothing reporting the difference. `@` already reads as "an axis" in the
472/// pattern language, where `@echo`, `@seam` and `@k` are axes.
473#[derive(Clone, Debug, PartialEq, Eq)]
474pub struct ExplainRef {
475    /// The axis, or one of `kind`, `guard` and `route`, which the
476    /// explanation carries whole rather than at each token.
477    pub axis: String,
478    /// The value of that axis's reading the reference names, where it names
479    /// one rather than the whole sentence.
480    pub piece: Option<String>,
481    /// Which token's reading, counted from zero over the tokens the match
482    /// spans. Without one the reference renders every reading, joined.
483    pub at: Option<usize>,
484}
485
486/// Where a match stands, as a report's template writes it.
487#[derive(Clone, Copy, Debug, PartialEq, Eq)]
488pub enum ReportField {
489    /// The input's name: its path, `-` for the standard input, nothing for
490    /// an inline string.
491    Path,
492    /// The line the match begins on, from one.
493    Line,
494    /// The column the match begins at, from one.
495    Col,
496    /// The byte offset the match begins at.
497    Start,
498    /// The byte offset just past it.
499    End,
500    /// The member of a set the match belongs to, by name, where the scan
501    /// ran a set; nothing otherwise.
502    Pattern,
503    /// The rule a finding is of, by name, where the scan ran rules; nothing
504    /// otherwise.
505    Rule,
506    /// The finding's severity.
507    Severity,
508    /// The finding's message, rendered.
509    Message,
510    /// The finding's fix, rendered; nothing where the rule has none.
511    Fix,
512}
513
514impl ReportField {
515    fn parse(name: &str) -> Option<ReportField> {
516        Some(match name {
517            "path" => ReportField::Path,
518            "line" => ReportField::Line,
519            "col" => ReportField::Col,
520            "start" => ReportField::Start,
521            "end" => ReportField::End,
522            "pattern" => ReportField::Pattern,
523            "rule" => ReportField::Rule,
524            "severity" => ReportField::Severity,
525            "message" => ReportField::Message,
526            "fix" => ReportField::Fix,
527            _ => return None,
528        })
529    }
530}
531
532/// The rule a finding is of, for a report's template to write: its name,
533/// its severity, and the message and fix rendered for the finding.
534#[derive(Clone, Copy, Debug, PartialEq, Eq)]
535pub struct ReportRule<'a> {
536    pub name: &'a str,
537    pub severity: &'a str,
538    pub message: &'a str,
539    pub fix: &'a str,
540}
541
542/// Where one match stands, for a report's template to write.
543#[derive(Clone, Copy, Debug, PartialEq, Eq)]
544pub struct ReportAt<'a> {
545    /// The input's name.
546    pub path: &'a str,
547    /// The line the match begins on, from one.
548    pub line: usize,
549    /// The column the match begins at, from one.
550    pub col: usize,
551    /// What the match's own offsets are added to: the place of the window
552    /// it was found in, in a longer input; `None` where that place was not
553    /// counted, which only a template writing no offset is rendered over.
554    pub base: Option<usize>,
555    /// The member of a set the match belongs to, where the scan ran one.
556    pub pattern: Option<&'a str>,
557    /// The rule the match is a finding of, where the scan ran rules.
558    pub rule: Option<ReportRule<'a>>,
559}
560
561impl ReportAt<'_> {
562    /// Where the window stands, for a template writing an offset.
563    ///
564    /// # Panics
565    ///
566    /// The place was not counted: a report whose template writes an offset
567    /// asks for it, so this is a template writing an offset its report chose
568    /// not to count.
569    fn placed(&self) -> usize {
570        self.base.expect("a window is placed wherever a template writes its offsets")
571    }
572
573    fn value<M: Spanned>(&self, field: ReportField, m: &M) -> String {
574        match field {
575            ReportField::Path => self.path.to_string(),
576            ReportField::Line => self.line.to_string(),
577            ReportField::Col => self.col.to_string(),
578            ReportField::Start => (m.start() + self.placed()).to_string(),
579            ReportField::End => (m.end() + self.placed()).to_string(),
580            ReportField::Pattern => self.pattern.unwrap_or("").to_string(),
581            ReportField::Rule => self.rule.map_or("", |r| r.name).to_string(),
582            ReportField::Severity => self.rule.map_or("", |r| r.severity).to_string(),
583            ReportField::Message => self.rule.map_or("", |r| r.message).to_string(),
584            ReportField::Fix => self.rule.map_or("", |r| r.fix).to_string(),
585        }
586    }
587}
588
589/// A parsed replacement template.
590#[derive(Debug)]
591pub struct Template {
592    parts: Vec<Part>,
593}
594
595/// A template parse failure: the byte offset into the template and a
596/// reason.
597#[derive(Clone, Debug, PartialEq, Eq)]
598pub struct TemplateError {
599    /// Byte offset into the template string where parsing stopped.
600    pub pos: usize,
601    /// Human-readable reason.
602    pub msg: String,
603}
604
605impl Template {
606    /// Parse `src` into a template, validating every `${name}` against
607    /// `bound` (the pattern's capture names). `${0}` is always valid.
608    ///
609    /// # Errors
610    ///
611    /// Returns a [`TemplateError`] for an unterminated `${...}`, an
612    /// unknown transform, or a capture name the pattern does not bind.
613    pub fn parse(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
614        Self::parse_full(src, bound, false)
615    }
616
617    /// [`Self::parse`] for a report's template, which may also write where
618    /// the match stands - `${path}`, `${line}`, `${col}`, `${start}`,
619    /// `${end}` - and the readings `--explain` computes for it,
620    /// `${@axis}` and `${@axis.piece}`. Both take accessors.
621    ///
622    /// A template written for a rewrite takes neither, because the bytes
623    /// spliced in have no place and no explanation to read.
624    ///
625    /// # Errors
626    ///
627    /// As [`Self::parse`], and an axis or a piece of one that the explainer
628    /// does not read.
629    pub fn parse_report(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
630        Self::parse_full(src, bound, true)
631    }
632
633    fn parse_full(src: &str, bound: &[String], report: bool) -> Result<Template, TemplateError> {
634        let b = src.as_bytes();
635        let mut parts: Vec<Part> = Vec::new();
636        let mut lit = String::new();
637        let mut i = 0;
638        while i < b.len() {
639            match b[i] {
640                b'$' if i + 1 < b.len() && b[i + 1] == b'$' => {
641                    lit.push('$');
642                    i += 2;
643                }
644                b'$' if i + 1 < b.len() && b[i + 1] == b'{' => {
645                    if !lit.is_empty() {
646                        parts.push(Part::Literal(std::mem::take(&mut lit)));
647                    }
648                    let start = i;
649                    let close = b[i + 2..]
650                        .iter()
651                        .position(|&c| c == b'}')
652                        .map(|p| i + 2 + p)
653                        .ok_or(TemplateError { pos: start, msg: "unterminated ${...}".into() })?;
654                    let body = &src[i + 2..close];
655                    let (name, accs) = match body.split_once(':') {
656                        Some((n, a)) => (n, a),
657                        None => (body, ""),
658                    };
659                    match ReportField::parse(name) {
660                        Some(field) if report => {
661                            let accs = if accs.is_empty() { Vec::new() } else { parse_accessors(accs, start)? };
662                            parts.push(Part::Where(field, accs));
663                        }
664                        _ => parts.push(parse_ref(body, start, bound, report)?),
665                    }
666                    i = close + 1;
667                }
668                b'\\' => {
669                    let escaped = match b.get(i + 1) {
670                        Some(b'n') => '\n',
671                        Some(b't') => '\t',
672                        Some(b'\\') => '\\',
673                        Some(&other) => {
674                            return Err(TemplateError {
675                                pos: i,
676                                msg: format!(
677                                    "unknown escape \\{}; a template knows \\n, \\t and \\\\",
678                                    other as char
679                                ),
680                            });
681                        }
682                        None => {
683                            return Err(TemplateError {
684                                pos: i,
685                                msg: "a backslash ends the template; write \\\\ for a backslash".into(),
686                            });
687                        }
688                    };
689                    lit.push(escaped);
690                    i += 2;
691                }
692                c => {
693                    lit.push(c as char);
694                    i += 1;
695                }
696            }
697        }
698        if !lit.is_empty() {
699            parts.push(Part::Literal(lit));
700        }
701        Ok(Template { parts })
702    }
703
704    /// Render this template for one match over `input`.
705    ///
706    /// A rewrite splices the render into the match's place; a caller
707    /// grouping matches by a key renders the same way and keeps the string.
708    #[must_use]
709    pub fn render<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
710        self.render_at(m, input, None, None)
711    }
712
713    /// Render a report's template for one match, `at` being where the
714    /// match stands.
715    #[must_use]
716    pub fn render_report<M: Spanned>(&self, m: &M, input: &[u8], at: &ReportAt<'_>) -> String {
717        self.render_at(m, input, Some(at), None)
718    }
719
720    /// Render this template for one match, `why` being the explanation of
721    /// that match, which every `${@...}` part reads.
722    ///
723    /// A template [`Self::reads_explanation`] rejects renders the same bytes
724    /// through [`Self::render`], so a caller builds the explainer only for
725    /// the templates that name an axis.
726    #[must_use]
727    pub fn render_explained<M: Spanned>(
728        &self,
729        m: &M,
730        input: &[u8],
731        at: Option<&ReportAt<'_>>,
732        why: &Explanation,
733    ) -> String {
734        self.render_at(m, input, at, Some(why))
735    }
736
737    fn render_at<M: Spanned>(
738        &self,
739        m: &M,
740        input: &[u8],
741        at: Option<&ReportAt<'_>>,
742        why: Option<&Explanation>,
743    ) -> String {
744        let mut out = String::new();
745        for part in &self.parts {
746            match part {
747                Part::Literal(s) => out.push_str(s),
748                // A place is written only where a report supplies one; a
749                // template parsed for a rewrite never holds a place.
750                Part::Where(field, accs) => {
751                    let value = at.map_or_else(String::new, |a| a.value(*field, m));
752                    out.push_str(&apply_all(accs, &value));
753                }
754                // An axis is read only where the caller supplies an
755                // explanation, which it builds when the template names one.
756                Part::Explain(named, accs) => {
757                    let value = why.map_or_else(String::new, |e| {
758                        e.field(&named.axis, named.piece.as_deref(), named.at)
759                    });
760                    out.push_str(&apply_all(accs, &value));
761                }
762                Part::WholeMatch(accs) => {
763                    let whole = String::from_utf8_lossy(&input[m.start()..m.end()]);
764                    out.push_str(&apply_all(accs, &whole));
765                }
766                Part::Capture(name, accs) => {
767                    let bytes = m
768                        .names()
769                        .iter()
770                        .position(|k| k == name)
771                        .and_then(|i| m.captures().get(i))
772                        .map_or(&[] as &[u8], |s| &input[s.range()]);
773                    let v = String::from_utf8_lossy(bytes);
774                    out.push_str(&apply_all(accs, &v));
775                }
776                Part::Item(name, index, accs) => {
777                    let bytes = m
778                        .history(name)
779                        .and_then(|all| all.get(*index))
780                        .map_or(&[] as &[u8], |s| &input[s.range()]);
781                    let v = String::from_utf8_lossy(bytes);
782                    out.push_str(&apply_all(accs, &v));
783                }
784                Part::All(name, accs) => out.push_str(&every_binding(m, input, name, accs)),
785            }
786        }
787        out
788    }
789
790    /// Whether a part reads a named capture, so the matches rendered must
791    /// carry their registers.
792    fn reads_captures(&self) -> bool {
793        self.parts.iter().any(|p| matches!(p, Part::Capture(..) | Part::Item(..) | Part::All(..)))
794    }
795
796    /// Whether a part reads one binding of a register bound under a
797    /// repetition by its index, so the matches rendered must carry every
798    /// binding, as [`captures_with_lists`] resolves them.
799    #[must_use]
800    pub fn reads_lists(&self) -> bool {
801        self.parts.iter().any(|p| matches!(p, Part::Item(..) | Part::All(..)))
802    }
803
804    /// Whether a part writes the line or column a match stands at, so a
805    /// report rendering it must count them.
806    #[must_use]
807    pub fn reads_place(&self) -> bool {
808        self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Line | ReportField::Col, _)))
809    }
810
811    /// Whether a part writes the offset a match starts or ends at, so a
812    /// report rendering it over a window must place the window.
813    #[must_use]
814    pub fn reads_offsets(&self) -> bool {
815        self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Start | ReportField::End, _)))
816    }
817
818    /// Whether a part names an axis, so the caller must build an explainer
819    /// over the input and render through [`Self::render_explained`].
820    ///
821    /// This is what keeps a template that names none costing what it always
822    /// did: the analyses behind the axes are built once per input, and only
823    /// where this answers true.
824    #[must_use]
825    pub fn reads_explanation(&self) -> bool {
826        self.parts.iter().any(|p| matches!(p, Part::Explain(..)))
827    }
828
829    /// Whether every match renders the same bytes: no part reads the match or a
830    /// register it bound, so the render is the template's literals and nothing
831    /// else.
832    ///
833    /// Stricter than [`Self::reads_captures`], which asks whether the matches
834    /// must carry their registers: `${0}` carries none and still renders
835    /// differently at every match.
836    fn renders_one_string(&self) -> bool {
837        self.parts.iter().all(|p| matches!(p, Part::Literal(_)))
838    }
839
840    /// The bytes this template renders at every match, for a template
841    /// [`Self::renders_one_string`] accepts.
842    fn one_string(&self) -> String {
843        self.parts
844            .iter()
845            .map(|p| match p {
846                Part::Literal(s) => s.as_str(),
847                Part::WholeMatch(_)
848                | Part::Capture(..)
849                | Part::Item(..)
850                | Part::All(..)
851                | Part::Where(..)
852                | Part::Explain(..) => "",
853            })
854            .collect()
855    }
856}
857
858/// The count `firstN` / `lastN` writes: a positive integer.
859fn parse_count(r: &str, name: &str, pos: usize) -> Result<usize, TemplateError> {
860    match r.parse::<usize>() {
861        Ok(n) if n > 0 => Ok(n),
862        Ok(_) => Err(TemplateError {
863            pos,
864            msg: format!("{name}0 names no characters; write {name}N with N at least 1"),
865        }),
866        Err(e) => Err(TemplateError {
867            pos,
868            msg: format!("{name}{r}: {e}; write {name}N, as in {name}4"),
869        }),
870    }
871}
872
873/// One field a redaction leaves readable: the whole match or a named
874/// register, and the slicing accessors that locate the field within it.
875/// Written as a reference is written inside `${...}`: `card:last4`,
876/// `ip:octet1-2`, `email:domain`, `0:last4`.
877#[derive(Clone, Debug)]
878pub struct Keep {
879    field: Field,
880    accessors: Vec<Accessor>,
881}
882
883impl Keep {
884    /// Parse a comma-separated list of kept fields against the pattern's
885    /// capture names; an empty list keeps nothing.
886    ///
887    /// # Errors
888    ///
889    /// A reference the pattern does not bind, an unknown accessor, or a
890    /// transforming accessor (`upper`, `lower`), whose value has no place in
891    /// the original text.
892    pub fn parse_list(src: &str, bound: &[String]) -> Result<Vec<Keep>, TemplateError> {
893        let mut out = Vec::new();
894        let mut pos = 0;
895        for entry in src.split(',') {
896            let at = pos + (entry.len() - entry.trim_start().len());
897            pos += entry.len() + 1;
898            let body = entry.trim();
899            if body.is_empty() {
900                continue;
901            }
902            let (field, accessors) = parse_field(body, at, bound)?;
903            if let Some(transform) = accessors.iter().find(|a| !a.slices()) {
904                let name = if *transform == Accessor::Upper { "upper" } else { "lower" };
905                return Err(TemplateError {
906                    pos: at,
907                    msg: format!(
908                        "`:{name}` transforms the text rather than slicing it, so it names nothing to keep in place"
909                    ),
910                });
911            }
912            out.push(Keep { field, accessors });
913        }
914        Ok(out)
915    }
916
917    /// The byte range of this field within `input` for one match: `None`
918    /// where the match did not bind the register, the field is absent, the
919    /// span is not UTF-8, whose offsets no character count reaches, or the
920    /// field is every binding, which stand at several ranges and not one.
921    fn locate<M: Spanned>(&self, m: &M, input: &[u8]) -> Option<ByteRange> {
922        let base = match &self.field {
923            Field::All(_) => return None,
924            Field::Whole => m.start()..m.end(),
925            Field::Register(name) => {
926                let i = m.names().iter().position(|k| k == name)?;
927                m.captures().get(i)?.range()
928            }
929            Field::Item(name, index) => m.history(name)?.get(*index)?.range(),
930        };
931        let text = match String::from_utf8_lossy(&input[base.clone()]) {
932            Cow::Borrowed(t) => t,
933            Cow::Owned(_) => return None,
934        };
935        let r = locate_all(&self.accessors, text)?;
936        Some(base.start + r.start..base.start + r.end)
937    }
938}
939
940/// The digit every digit is masked to, and the letter every letter is.
941///
942/// Measured against the lexer rather than chosen for looks, which is what
943/// decided the letter: `a` is both a letter and a hex digit, so one rule
944/// covers both families. Masking to `x` keeps an email and a word reading as
945/// themselves and breaks every hex-shaped kind - `#A3F2B1` becomes `#x0xxxx`,
946/// which is a punctuation mark and a word rather than a color, and a uuid and
947/// a mac fall apart into a dozen tokens each. With `a` the shapes hold:
948/// hexcolor, uuid, mac, email, ip, timestamp, version and url all still lex
949/// as their kind.
950const MASKED_DIGIT: u8 = b'0';
951const MASKED_LETTER: u8 = b'a';
952
953/// The book a pseudonym mask keeps: for each kind, the values seen under it
954/// in the order they were first seen.
955///
956/// One book for a whole run, so a value gives the same name in every input of
957/// it - which is what lets a reader join the redacted copies of two files
958/// that shared a value.
959#[derive(Clone, Debug, Default, PartialEq, Eq)]
960pub struct Pseudonyms {
961    seen: Vec<(String, Vec<Vec<u8>>)>,
962}
963
964impl Pseudonyms {
965    /// The name `value` stands under, minting one where it is new.
966    fn name(&mut self, kind: &str, value: &[u8]) -> String {
967        let values = match self.seen.iter().position(|(k, _)| k == kind) {
968            Some(at) => &mut self.seen[at].1,
969            None => {
970                self.seen.push((kind.to_string(), Vec::new()));
971                let last = self.seen.len() - 1;
972                &mut self.seen[last].1
973            }
974        };
975        let at = match values.iter().position(|v| v == value) {
976            Some(at) => at,
977            None => {
978                values.push(value.to_vec());
979                values.len() - 1
980            }
981        };
982        // Numbered from one, in order of first sight, and the kind in
983        // capitals. The whole name is one Word token to the lexer, which is
984        // what carries the recurrence into the redacted copy: a name that lexed
985        // as several tokens would lose exactly what this mask exists to keep.
986        format!("{}_{}", kind.to_uppercase(), at + 1)
987    }
988}
989
990/// What a redaction writes over the characters it removes.
991#[derive(Clone, Debug, PartialEq, Eq)]
992pub enum Mask {
993    /// One copy of the character for each character removed, so every
994    /// offset and column after the span survives.
995    PerChar(char),
996    /// One copy of the token for each masked run, whatever the run's length.
997    Token(String),
998    /// Every letter and digit masked and every other byte kept, so the run
999    /// keeps the shape its kind is recognized by and the redacted copy still
1000    /// lexes as the original did.
1001    Shape,
1002    /// Each distinct value replaced by a stable name per kind, so a value
1003    /// that recurred still recurs and the axes that read recurrence - echo,
1004    /// the joins, the templates - read the redacted copy as they read the
1005    /// original.
1006    Pseudonym(Pseudonyms),
1007}
1008
1009impl Mask {
1010    /// A one-character string masks per character; `shape` and `pseudonym`
1011    /// name the two masks that read the run; any other longer string is the
1012    /// token each masked run becomes.
1013    ///
1014    /// # Errors
1015    ///
1016    /// An empty mask, which would leave the removed characters nothing in
1017    /// their place.
1018    pub fn parse(src: &str) -> Result<Mask, String> {
1019        if src == "shape" {
1020            return Ok(Mask::Shape);
1021        }
1022        if src == "pseudonym" {
1023            return Ok(Mask::Pseudonym(Pseudonyms::default()));
1024        }
1025        let mut chars = src.chars();
1026        match (chars.next(), chars.next()) {
1027            (None, _) => Err("the mask is empty; give a character or a token".to_string()),
1028            (Some(c), None) => Ok(Mask::PerChar(c)),
1029            (Some(_), Some(_)) => Ok(Mask::Token(src.to_string())),
1030        }
1031    }
1032
1033    /// The kind a masked run reads as, for the masks that need one: the kind
1034    /// of its only significant token as `lexing` lexes it, a declared or
1035    /// library kind by the name its declaration gives it, or `value` where
1036    /// the run is not one token.
1037    fn kind_of(run: &[u8], lexing: &crate::ShapeSet) -> String {
1038        let toks = if lexing.is_empty() {
1039            crate::lexer::lex(run)
1040        } else {
1041            crate::lexer::lex_with_shapes(run, &crate::lexer::blob_runs(run), lexing, 0)
1042        };
1043        let mut significant = toks.iter().filter(|t| t.is_significant());
1044        match (significant.next(), significant.next()) {
1045            (Some(one), None) => match one.kind {
1046                crate::token::TokenKind::Custom(id) => match lexing.name_of(id) {
1047                    Some(name) => name.to_string(),
1048                    None => one.kind.name().to_string(),
1049                },
1050                other => other.name().to_string(),
1051            },
1052            _ => "value".to_string(),
1053        }
1054    }
1055
1056    /// Write what stands in for the bytes of `run`, which is never empty,
1057    /// reading its kind under `lexing` where the mask names the kind.
1058    fn cover(&mut self, run: &[u8], out: &mut Vec<u8>, lexing: &crate::ShapeSet) {
1059        match self {
1060            Mask::PerChar(c) => {
1061                let mut buf = [0u8; 4];
1062                let encoded = c.encode_utf8(&mut buf).as_bytes();
1063                let chars = run.iter().filter(|&&b| b & 0xC0 != 0x80).count();
1064                for _ in 0..chars {
1065                    out.extend_from_slice(encoded);
1066                }
1067            }
1068            Mask::Token(t) => out.extend_from_slice(t.as_bytes()),
1069            Mask::Shape => out.extend(run.iter().map(|&b| match b {
1070                b'0'..=b'9' => MASKED_DIGIT,
1071                b'A'..=b'Z' | b'a'..=b'z' => MASKED_LETTER,
1072                other => other,
1073            })),
1074            Mask::Pseudonym(book) => {
1075                let kind = Mask::kind_of(run, lexing);
1076                out.extend_from_slice(book.name(&kind, run).as_bytes());
1077            }
1078        }
1079    }
1080}
1081
1082/// The edits that redact `matches` over `input`: each match's span becomes
1083/// the mask, with the fields `keeps` locate left where they stand. A kept
1084/// field outside its match, which a register bound in an assertion can be,
1085/// is clipped to the match; overlapping fields are kept once. A pseudonym
1086/// reads each run's kind with no declarations; a pattern compiled against
1087/// some redacts through [`redactions_with_shapes`].
1088#[must_use]
1089pub fn redactions<M: Spanned>(
1090    input: &[u8],
1091    matches: &[M],
1092    keeps: &[Keep],
1093    mask: &mut Mask,
1094) -> Vec<crate::files::Edit> {
1095    redactions_under(input, matches, keeps, mask, &crate::ShapeSet::new())
1096}
1097
1098/// As [`redactions`], for matches of `pattern` compiled against `shapes`: a
1099/// pseudonym names a run of a declared shape or kind, or of a library kind
1100/// the pattern names, by that kind's own name, as `CUSTOMER_1`.
1101#[must_use]
1102pub fn redactions_with_shapes<M: Spanned>(
1103    input: &[u8],
1104    matches: &[M],
1105    keeps: &[Keep],
1106    mask: &mut Mask,
1107    pattern: &Pattern,
1108    shapes: &crate::ShapeSet,
1109) -> Vec<crate::files::Edit> {
1110    redactions_under(input, matches, keeps, mask, &shapes.with_library_shapes(&pattern.library_kinds()))
1111}
1112
1113/// The redaction both forms make, a run's kind read under `lexing`.
1114fn redactions_under<M: Spanned>(
1115    input: &[u8],
1116    matches: &[M],
1117    keeps: &[Keep],
1118    mask: &mut Mask,
1119    lexing: &crate::ShapeSet,
1120) -> Vec<crate::files::Edit> {
1121    matches
1122        .iter()
1123        .map(|m| {
1124            let (start, end) = (m.start(), m.end());
1125            let mut kept: Vec<ByteRange> = keeps
1126                .iter()
1127                .filter_map(|k| k.locate(m, input))
1128                .map(|r| r.start.max(start)..r.end.min(end))
1129                .filter(|r| !r.is_empty())
1130                .collect();
1131            kept.sort_by_key(|r| (r.start, r.end));
1132            let mut replacement = Vec::with_capacity(end - start);
1133            let mut cursor = start;
1134            for r in kept {
1135                if r.start > cursor {
1136                    mask.cover(&input[cursor..r.start], &mut replacement, lexing);
1137                }
1138                if r.end > cursor {
1139                    replacement.extend_from_slice(&input[r.start.max(cursor)..r.end]);
1140                    cursor = r.end;
1141                }
1142            }
1143            if cursor < end {
1144                mask.cover(&input[cursor..end], &mut replacement, lexing);
1145            }
1146            crate::files::Edit { start, end, replacement }
1147        })
1148        .collect()
1149}
1150
1151/// A match as a render reads it: its byte range and the registers it bound,
1152/// none for a plain span.
1153pub trait Spanned: Sync {
1154    /// The byte offset the match begins at.
1155    fn start(&self) -> usize;
1156    /// The byte offset just past it.
1157    fn end(&self) -> usize;
1158    /// The register spans, in the order [`Self::names`] holds their names.
1159    fn captures(&self) -> &[Span];
1160    /// The register names, which belong to the pattern rather than to any one
1161    /// of its matches.
1162    fn names(&self) -> &[String];
1163    /// Every binding the register called `name` made, oldest first, where it
1164    /// is bound under a repetition; `None` where it is not, or where the
1165    /// match carries no such bindings.
1166    fn history(&self, _name: &str) -> Option<&[Span]> {
1167        None
1168    }
1169}
1170
1171impl Spanned for Span {
1172    fn start(&self) -> usize {
1173        Span::start(self)
1174    }
1175
1176    fn end(&self) -> usize {
1177        Span::end(self)
1178    }
1179
1180    fn captures(&self) -> &[Span] {
1181        &[]
1182    }
1183
1184    fn names(&self) -> &[String] {
1185        &[]
1186    }
1187}
1188
1189impl Spanned for Match {
1190    fn start(&self) -> usize {
1191        self.start
1192    }
1193
1194    fn end(&self) -> usize {
1195        self.end
1196    }
1197
1198    fn captures(&self) -> &[Span] {
1199        &self.captures
1200    }
1201
1202    fn names(&self) -> &[String] {
1203        Match::names(self)
1204    }
1205
1206    fn history(&self, name: &str) -> Option<&[Span]> {
1207        self.list(name)
1208    }
1209}
1210
1211/// Parse the body of a `${...}` reference: a name (`0` for the whole
1212/// match) and an optional `:transform`.
1213/// What a `${...}` reference names: the whole match, or one register.
1214#[derive(Clone, Debug, PartialEq, Eq)]
1215pub enum Field {
1216    /// `${0}`: the whole matched span.
1217    Whole,
1218    /// `${name}` or `${n}`: the register the pattern binds under that name,
1219    /// or at that position.
1220    Register(String),
1221    /// `${name[i]}`, `${a[i].b}`: the i-th binding, from zero, of a register
1222    /// bound under a repetition.
1223    Item(String, usize),
1224    /// `${name[*]}`: every binding of a register, joined with a comma; the
1225    /// one binding of a register bound once.
1226    All(String),
1227}
1228
1229/// Every binding of the register `name` in `m`, each read through `accs`,
1230/// joined with a comma: the bindings a repetition made, or the register's
1231/// one binding where it was bound once.
1232fn every_binding<M: Spanned>(m: &M, input: &[u8], name: &str, accs: &[Accessor]) -> String {
1233    let read = |range: std::ops::Range<usize>| apply_all(accs, &String::from_utf8_lossy(&input[range]));
1234    match m.history(name) {
1235        Some(all) => all.iter().map(|s| read(s.range())).collect::<Vec<_>>().join(","),
1236        None => match m.names().iter().position(|k| k == name).and_then(|i| m.captures().get(i)) {
1237            Some(s) => read(s.range()),
1238            None => String::new(),
1239        },
1240    }
1241}
1242
1243/// The `${@...}` reference `body` writes, or `None` where it writes no `@`
1244/// and so names a register.
1245///
1246/// The axis and the piece are checked here, against the axes the explainer
1247/// reads, so `${@entrpoy}` stops at parse time as a misspelled register
1248/// name does. An axis the pattern never reads is not an error: it renders
1249/// empty, which is what the match has to say about it.
1250fn parse_explain(body: &str, pos: usize, allowed: bool) -> Result<Option<Part>, TemplateError> {
1251    if !body.starts_with('@') {
1252        return Ok(None);
1253    }
1254    if !allowed {
1255        return Err(TemplateError {
1256            pos,
1257            msg: format!(
1258                "${{{body}}} reads an axis, which a scan's --format renders; \
1259                 the bytes a rewrite splices in have no explanation to read"
1260            ),
1261        });
1262    }
1263    let (name, accs) = match body.split_once(':') {
1264        Some((n, a)) => (n, parse_accessors(a, pos)?),
1265        None => (body, Vec::new()),
1266    };
1267    let (name, at) = match split_index(&name[1..], pos)? {
1268        (name, Some(Pick::All)) => {
1269            return Err(TemplateError {
1270                pos,
1271                msg: format!("${{@{name}[*]}}: an axis written bare already joins every reading; [i] picks one"),
1272            });
1273        }
1274        (name, Some(Pick::At(i))) => (name, Some(i)),
1275        (name, None) => (name, None),
1276    };
1277    let (axis, piece) = match name.split_once('.') {
1278        Some((axis, piece)) => (axis, Some(piece)),
1279        None => (name.as_str(), None),
1280    };
1281    crate::explain::check_explain_field(axis, piece)
1282        .map_err(|msg| TemplateError { pos, msg })?;
1283    let named =
1284        ExplainRef { axis: axis.to_string(), piece: piece.map(str::to_string), at };
1285    Ok(Some(Part::Explain(named, accs)))
1286}
1287
1288fn parse_ref(
1289    body: &str,
1290    pos: usize,
1291    bound: &[String],
1292    axes: bool,
1293) -> Result<Part, TemplateError> {
1294    if let Some(part) = parse_explain(body, pos, axes)? {
1295        return Ok(part);
1296    }
1297    let (field, accs) = parse_field(body, pos, bound)?;
1298    Ok(match field {
1299        Field::Whole => Part::WholeMatch(accs),
1300        Field::Register(name) => Part::Capture(name, accs),
1301        Field::Item(name, index) => Part::Item(name, index, accs),
1302        Field::All(name) => Part::All(name, accs),
1303    })
1304}
1305
1306/// Which bindings of a register a reference's index picks: one by its
1307/// place, or `[*]`, every one.
1308enum Pick {
1309    At(usize),
1310    All,
1311}
1312
1313/// A reference's name with its index taken out: `a[i].b` and `a.b[i]` both
1314/// name the register `a.b` at `i`, `[*]` names every binding, and a
1315/// reference carries one index at most.
1316fn split_index(name: &str, pos: usize) -> Result<(String, Option<Pick>), TemplateError> {
1317    let mut out = String::new();
1318    let mut index = None;
1319    for segment in name.split('.') {
1320        let (base, at) = match segment.split_once('[') {
1321            Some((base, rest)) => {
1322                let digits = rest.strip_suffix(']').ok_or_else(|| TemplateError {
1323                    pos,
1324                    msg: format!("{segment:?}: an index closes with ], as in {base}[0]"),
1325                })?;
1326                if digits == "*" {
1327                    (base, Some(Pick::All))
1328                } else {
1329                    let i = digits.parse::<usize>().map_err(|e| TemplateError {
1330                        pos,
1331                        msg: format!("{segment:?}: an index is a number counted from 0, or * for every one: {e}"),
1332                    })?;
1333                    (base, Some(Pick::At(i)))
1334                }
1335            }
1336            None => (segment, None),
1337        };
1338        if let Some(i) = at {
1339            if index.is_some() {
1340                return Err(TemplateError {
1341                    pos,
1342                    msg: format!("{name:?} carries two indexes; a reference carries one"),
1343                });
1344            }
1345            index = Some(i);
1346        }
1347        if !out.is_empty() {
1348            out.push('.');
1349        }
1350        out.push_str(base);
1351    }
1352    Ok((out, index))
1353}
1354
1355/// The reference and the accessor pipeline a `${...}` body writes,
1356/// validated against the pattern's capture names.
1357fn parse_field(
1358    body: &str,
1359    pos: usize,
1360    bound: &[String],
1361) -> Result<(Field, Vec<Accessor>), TemplateError> {
1362    let (name, accs) = match body.split_once(':') {
1363        Some((n, a)) => (n, parse_accessors(a, pos)?),
1364        None => (body, Vec::new()),
1365    };
1366    if name.is_empty() {
1367        return Err(TemplateError { pos, msg: "empty capture name in ${...}".into() });
1368    }
1369    // Reached with an `@` only from a reference read outside a template,
1370    // which resolves to bytes of the match; a template's own parse takes the
1371    // axis path before here.
1372    if let Some(axis) = name.strip_prefix('@') {
1373        return Err(TemplateError {
1374            pos,
1375            msg: format!(
1376                "${{@{axis}}} reads an axis, which a --format or report template renders and a reference to the match's bytes cannot"
1377            ),
1378        });
1379    }
1380    let (name, index) = split_index(name, pos)?;
1381    if name == "0" {
1382        return match index {
1383            None => Ok((Field::Whole, accs)),
1384            Some(_) => Err(TemplateError { pos, msg: "${0} is the whole match and takes no index".into() }),
1385        };
1386    }
1387    let field = |name: String| match index {
1388        Some(Pick::At(i)) => Field::Item(name, i),
1389        Some(Pick::All) => Field::All(name),
1390        None => Field::Register(name),
1391    };
1392    // A number is the capture's position: `${1}` is the first name the
1393    // pattern binds. trex's `(...)` is a logical group and binds nothing, so
1394    // there is no group to count; what is numbered is the bindings, in the
1395    // order they are written. That keeps one meaning of "capture" rather than
1396    // adding a second, anonymous kind alongside `:name`.
1397    if name.bytes().all(|b| b.is_ascii_digit()) {
1398        let n: usize = match name.parse() {
1399            Ok(n) => n,
1400            Err(e) => {
1401                return Err(TemplateError {
1402                    pos,
1403                    msg: format!("capture number {name:?} is out of range: {e}"),
1404                });
1405            }
1406        };
1407        return match bound.get(n.wrapping_sub(1)) {
1408            Some(found) => Ok((field(found.clone()), accs)),
1409            None => Err(TemplateError {
1410                pos,
1411                msg: format!(
1412                    "template references ${{{n}}} but the pattern binds {} capture(s)",
1413                    bound.len()
1414                ),
1415            }),
1416        };
1417    }
1418    if !bound.contains(&name) {
1419        return Err(TemplateError {
1420            pos,
1421            msg: format!("template references ${{{name}}} but the pattern binds no such capture"),
1422        });
1423    }
1424    Ok((field(name), accs))
1425}
1426
1427/// A `${...}` reference read outside a template: the field it names and the
1428/// accessors that slice it, validated against the pattern's capture names as
1429/// a template's are, so a callback reads `e:domain` or `0:last4` as a
1430/// template renders it.
1431#[derive(Clone, Debug, PartialEq, Eq)]
1432pub struct Reference {
1433    field: Field,
1434    accessors: Vec<Accessor>,
1435}
1436
1437impl Reference {
1438    /// Parse the body a template writes inside `${...}`: `0`, a register's
1439    /// name or its position among `bound`, and the accessors after a colon.
1440    ///
1441    /// # Errors
1442    ///
1443    /// A name the pattern binds no register under, a position past the last
1444    /// it binds, or an accessor that is not one.
1445    pub fn parse(body: &str, bound: &[String]) -> Result<Reference, TemplateError> {
1446        let (field, accessors) = parse_field(body, 0, bound)?;
1447        Ok(Reference { field, accessors })
1448    }
1449
1450    /// What the reference names: the whole match or one register.
1451    #[must_use]
1452    pub fn field(&self) -> &Field {
1453        &self.field
1454    }
1455
1456    /// The accessors applied to `text`, left to right: `text` itself where
1457    /// there are none, and nothing where a slicing accessor finds nothing.
1458    #[must_use]
1459    pub fn apply(&self, text: &str) -> String {
1460        apply_all(&self.accessors, text)
1461    }
1462
1463    /// The reference read over one match: the field's bytes, then the
1464    /// accessors, as a template renders `${body}` there.
1465    #[must_use]
1466    pub fn read<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
1467        let bytes = match &self.field {
1468            Field::All(name) => return every_binding(m, input, name, &self.accessors),
1469            Field::Whole => &input[m.start()..m.end()],
1470            Field::Register(name) => m
1471                .names()
1472                .iter()
1473                .position(|k| k == name)
1474                .and_then(|i| m.captures().get(i))
1475                .map_or(&[] as &[u8], |s| &input[s.range()]),
1476            Field::Item(name, index) => m
1477                .history(name)
1478                .and_then(|all| all.get(*index))
1479                .map_or(&[] as &[u8], |s| &input[s.range()]),
1480        };
1481        self.apply(&String::from_utf8_lossy(bytes))
1482    }
1483}
1484
1485/// Parse a `|`-separated accessor pipeline (the part after `:`).
1486fn parse_accessors(s: &str, pos: usize) -> Result<Vec<Accessor>, TemplateError> {
1487    s.split('|').map(|t| parse_accessor(t.trim(), pos)).collect()
1488}
1489
1490fn parse_accessor(t: &str, pos: usize) -> Result<Accessor, TemplateError> {
1491    let a = match t {
1492        "upper" => Accessor::Upper,
1493        "lower" => Accessor::Lower,
1494        "trim" => Accessor::Trim,
1495        "scheme" => Accessor::UrlScheme,
1496        "host" => Accessor::UrlHost,
1497        "port" => Accessor::UrlPort,
1498        "path" => Accessor::UrlPath,
1499        "query" => Accessor::UrlQuery,
1500        "user" => Accessor::EmailUser,
1501        "domain" => Accessor::EmailDomain,
1502        "major" => Accessor::VerMajor,
1503        "minor" => Accessor::VerMinor,
1504        "patch" => Accessor::VerPatch,
1505        "year" => Accessor::TsField(0),
1506        "month" => Accessor::TsField(1),
1507        "day" => Accessor::TsField(2),
1508        "hour" => Accessor::TsField(3),
1509        "minute" => Accessor::TsField(4),
1510        "second" => Accessor::TsField(5),
1511        "dir" => Accessor::PathDir,
1512        "name" => Accessor::PathName,
1513        "ext" => Accessor::PathExt,
1514        "value" => Accessor::QtyValue,
1515        "unit" => Accessor::QtyUnit,
1516        _ => {
1517            if let Some(r) = t.strip_prefix("octet") {
1518                let (x, y) = parse_range(r, pos)?;
1519                Accessor::Octet(x, y)
1520            } else if let Some(r) = t.strip_prefix("group") {
1521                let (x, y) = parse_range(r, pos)?;
1522                Accessor::Group(x, y)
1523            } else if let Some(r) = t.strip_prefix("first") {
1524                Accessor::First(parse_count(r, "first", pos)?)
1525            } else if let Some(r) = t.strip_prefix("last") {
1526                Accessor::Last(parse_count(r, "last", pos)?)
1527            } else {
1528                return Err(TemplateError {
1529                    pos,
1530                    msg: format!(
1531                        "unknown accessor ':{t}' (use upper/lower/trim; firstN/lastN; octetN[-M]; groupN[-M]; \
1532                         scheme/host/port/path/query; user/domain; major/minor/patch; \
1533                         year/month/day/hour/minute/second; dir/name/ext; value/unit)"
1534                    ),
1535                });
1536            }
1537        }
1538    };
1539    Ok(a)
1540}
1541
1542/// Parse a 1-based index `N` or inclusive range `N-M`.
1543fn parse_range(r: &str, pos: usize) -> Result<(usize, usize), TemplateError> {
1544    let bad = || TemplateError { pos, msg: format!("bad index '{r}' (use N or N-M, 1-based)") };
1545    match r.split_once('-') {
1546        Some((a, b)) => {
1547            let a = a.parse::<usize>().map_err(|_| bad())?;
1548            let b = b.parse::<usize>().map_err(|_| bad())?;
1549            if a == 0 || b < a {
1550                return Err(bad());
1551            }
1552            Ok((a, b))
1553        }
1554        None => {
1555            let a = r.parse::<usize>().map_err(|_| bad())?;
1556            if a == 0 {
1557                return Err(bad());
1558            }
1559            Ok((a, a))
1560        }
1561    }
1562}
1563
1564/// The spans with their registers resolved as `template` reads them: every
1565/// binding under a repetition where it reads one by index, the last alone
1566/// otherwise, which the cheaper rungs answer.
1567fn resolve(pattern: &Pattern, template: &Template, input: &[u8], spans: &[Span]) -> Vec<Match> {
1568    if template.reads_lists() {
1569        captures_with_lists(pattern, input, spans)
1570    } else {
1571        captures(pattern, input, spans)
1572    }
1573}
1574
1575/// [`resolve`] under declared shapes, which decide the token boundaries a
1576/// match was found on and so must decide the ones its registers are read on.
1577fn resolve_with_shapes(
1578    pattern: &Pattern,
1579    template: &Template,
1580    input: &[u8],
1581    shapes: &crate::custom::ShapeSet,
1582    spans: &[Span],
1583) -> Vec<Match> {
1584    if template.reads_lists() {
1585        crate::engine::captures_with_shapes_and_lists(pattern, input, shapes, spans)
1586    } else {
1587        crate::engine::captures_with_shapes(pattern, input, shapes, spans)
1588    }
1589}
1590
1591/// [`edits`] with `shapes` in force, for a caller whose pattern file
1592/// declares a shape or a kind: the scan lexes under them and the registers
1593/// are resolved on the same boundaries, so a rewrite over a declared kind
1594/// replaces what a scan over it reports.
1595#[must_use]
1596pub fn edits_with_shapes(
1597    pattern: &Pattern,
1598    template: &Template,
1599    input: &[u8],
1600    shapes: &crate::custom::ShapeSet,
1601) -> Vec<crate::files::Edit> {
1602    edits_at(pattern, template, input, shapes, &crate::engine::scan_with_shapes(pattern, input, shapes))
1603}
1604
1605/// The edits `template` makes at `spans`, matches of `pattern` over `input`
1606/// found under `shapes`: each span and the bytes the template renders for
1607/// it, the registers resolved on the boundaries the shapes decide. What a
1608/// rewrite of a stream renders once the stream has committed its matches.
1609#[must_use]
1610pub fn edits_at(
1611    pattern: &Pattern,
1612    template: &Template,
1613    input: &[u8],
1614    shapes: &crate::custom::ShapeSet,
1615    spans: &[Span],
1616) -> Vec<crate::files::Edit> {
1617    if template.reads_captures() {
1618        resolve_with_shapes(pattern, template, input, shapes, spans)
1619            .iter()
1620            .map(|m| crate::files::Edit {
1621                start: m.start,
1622                end: m.end,
1623                replacement: template.render(m, input).into_bytes(),
1624            })
1625            .collect()
1626    } else {
1627        spans
1628            .iter()
1629            .map(|s| crate::files::Edit {
1630                start: s.start(),
1631                end: s.end(),
1632                replacement: template.render(s, input).into_bytes(),
1633            })
1634            .collect()
1635    }
1636}
1637
1638/// [`rewrite`] with `shapes` in force. A set with nothing in it is the
1639/// plain rewrite, which keeps the backend routing a declared shape rules
1640/// out.
1641#[must_use]
1642pub fn rewrite_with_shapes(
1643    pattern: &Pattern,
1644    template: &Template,
1645    input: &[u8],
1646    shapes: &crate::custom::ShapeSet,
1647) -> Vec<u8> {
1648    if shapes.is_empty() && pattern.library_kinds().is_empty() {
1649        return rewrite(pattern, template, input);
1650    }
1651    let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
1652    if template.reads_captures() {
1653        return splice_parallel(input, &resolve_with_shapes(pattern, template, input, shapes, &spans), template);
1654    }
1655    splice_parallel(input, &spans, template)
1656}
1657
1658/// The edits [`rewrite`] would make: each match's span and the bytes the
1659/// template renders for it, in input order. What a dry run diffs.
1660#[must_use]
1661pub fn edits(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<crate::files::Edit> {
1662    let spans = scan(pattern, input);
1663    if template.reads_captures() {
1664        resolve(pattern, template, input, &spans)
1665            .iter()
1666            .map(|m| crate::files::Edit {
1667                start: m.start,
1668                end: m.end,
1669                replacement: template.render(m, input).into_bytes(),
1670            })
1671            .collect()
1672    } else {
1673        spans
1674            .iter()
1675            .map(|s| crate::files::Edit {
1676                start: s.start(),
1677                end: s.end(),
1678                replacement: template.render(s, input).into_bytes(),
1679            })
1680            .collect()
1681    }
1682}
1683
1684/// Rewrite `input` for `pattern` with `template`: every leftmost,
1685/// non-overlapping match is replaced by the rendered template, and the
1686/// gaps between matches are copied verbatim.
1687#[must_use]
1688pub fn rewrite(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
1689    let spans = scan(pattern, input);
1690    if template.reads_captures() {
1691        return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
1692    }
1693    splice_parallel(input, &spans, template)
1694}
1695
1696/// [`rewrite`] stopping after `n` matches, leaving the rest of `input` as it
1697/// stands.
1698///
1699/// A rewrite of a single match reads no further than that match, because it
1700/// takes the same early-stopping path [`crate::find`] takes. A rewrite of the
1701/// first few reads the whole input where a route answers the pattern: a route
1702/// reports its matches together and has no form that stops at the n-th, so a
1703/// cursor over one has already computed them all before the first is taken.
1704///
1705/// The splice is the same one a full rewrite uses: a partial rewrite differs
1706/// in how many matches it is given, not in how they are rendered or copied
1707/// around.
1708#[must_use]
1709pub fn rewrite_n(pattern: &Pattern, template: &Template, input: &[u8], n: usize) -> Vec<u8> {
1710    // A cursor over a routed pattern computes every match before the first is
1711    // taken, so `take(1)` discards work already done. `find` stops at the
1712    // first match where a route can. A larger `n` still pays it.
1713    let spans: Vec<crate::engine::Span> = if n == 1 {
1714        crate::cursor::find(pattern, input).into_iter().collect()
1715    } else {
1716        crate::cursor::find_iter(pattern, input).take(n).collect()
1717    };
1718    if template.reads_captures() {
1719        return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
1720    }
1721    splice_parallel(input, &spans, template)
1722}
1723
1724/// [`rewrite`] of the first match only.
1725#[must_use]
1726pub fn rewrite_first(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
1727    rewrite_n(pattern, template, input, 1)
1728}
1729
1730/// One match as a rewrite's callback sees it: its span and bytes in the
1731/// input, what each register bound, and any slice a template reference
1732/// names.
1733#[derive(Clone, Copy, Debug)]
1734pub struct Matched<'a> {
1735    input: &'a [u8],
1736    m: &'a Match,
1737    /// The pattern's capture names in written order, which a numbered
1738    /// reference counts through.
1739    bound: &'a [String],
1740    /// The kind each register binds, for [`Matched::value`]. Empty where the
1741    /// caller supplied none, in which case no register reports a value.
1742    kinds: &'a [(String, Option<crate::token::TokenKind>)],
1743}
1744
1745impl<'a> Matched<'a> {
1746    /// The match `m` over `input`, with `bound` the pattern's
1747    /// [`Pattern::capture_names`], which a reference such as `1:upper`
1748    /// numbers through.
1749    /// Built this way no register reports a typed value, because nothing
1750    /// here says which kind any of them binds. [`Matched::with_kinds`]
1751    /// supplies that.
1752    #[must_use]
1753    pub fn new(m: &'a Match, input: &'a [u8], bound: &'a [String]) -> Self {
1754        Matched { input, m, bound, kinds: &[] }
1755    }
1756
1757    /// [`Matched::new`] knowing which kind each register binds, from
1758    /// [`Pattern::capture_kinds`], so [`Matched::value`] can answer.
1759    #[must_use]
1760    pub fn with_kinds(
1761        m: &'a Match,
1762        input: &'a [u8],
1763        bound: &'a [String],
1764        kinds: &'a [(String, Option<crate::token::TokenKind>)],
1765    ) -> Self {
1766        Matched { input, m, bound, kinds }
1767    }
1768
1769    /// The parsed value the register `name` bound, in its base unit, or
1770    /// `None` where it bound no single typed kind or its text does not parse
1771    /// as one.
1772    ///
1773    /// This is the read a `:value` clause makes, so a rewrite computing from
1774    /// a value and a predicate selecting on one cannot disagree. A byte size
1775    /// arrives in bytes, a duration in nanoseconds, a timestamp as a calendar
1776    /// instant; nothing is parsed twice.
1777    #[must_use]
1778    pub fn value(&self, name: &str) -> Option<crate::typed::TypedValue> {
1779        let kind = self.kinds.iter().find(|(n, _)| n == name).and_then(|(_, k)| *k)?;
1780        let text = self.group(name)?;
1781        crate::typed::value_of(kind, &String::from_utf8_lossy(text))
1782    }
1783
1784    /// The byte offset the match begins at.
1785    #[must_use]
1786    pub fn start(&self) -> usize {
1787        self.m.start
1788    }
1789
1790    /// The byte offset just past it.
1791    #[must_use]
1792    pub fn end(&self) -> usize {
1793        self.m.end
1794    }
1795
1796    /// The matched bytes.
1797    #[must_use]
1798    pub fn as_bytes(&self) -> &'a [u8] {
1799        &self.input[self.m.start..self.m.end]
1800    }
1801
1802    /// The matched bytes as text, with replacement characters where they
1803    /// are not UTF-8.
1804    #[must_use]
1805    pub fn text(&self) -> Cow<'a, str> {
1806        String::from_utf8_lossy(self.as_bytes())
1807    }
1808
1809    /// The whole input the match was found in.
1810    #[must_use]
1811    pub fn input(&self) -> &'a [u8] {
1812        self.input
1813    }
1814
1815    /// The match itself, its registers as spans through [`Match::captures`]
1816    /// and [`Match::names`].
1817    #[must_use]
1818    pub fn inner(&self) -> &'a Match {
1819        self.m
1820    }
1821
1822    /// The bytes the register called `name` bound, or `None` where the
1823    /// pattern names no such register.
1824    #[must_use]
1825    pub fn group(&self, name: &str) -> Option<&'a [u8]> {
1826        self.m.group(name, self.input)
1827    }
1828
1829    /// The register names, in the order [`Match::captures`] holds their
1830    /// spans.
1831    #[must_use]
1832    pub fn names(&self) -> &'a [String] {
1833        self.m.names()
1834    }
1835
1836    /// A template reference read over this match, as `${...}` renders it:
1837    /// `0` the whole match, `e` a register, `1` the first the pattern
1838    /// binds, and `e:domain`, `0:last4` or `ip:octet1-2` a slice of one.
1839    ///
1840    /// # Errors
1841    ///
1842    /// A name the pattern binds no register under, a position past the last
1843    /// it binds, or an accessor that is not one.
1844    pub fn get(&self, reference: &str) -> Result<String, TemplateError> {
1845        Ok(Reference::parse(reference, self.bound)?.read(self.m, self.input))
1846    }
1847}
1848
1849/// Splice what `replace` returns for each match into the input, serially:
1850/// the callback decides every replacement in turn, so there is no render to
1851/// share out.
1852fn splice_with<F, R>(
1853    input: &[u8],
1854    matches: &[Match],
1855    bound: &[String],
1856    kinds: &[(String, Option<crate::token::TokenKind>)],
1857    mut replace: F,
1858) -> Vec<u8>
1859where
1860    F: FnMut(&Matched<'_>) -> R,
1861    R: AsRef<[u8]>,
1862{
1863    let mut out = Vec::with_capacity(input.len());
1864    let mut at = 0;
1865    for m in matches {
1866        out.extend_from_slice(&input[at..m.start]);
1867        out.extend_from_slice(replace(&Matched { input, m, bound, kinds }).as_ref());
1868        at = m.end;
1869    }
1870    out.extend_from_slice(&input[at..]);
1871    out
1872}
1873
1874/// Rewrite `input` for `pattern` with `replace` deciding each replacement:
1875/// every leftmost, non-overlapping match, its registers resolved, is handed
1876/// to `replace` as a [`Matched`], the bytes it returns take the match's
1877/// place, and the gaps between matches are copied verbatim. What a template
1878/// cannot say - a replacement computed from the match, a count, a lookup -
1879/// is written here; a template is [`rewrite`].
1880pub fn rewrite_with<F, R>(pattern: &Pattern, input: &[u8], replace: F) -> Vec<u8>
1881where
1882    F: FnMut(&Matched<'_>) -> R,
1883    R: AsRef<[u8]>,
1884{
1885    let spans = scan(pattern, input);
1886    let bound = pattern.capture_names();
1887    let kinds = pattern.capture_kinds();
1888    // The closure may read any binding, so every one is kept.
1889    splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1890}
1891
1892/// [`rewrite_with`] stopping after `n` matches, leaving the rest of `input`
1893/// as it stands; the matches are found as [`rewrite_n`] finds them.
1894pub fn rewrite_n_with<F, R>(pattern: &Pattern, input: &[u8], n: usize, replace: F) -> Vec<u8>
1895where
1896    F: FnMut(&Matched<'_>) -> R,
1897    R: AsRef<[u8]>,
1898{
1899    let spans: Vec<Span> = if n == 1 {
1900        crate::cursor::find(pattern, input).into_iter().collect()
1901    } else {
1902        crate::cursor::find_iter(pattern, input).take(n).collect()
1903    };
1904    let bound = pattern.capture_names();
1905    let kinds = pattern.capture_kinds();
1906    // The closure may read any binding, so every one is kept.
1907    splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1908}
1909
1910/// Rewrite on the GPU, or `None` when the device path does not apply
1911/// (the pattern is outside the device subset, no device, or a build
1912/// without the `gpu` feature). The match runs on the device; rendering
1913/// and the splice run on the host. The output is identical to
1914/// [`rewrite`]. The caller falls back to [`rewrite`] on `None`.
1915#[must_use]
1916pub fn rewrite_gpu(pattern: &Pattern, template: &Template, input: &[u8]) -> Option<Vec<u8>> {
1917    let spans = scan_gpu(pattern, input)?;
1918    if template.reads_captures() {
1919        return Some(splice_parallel(input, &resolve(pattern, template, input, &spans), template));
1920    }
1921    Some(splice_parallel(input, &spans, template))
1922}
1923
1924/// Rewrite on the backend chosen by `backend`, and report which one ran.
1925/// `Auto` matches where [`scan_with_backend`] places the scan; `Gpu` forces
1926/// the device and falls back when it cannot run; `Cpu` never touches the
1927/// device. The rendering and the splice always run on the host, so the
1928/// output is identical to [`rewrite`] on every backend.
1929#[must_use]
1930pub fn rewrite_with_backend(
1931    pattern: &Pattern,
1932    template: &Template,
1933    input: &[u8],
1934    backend: Backend,
1935) -> (Vec<u8>, BackendUsed) {
1936    match backend {
1937        Backend::Cpu => (rewrite(pattern, template, input), BackendUsed::Cpu),
1938        Backend::Gpu => match rewrite_gpu(pattern, template, input) {
1939            Some(o) => (o, BackendUsed::Gpu),
1940            None => (rewrite(pattern, template, input), BackendUsed::Cpu),
1941        },
1942        Backend::Auto => {
1943            let (spans, used) = scan_with_backend(pattern, input, Backend::Auto);
1944            let out = if template.reads_captures() {
1945                splice_parallel(input, &resolve(pattern, template, input, &spans), template)
1946            } else {
1947                splice_parallel(input, &spans, template)
1948            };
1949            (out, used)
1950        }
1951    }
1952}
1953
1954/// Splice rendered matches into the input, serially. The canonical
1955/// output the parallel path is checked against.
1956#[must_use]
1957pub(crate) fn splice<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
1958    let mut out: Vec<u8> = Vec::with_capacity(input.len());
1959    let mut pos = 0;
1960    for m in matches {
1961        out.extend_from_slice(&input[pos..m.start()]);
1962        out.extend_from_slice(template.render(m, input).as_bytes());
1963        pos = m.end();
1964    }
1965    out.extend_from_slice(&input[pos..]);
1966    out
1967}
1968
1969/// Splice `rendered` into every match's place, for a template that renders the
1970/// same bytes at each.
1971///
1972/// The output is what [`splice`] produces for such a template, reached without
1973/// a render a match. The whole output is sized before the first copy, because
1974/// the matches say exactly how many bytes they replace.
1975#[must_use]
1976fn splice_one_string<M: Spanned>(input: &[u8], matches: &[M], rendered: &[u8]) -> Vec<u8> {
1977    let replaced: usize = matches.iter().map(|m| m.end() - m.start()).sum();
1978    let mut out: Vec<u8> =
1979        Vec::with_capacity(input.len() - replaced + matches.len() * rendered.len());
1980    let mut pos = 0;
1981    for m in matches {
1982        out.extend_from_slice(&input[pos..m.start()]);
1983        out.extend_from_slice(rendered);
1984        pos = m.end();
1985    }
1986    out.extend_from_slice(&input[pos..]);
1987    out
1988}
1989
1990/// Above this many matches the renders are computed across cores. Each
1991/// match's render is independent (it reads only its own captures), so
1992/// the render phase is an embarrassingly parallel map; the assembly that
1993/// follows is one sequential copy.
1994const PARALLEL_REWRITE_THRESHOLD: usize = 1024;
1995
1996/// Splice with the per-match renders computed in parallel. Identical
1997/// output to [`splice`]: the match set is non-overlapping and each
1998/// render depends only on its own match, so order is preserved and the
1999/// renders never interact.
2000#[must_use]
2001pub(crate) fn splice_parallel<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
2002    // A template of literals alone renders the same bytes at every match, so
2003    // the render is computed once and copied. Rendering it a match allocates a
2004    // string a match here and a buffer a match below, for bytes that never
2005    // differ - and a constant replacement is the commonest rewrite there is.
2006    if template.renders_one_string() {
2007        return splice_one_string(input, matches, template.one_string().as_bytes());
2008    }
2009    let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
2010    if matches.len() < PARALLEL_REWRITE_THRESHOLD || cores <= 1 {
2011        return splice(input, matches, template);
2012    }
2013
2014    // Render every match into its own byte buffer across cores through the
2015    // work-stealing pool. Each render reads only its own match, so the
2016    // renders are independent and the leaf writes only its own slots. The
2017    // per-render estimate makes the pool's serial-vs-parallel choice depend
2018    // on total work, not on K_outer (which a rewrite has no meaning for).
2019    let mut renders: Vec<Vec<u8>> = matches.iter().map(|_| Vec::new()).collect();
2020    let min_leaf = matches.len().div_ceil(cores * 4).max(64);
2021    let plan = flynnel::JobPlan::new(0, matches.len() as u32)
2022        .with_leaf_shape(flynnel::LeafShape::PortCompute);
2023    flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf(
2024        &plan,
2025        &mut renders,
2026        min_leaf,
2027        |start, slots| {
2028            for (i, slot) in slots.iter_mut().enumerate() {
2029                *slot = template.render(&matches[start + i], input).into_bytes();
2030            }
2031        },
2032    );
2033
2034    // Assemble: gaps verbatim, renders in order. One sequential copy.
2035    let total: usize =
2036        input.len() + renders.iter().map(Vec::len).sum::<usize>() - spanned_len(matches);
2037    let mut out: Vec<u8> = Vec::with_capacity(total);
2038    let mut pos = 0;
2039    for (m, r) in matches.iter().zip(&renders) {
2040        out.extend_from_slice(&input[pos..m.start()]);
2041        out.extend_from_slice(r);
2042        pos = m.end();
2043    }
2044    out.extend_from_slice(&input[pos..]);
2045    out
2046}
2047
2048/// Total input bytes covered by the matches, subtracted when sizing the
2049/// output (the matched spans are replaced by their renders).
2050fn spanned_len<M: Spanned>(matches: &[M]) -> usize {
2051    matches.iter().map(|m| m.end() - m.start()).sum()
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056    use super::*;
2057    use crate::parser::parse;
2058
2059    /// A timestamp's fields come from the form it is written in: the runs of
2060    /// digits stand in a different order in every form a log writes, and a
2061    /// field written as a name or not written at all reads empty.
2062    #[test]
2063    fn a_timestamp_field_is_read_where_its_form_puts_it() {
2064        let fields =
2065            |v: &str| (0..6).map(|i| Accessor::TsField(i).apply(v)).collect::<Vec<_>>().join("|");
2066        assert_eq!(fields("2026-09-15T10:11:12"), "2026|09|15|10|11|12");
2067        assert_eq!(fields("2026/09/15 10:11:12"), "2026|09|15|10|11|12");
2068        assert_eq!(fields("15/09/2026 10:11:12"), "2026|09|15|10|11|12");
2069        assert_eq!(fields("09/15/2026 10:11:12"), "2026|09|15|10|11|12");
2070        // Apache writes the year third and names the month; syslog writes no
2071        // year at all; a bare clock opens with the hour and not the year.
2072        assert_eq!(fields("15/Sep/2026:10:11:12"), "2026||15|10|11|12");
2073        assert_eq!(fields("Sep 15 10:11:12"), "||15|10|11|12");
2074        assert_eq!(fields("10:11:12"), "|||10|11|12");
2075    }
2076
2077    /// Rendering once and copying produces what rendering at every match
2078    /// produces, and only the templates whose every part is a literal take that
2079    /// path: `${0}` carries no register and still renders differently at each.
2080    #[test]
2081    fn a_template_of_literals_splices_what_rendering_each_match_splices() {
2082        let mut text = String::new();
2083        for i in 0..2000u32 {
2084            text.push_str(&format!("let value_{i} = {} ; call_{i}(alpha, beta) ;\n", i * 7));
2085        }
2086        let input = text.as_bytes();
2087        let pat = parse("\"let\" \\W:v \"=\"").expect("pattern parses");
2088        let names = pat.capture_names();
2089        let spans = crate::scan(&pat, input);
2090        let ms = crate::captures(&pat, input, &spans);
2091        assert!(ms.len() > PARALLEL_REWRITE_THRESHOLD, "the corpus crosses the parallel threshold");
2092        for (src, one) in [
2093            ("X", true),
2094            ("", true),
2095            ("<>", true),
2096            ("[${0}]", false),
2097            ("${v}", false),
2098            ("a${v}b", false),
2099            ("${0}${v}", false),
2100        ] {
2101            let tpl = Template::parse(src, &names).expect("template parses");
2102            assert_eq!(tpl.renders_one_string(), one, "{src:?}");
2103            assert_eq!(splice_parallel(input, &ms, &tpl), splice(input, &ms, &tpl), "{src:?}");
2104            // The spans alone take the same path, and a template reading a
2105            // register renders it empty over them rather than differently.
2106            if !tpl.reads_captures() {
2107                assert_eq!(
2108                    splice_parallel(input, &spans, &tpl),
2109                    splice(input, &spans, &tpl),
2110                    "{src:?} over spans"
2111                );
2112            }
2113        }
2114    }
2115
2116    #[test]
2117    fn rewriting_one_match_takes_a_different_path_and_the_same_answer() {
2118        // A single match no longer comes from a cursor, which computes every
2119        // match of a routed pattern before the first is taken. It comes from
2120        // find, which stops at the first. Those are different code paths and
2121        // they have to agree, across the routes and the engine alike.
2122        let inputs = [
2123            "alpha beta alpha gamma alpha",
2124            "let a = 1 ; let b = 2 ; let c = 3 ;",
2125            "nothing here matches at all",
2126        ];
2127        for src in ["\"alpha\"", "\\W \"=\"", "\\N", "\\W:k \"=\""] {
2128            for input in inputs {
2129                let pat = parse(src).expect("pattern parses");
2130                let tpl = Template::parse("X", &pat.capture_names()).expect("template parses");
2131                let one = rewrite_first(&pat, &tpl, input.as_bytes());
2132                let by_n = rewrite_n(&pat, &tpl, input.as_bytes(), 1);
2133                assert_eq!(one, by_n, "{src} over {input:?}");
2134
2135                // And against the spans taken one at a time, which is the
2136                // arm that did not change.
2137                let first: Vec<_> = crate::cursor::find_iter(&pat, input.as_bytes()).take(1).collect();
2138                let want = splice_parallel(input.as_bytes(), &first, &tpl);
2139                assert_eq!(one, want, "{src} over {input:?}: the paths disagree");
2140            }
2141        }
2142    }
2143
2144    fn rw(pattern_src: &str, template_src: &str, input: &str) -> String {
2145        let pat = parse(pattern_src).expect("pattern parses");
2146        let tpl = Template::parse(template_src, &pat.capture_names()).expect("template parses");
2147        String::from_utf8(rewrite(&pat, &tpl, input.as_bytes())).expect("utf8")
2148    }
2149
2150    #[test]
2151    fn a_capture_can_be_referenced_by_position() {
2152        // `${1}` is the first binding the pattern writes, so the two
2153        // spellings render the same thing and can be mixed.
2154        assert_eq!(
2155            rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"),
2156            rw("\\W:first \\W:second", "${second} ${first}", "alpha beta"),
2157        );
2158        assert_eq!(rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"), "beta alpha");
2159        // Accessors chain off a numbered reference the same way.
2160        assert_eq!(rw("\\W:a \\W:b", "${1:upper}", "alpha beta"), "ALPHA");
2161        // `${0}` stays the whole match, as it is in a regular expression.
2162        assert_eq!(rw("\\W:a \\W:b", "[${0}]", "alpha beta"), "[alpha beta]");
2163    }
2164
2165    #[test]
2166    fn a_position_past_the_last_capture_is_a_template_error() {
2167        let pat = parse("\\W:only").expect("parses");
2168        let err = Template::parse("${2}", &pat.capture_names()).expect_err("refused");
2169        let msg = format!("{err:?}");
2170        assert!(msg.contains('2'), "names the position: {msg}");
2171        assert!(msg.contains('1'), "and says how many there are: {msg}");
2172    }
2173
2174    #[test]
2175    fn redacts_typed_atom() {
2176        assert_eq!(rw("\\E:e", "[redacted]", "mail bob@x.com now"), "mail [redacted] now");
2177    }
2178
2179    #[test]
2180    fn a_pseudonym_names_a_declared_or_library_kind_by_its_own_name() {
2181        fn replaced(pattern: &Pattern, shapes: &crate::ShapeSet, input: &[u8]) -> Vec<Vec<u8>> {
2182            let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
2183            let mut mask = Mask::parse("pseudonym").expect("a mask");
2184            redactions_with_shapes(input, &spans, &[], &mut mask, pattern, shapes)
2185                .into_iter()
2186                .map(|e| e.replacement)
2187                .collect()
2188        }
2189        let mut shapes = crate::ShapeSet::new();
2190        shapes.declare_text("shape customer = `C\\d{5}`").expect("declares");
2191        let customer = crate::parser::parse_with_shapes("\\{customer}", &shapes).expect("parses");
2192        assert_eq!(
2193            replaced(&customer, &shapes, b"for C00042 and C00077 then C00042"),
2194            [b"CUSTOMER_1".to_vec(), b"CUSTOMER_2".to_vec(), b"CUSTOMER_1".to_vec()]
2195        );
2196        let iban = parse("\\{iban}").expect("parses");
2197        assert_eq!(
2198            replaced(&iban, &crate::ShapeSet::new(), b"pay DE89370400440532013000 now"),
2199            [b"IBAN_1".to_vec()]
2200        );
2201    }
2202
2203    #[test]
2204    fn renames_balanced_tag_and_uppercases_body() {
2205        // The close tag is rewritten from the captured open, so balance
2206        // is preserved; the body is uppercased.
2207        assert_eq!(
2208            rw("<\\W:t>(.*):body</=t>", "<${t}>${body:upper}</${t}>", "<div>hi there</div>"),
2209            "<div>HI THERE</div>"
2210        );
2211    }
2212
2213    #[test]
2214    fn reorders_captures() {
2215        assert_eq!(rw("\\W:a \\N:b", "${b}=${a}", "width 50"), "50=width");
2216    }
2217
2218    #[test]
2219    fn whole_match_reference() {
2220        assert_eq!(rw("\\N", "[${0}]", "a 12 b 34"), "a [12] b [34]");
2221    }
2222
2223    #[test]
2224    fn literal_dollar_and_gaps_preserved() {
2225        assert_eq!(rw("\\N:n", "$$${n}", "cost 5 dollars"), "cost $5 dollars");
2226    }
2227
2228    #[test]
2229    fn unbound_capture_is_a_template_error() {
2230        let pat = parse("\\W:a").unwrap();
2231        let e = Template::parse("${b}", &pat.capture_names()).unwrap_err();
2232        assert!(e.msg.contains("binds no such capture"));
2233    }
2234
2235    #[test]
2236    fn unknown_accessor_is_an_error() {
2237        let pat = parse("\\W:a").unwrap();
2238        let e = Template::parse("${a:shout}", &pat.capture_names()).unwrap_err();
2239        assert!(e.msg.contains("unknown accessor"));
2240    }
2241
2242    #[test]
2243    fn ipv4_octet_slice() {
2244        // The user's case: capture an IP, keep the first two octets in one swoop.
2245        assert_eq!(rw("\\I:ip", "${ip:octet1-2}.0.0/16", "from 192.168.5.9"), "from 192.168.0.0/16");
2246        assert_eq!(rw("\\I:ip", "${ip:octet4}", "from 192.168.5.9"), "from 9");
2247    }
2248
2249    #[test]
2250    fn ipv6_group_slice() {
2251        // The same \I atom matches IPv6, decomposed into groups, not octets.
2252        assert_eq!(
2253            rw("\\I:ip", "${ip:group1-3}", "addr 2001:db8:85a3:0:0:8a2e:370:7334"),
2254            "addr 2001:db8:85a3"
2255        );
2256    }
2257
2258    #[test]
2259    fn url_email_version_fields() {
2260        assert_eq!(rw("\\U:u", "${u:host}", "get https://example.com:8080/a?q=1"), "get example.com");
2261        assert_eq!(rw("\\U:u", "${u:port}", "get https://example.com:8080/a"), "get 8080");
2262        assert_eq!(rw("\\E:e", "${e:user}@X", "to bob@x.com"), "to bob@X");
2263        assert_eq!(rw("\\V:v", "${v:major}", "v 1.2.3-rc1"), "v 1");
2264    }
2265
2266    #[test]
2267    fn accessor_pipeline_chains() {
2268        assert_eq!(rw("\\E:e", "${e:domain|upper}", "to bob@x.com"), "to X.COM");
2269    }
2270
2271    #[test]
2272    fn no_match_leaves_input_unchanged() {
2273        assert_eq!(rw("\\N", "X", "no digits here"), "no digits here");
2274    }
2275
2276    #[test]
2277    fn parallel_splice_matches_serial_on_many_matches() {
2278        // Enough matches to cross the parallel threshold; the parallel
2279        // render must produce exactly the serial output.
2280        let mut input = String::new();
2281        for i in 0..5000 {
2282            input.push_str(&format!("row {i} val {} end\n", i * 3));
2283        }
2284        let pat = parse("\\W:k \\N:v").expect("pattern parses");
2285        let tpl = Template::parse("${k:upper}=${v}", &pat.capture_names()).expect("template");
2286        let matches = captures(&pat, input.as_bytes(), &scan(&pat, input.as_bytes()));
2287        let serial = splice(input.as_bytes(), &matches, &tpl);
2288        let parallel = splice_parallel(input.as_bytes(), &matches, &tpl);
2289        assert_eq!(serial, parallel);
2290        assert_eq!(rewrite(&pat, &tpl, input.as_bytes()), serial);
2291        assert!(
2292            serial.starts_with(b"ROW=0 VAL=0 end\nROW=1 VAL=3 end\n"),
2293            "{}",
2294            String::from_utf8_lossy(&serial[..32])
2295        );
2296    }
2297}