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