use std::borrow::Cow;
use std::ops::Range;
use crate::ast::Pattern;
use crate::engine::{Match, Span, captures, captures_with_lists, scan};
use crate::explain::Explanation;
use crate::gpu::{Backend, BackendUsed, scan_gpu, scan_with_backend};
use crate::token::TokenKind;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Accessor {
Upper,
Lower,
Trim,
First(usize),
Last(usize),
Octet(usize, usize),
Group(usize, usize),
UrlScheme,
UrlHost,
UrlPort,
UrlPath,
UrlQuery,
EmailUser,
EmailDomain,
VerMajor,
VerMinor,
VerPatch,
TsField(usize),
PathDir,
PathName,
PathExt,
QtyValue,
QtyUnit,
}
type ByteRange = Range<usize>;
impl Accessor {
fn slices(&self) -> bool {
!matches!(self, Accessor::Upper | Accessor::Lower)
}
fn locate(&self, v: &str) -> Option<ByteRange> {
let r = match self {
Accessor::Upper | Accessor::Lower => return None,
Accessor::Trim => {
let start = v.len() - v.trim_start().len();
start..start + v.trim().len()
}
Accessor::First(n) => 0..v.char_indices().nth(*n).map_or(v.len(), |(i, _)| i),
Accessor::Last(n) => {
v.char_indices().rev().nth(n.checked_sub(1)?).map_or(0, |(i, _)| i)..v.len()
}
Accessor::Octet(a, b) => parts_range(v, '.', *a, *b)?,
Accessor::Group(a, b) => parts_range(v, ':', *a, *b)?,
Accessor::UrlScheme => url_range(v, UrlPart::Scheme),
Accessor::UrlHost => url_range(v, UrlPart::Host),
Accessor::UrlPort => url_range(v, UrlPart::Port),
Accessor::UrlPath => url_range(v, UrlPart::Path),
Accessor::UrlQuery => url_range(v, UrlPart::Query),
Accessor::EmailUser => 0..v.find('@')?,
Accessor::EmailDomain => v.find('@')? + 1..v.len(),
Accessor::VerMajor => ver_range(v, 0)?,
Accessor::VerMinor => ver_range(v, 1)?,
Accessor::VerPatch => ver_range(v, 2)?,
Accessor::TsField(i) => ts_range(v, *i)?,
Accessor::PathDir => path_range(v, PathPart::Dir),
Accessor::PathName => path_range(v, PathPart::Name),
Accessor::PathExt => path_range(v, PathPart::Ext),
Accessor::QtyValue => 0..crate::quantity::split(v)?.0.len(),
Accessor::QtyUnit => v.len() - crate::quantity::split(v)?.1.len()..v.len(),
};
(!r.is_empty()).then_some(r)
}
fn apply(&self, v: &str) -> String {
match self {
Accessor::Upper => v.to_uppercase(),
Accessor::Lower => v.to_lowercase(),
_ => self.locate(v).map_or_else(String::new, |r| v[r].to_string()),
}
}
}
pub(crate) fn accessors_locating(kind: TokenKind, text: &str, part: Range<usize>) -> Vec<String> {
let mut typed: Vec<(String, Accessor)> = Vec::new();
let named = |names: &[(&str, Accessor)]| -> Vec<(String, Accessor)> {
names.iter().map(|(n, a)| ((*n).to_string(), a.clone())).collect()
};
match kind {
TokenKind::Url => typed = named(&[
("scheme", Accessor::UrlScheme),
("host", Accessor::UrlHost),
("port", Accessor::UrlPort),
("path", Accessor::UrlPath),
("query", Accessor::UrlQuery),
]),
TokenKind::Email => typed = named(&[("user", Accessor::EmailUser), ("domain", Accessor::EmailDomain)]),
TokenKind::Version => typed = named(&[
("major", Accessor::VerMajor),
("minor", Accessor::VerMinor),
("patch", Accessor::VerPatch),
]),
TokenKind::Timestamp => {
for (i, n) in ["year", "month", "day", "hour", "minute", "second"].into_iter().enumerate() {
typed.push((n.to_string(), Accessor::TsField(i)));
}
}
TokenKind::Path => typed = named(&[
("dir", Accessor::PathDir),
("name", Accessor::PathName),
("ext", Accessor::PathExt),
]),
TokenKind::Quantity | TokenKind::ByteSize | TokenKind::Duration | TokenKind::Percent => {
typed = named(&[("value", Accessor::QtyValue), ("unit", Accessor::QtyUnit)]);
}
TokenKind::Ip => {
for a in 1..=8 {
for b in a..=8 {
let span = if a == b { a.to_string() } else { format!("{a}-{b}") };
if b <= 4 {
typed.push((format!("octet{span}"), Accessor::Octet(a, b)));
}
typed.push((format!("group{span}"), Accessor::Group(a, b)));
}
}
}
_ => {}
}
let mut found: Vec<String> =
typed.into_iter().filter(|(_, a)| a.locate(text) == Some(part.clone())).map(|(n, _)| n).collect();
let chars = text.get(part.clone()).map_or(0, |p| p.chars().count());
if chars > 0 && part.start == 0 && part.end < text.len() {
found.push(format!("first{chars}"));
}
if chars > 0 && part.end == text.len() && part.start > 0 {
found.push(format!("last{chars}"));
}
found
}
pub(crate) fn apply_named(accs: &str, text: &str) -> Result<String, String> {
match parse_accessors(accs, 0) {
Ok(parsed) => Ok(apply_all(&parsed, text)),
Err(e) => Err(e.msg),
}
}
pub(crate) fn is_report_field(name: &str) -> bool {
ReportField::parse(name).is_some()
}
fn apply_all(accs: &[Accessor], v: &str) -> String {
let mut s = v.to_string();
for a in accs {
s = a.apply(&s);
}
s
}
fn locate_all(accs: &[Accessor], v: &str) -> Option<ByteRange> {
let mut r = 0..v.len();
for a in accs {
let sub = a.locate(&v[r.clone()])?;
r = r.start + sub.start..r.start + sub.end;
}
(!r.is_empty()).then_some(r)
}
fn parts_range(v: &str, sep: char, a: usize, b: usize) -> Option<ByteRange> {
if a == 0 || b < a {
return None;
}
let mut parts: Vec<ByteRange> = Vec::new();
let mut start = 0;
for (i, _) in v.match_indices(sep) {
parts.push(start..i);
start = i + sep.len_utf8();
}
parts.push(start..v.len());
if b > parts.len() {
return None;
}
Some(parts[a - 1].start..parts[b - 1].end)
}
pub(crate) enum UrlPart {
Scheme,
Host,
Port,
Path,
Query,
}
fn url_range(v: &str, part: UrlPart) -> ByteRange {
let (scheme_end, rest_start) = match v.find("://") {
Some(i) => (i, i + 3),
None => (0, 0),
};
let rest = &v[rest_start..];
let auth_end = rest.find(['/', '?']).unwrap_or(rest.len());
let authority = &rest[..auth_end];
let host_end = match authority.rsplit_once(':') {
Some((h, p)) if !p.is_empty() && p.bytes().all(|c| c.is_ascii_digit()) => h.len(),
_ => auth_end,
};
let after = &rest[auth_end..];
let path_end = after.find('?').unwrap_or(after.len());
let at = |i: usize| rest_start + i;
match part {
UrlPart::Scheme => 0..scheme_end,
UrlPart::Host => at(0)..at(host_end),
UrlPart::Port if host_end < auth_end => at(host_end + 1)..at(auth_end),
UrlPart::Path => at(auth_end)..at(auth_end + path_end),
UrlPart::Query if path_end < after.len() => at(auth_end + path_end + 1)..v.len(),
UrlPart::Port | UrlPart::Query => 0..0,
}
}
pub(crate) fn url_part(v: &str, part: UrlPart) -> String {
v[url_range(v, part)].to_string()
}
fn ver_range(v: &str, i: usize) -> Option<ByteRange> {
let core_end = v.find(['-', '+']).unwrap_or(v.len());
parts_range(&v[..core_end], '.', i + 1, i + 1)
}
fn digit_runs(v: &str) -> Vec<ByteRange> {
let b = v.as_bytes();
let mut runs = Vec::new();
let mut j = 0;
while j < b.len() {
if !b[j].is_ascii_digit() {
j += 1;
continue;
}
let start = j;
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
runs.push(start..j);
}
runs
}
fn ts_range(v: &str, i: usize) -> Option<ByteRange> {
let b = v.as_bytes();
let runs = digit_runs(v);
let (date, clock) = if crate::typed::month_abbrev(b).is_some() && b.get(3) == Some(&b' ') {
((None, None, Some(0)), 1)
} else if b.get(2) == Some(&b'/')
&& b.get(3..).is_some_and(|rest| crate::typed::month_abbrev(rest).is_some())
{
((Some(1), None, Some(0)), 2)
} else if let Some(d) = crate::typed::slash_date(b, 0) {
let (year, month, day) = d.runs;
((Some(year), Some(month), Some(day)), 3)
} else if runs.first().is_some_and(|r| r.len() == 4) && b.get(4) == Some(&b'-') {
((Some(0), Some(1), Some(2)), 3)
} else {
((None, None, None), 0)
};
let run = match i {
0 => date.0?,
1 => date.1?,
2 => date.2?,
_ => clock + i - 3,
};
runs.get(run).cloned()
}
pub(crate) enum PathPart {
Dir,
Name,
Ext,
}
fn path_range(v: &str, part: PathPart) -> ByteRange {
let sep = if v.contains('\\') { '\\' } else { '/' };
let name_start = v.rfind(sep).map_or(0, |i| i + 1);
match part {
PathPart::Dir => 0..name_start.saturating_sub(1),
PathPart::Name => name_start..v.len(),
PathPart::Ext => match v[name_start..].rfind('.') {
Some(dot) => name_start + dot + 1..v.len(),
None => 0..0,
},
}
}
pub(crate) fn path_field(v: &str, part: PathPart) -> String {
v[path_range(v, part)].to_string()
}
#[derive(Debug)]
enum Part {
Literal(String),
WholeMatch(Vec<Accessor>),
Capture(String, Vec<Accessor>),
Item(String, usize, Vec<Accessor>),
All(String, Vec<Accessor>),
Where(ReportField, Vec<Accessor>),
Explain(ExplainRef, Vec<Accessor>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExplainRef {
pub axis: String,
pub piece: Option<String>,
pub at: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReportField {
Path,
Line,
Col,
Start,
End,
Pattern,
Rule,
Severity,
Message,
Fix,
}
impl ReportField {
fn parse(name: &str) -> Option<ReportField> {
Some(match name {
"path" => ReportField::Path,
"line" => ReportField::Line,
"col" => ReportField::Col,
"start" => ReportField::Start,
"end" => ReportField::End,
"pattern" => ReportField::Pattern,
"rule" => ReportField::Rule,
"severity" => ReportField::Severity,
"message" => ReportField::Message,
"fix" => ReportField::Fix,
_ => return None,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReportRule<'a> {
pub name: &'a str,
pub severity: &'a str,
pub message: &'a str,
pub fix: &'a str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ReportAt<'a> {
pub path: &'a str,
pub line: usize,
pub col: usize,
pub base: Option<usize>,
pub pattern: Option<&'a str>,
pub rule: Option<ReportRule<'a>>,
}
impl ReportAt<'_> {
fn placed(&self) -> usize {
self.base.expect("a window is placed wherever a template writes its offsets")
}
fn value<M: Spanned>(&self, field: ReportField, m: &M) -> String {
match field {
ReportField::Path => self.path.to_string(),
ReportField::Line => self.line.to_string(),
ReportField::Col => self.col.to_string(),
ReportField::Start => (m.start() + self.placed()).to_string(),
ReportField::End => (m.end() + self.placed()).to_string(),
ReportField::Pattern => self.pattern.unwrap_or("").to_string(),
ReportField::Rule => self.rule.map_or("", |r| r.name).to_string(),
ReportField::Severity => self.rule.map_or("", |r| r.severity).to_string(),
ReportField::Message => self.rule.map_or("", |r| r.message).to_string(),
ReportField::Fix => self.rule.map_or("", |r| r.fix).to_string(),
}
}
}
#[derive(Debug)]
pub struct Template {
parts: Vec<Part>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TemplateError {
pub pos: usize,
pub msg: String,
}
impl Template {
pub fn parse(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
Self::parse_full(src, bound, false)
}
pub fn parse_report(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
Self::parse_full(src, bound, true)
}
fn parse_full(src: &str, bound: &[String], report: bool) -> Result<Template, TemplateError> {
let b = src.as_bytes();
let mut parts: Vec<Part> = Vec::new();
let mut lit = String::new();
let mut i = 0;
while i < b.len() {
match b[i] {
b'$' if i + 1 < b.len() && b[i + 1] == b'$' => {
lit.push('$');
i += 2;
}
b'$' if i + 1 < b.len() && b[i + 1] == b'{' => {
if !lit.is_empty() {
parts.push(Part::Literal(std::mem::take(&mut lit)));
}
let start = i;
let close = b[i + 2..]
.iter()
.position(|&c| c == b'}')
.map(|p| i + 2 + p)
.ok_or(TemplateError { pos: start, msg: "unterminated ${...}".into() })?;
let body = &src[i + 2..close];
let (name, accs) = match body.split_once(':') {
Some((n, a)) => (n, a),
None => (body, ""),
};
match ReportField::parse(name) {
Some(field) if report => {
let accs = if accs.is_empty() { Vec::new() } else { parse_accessors(accs, start)? };
parts.push(Part::Where(field, accs));
}
_ => parts.push(parse_ref(body, start, bound, report)?),
}
i = close + 1;
}
b'\\' => {
let escaped = match b.get(i + 1) {
Some(b'n') => '\n',
Some(b't') => '\t',
Some(b'\\') => '\\',
Some(&other) => {
return Err(TemplateError {
pos: i,
msg: format!(
"unknown escape \\{}; a template knows \\n, \\t and \\\\",
other as char
),
});
}
None => {
return Err(TemplateError {
pos: i,
msg: "a backslash ends the template; write \\\\ for a backslash".into(),
});
}
};
lit.push(escaped);
i += 2;
}
c => {
lit.push(c as char);
i += 1;
}
}
}
if !lit.is_empty() {
parts.push(Part::Literal(lit));
}
Ok(Template { parts })
}
#[must_use]
pub fn render<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
self.render_at(m, input, None, None)
}
#[must_use]
pub fn render_report<M: Spanned>(&self, m: &M, input: &[u8], at: &ReportAt<'_>) -> String {
self.render_at(m, input, Some(at), None)
}
#[must_use]
pub fn render_explained<M: Spanned>(
&self,
m: &M,
input: &[u8],
at: Option<&ReportAt<'_>>,
why: &Explanation,
) -> String {
self.render_at(m, input, at, Some(why))
}
fn render_at<M: Spanned>(
&self,
m: &M,
input: &[u8],
at: Option<&ReportAt<'_>>,
why: Option<&Explanation>,
) -> String {
let mut out = String::new();
for part in &self.parts {
match part {
Part::Literal(s) => out.push_str(s),
Part::Where(field, accs) => {
let value = at.map_or_else(String::new, |a| a.value(*field, m));
out.push_str(&apply_all(accs, &value));
}
Part::Explain(named, accs) => {
let value = why.map_or_else(String::new, |e| {
e.field(&named.axis, named.piece.as_deref(), named.at)
});
out.push_str(&apply_all(accs, &value));
}
Part::WholeMatch(accs) => {
let whole = String::from_utf8_lossy(&input[m.start()..m.end()]);
out.push_str(&apply_all(accs, &whole));
}
Part::Capture(name, accs) => {
let bytes = m
.names()
.iter()
.position(|k| k == name)
.and_then(|i| m.captures().get(i))
.map_or(&[] as &[u8], |s| &input[s.range()]);
let v = String::from_utf8_lossy(bytes);
out.push_str(&apply_all(accs, &v));
}
Part::Item(name, index, accs) => {
let bytes = m
.history(name)
.and_then(|all| all.get(*index))
.map_or(&[] as &[u8], |s| &input[s.range()]);
let v = String::from_utf8_lossy(bytes);
out.push_str(&apply_all(accs, &v));
}
Part::All(name, accs) => out.push_str(&every_binding(m, input, name, accs)),
}
}
out
}
fn reads_captures(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Capture(..) | Part::Item(..) | Part::All(..)))
}
#[must_use]
pub fn reads_lists(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Item(..) | Part::All(..)))
}
#[must_use]
pub fn reads_place(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Line | ReportField::Col, _)))
}
#[must_use]
pub fn reads_offsets(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Start | ReportField::End, _)))
}
#[must_use]
pub fn reads_explanation(&self) -> bool {
self.parts.iter().any(|p| matches!(p, Part::Explain(..)))
}
fn renders_one_string(&self) -> bool {
self.parts.iter().all(|p| matches!(p, Part::Literal(_)))
}
fn one_string(&self) -> String {
self.parts
.iter()
.map(|p| match p {
Part::Literal(s) => s.as_str(),
Part::WholeMatch(_)
| Part::Capture(..)
| Part::Item(..)
| Part::All(..)
| Part::Where(..)
| Part::Explain(..) => "",
})
.collect()
}
}
fn parse_count(r: &str, name: &str, pos: usize) -> Result<usize, TemplateError> {
match r.parse::<usize>() {
Ok(n) if n > 0 => Ok(n),
Ok(_) => Err(TemplateError {
pos,
msg: format!("{name}0 names no characters; write {name}N with N at least 1"),
}),
Err(e) => Err(TemplateError {
pos,
msg: format!("{name}{r}: {e}; write {name}N, as in {name}4"),
}),
}
}
#[derive(Clone, Debug)]
pub struct Keep {
field: Field,
accessors: Vec<Accessor>,
}
impl Keep {
pub fn parse_list(src: &str, bound: &[String]) -> Result<Vec<Keep>, TemplateError> {
let mut out = Vec::new();
let mut pos = 0;
for entry in src.split(',') {
let at = pos + (entry.len() - entry.trim_start().len());
pos += entry.len() + 1;
let body = entry.trim();
if body.is_empty() {
continue;
}
let (field, accessors) = parse_field(body, at, bound)?;
if let Some(transform) = accessors.iter().find(|a| !a.slices()) {
let name = if *transform == Accessor::Upper { "upper" } else { "lower" };
return Err(TemplateError {
pos: at,
msg: format!(
"`:{name}` transforms the text rather than slicing it, so it names nothing to keep in place"
),
});
}
out.push(Keep { field, accessors });
}
Ok(out)
}
fn locate<M: Spanned>(&self, m: &M, input: &[u8]) -> Option<ByteRange> {
let base = match &self.field {
Field::All(_) => return None,
Field::Whole => m.start()..m.end(),
Field::Register(name) => {
let i = m.names().iter().position(|k| k == name)?;
m.captures().get(i)?.range()
}
Field::Item(name, index) => m.history(name)?.get(*index)?.range(),
};
let text = match String::from_utf8_lossy(&input[base.clone()]) {
Cow::Borrowed(t) => t,
Cow::Owned(_) => return None,
};
let r = locate_all(&self.accessors, text)?;
Some(base.start + r.start..base.start + r.end)
}
}
const MASKED_DIGIT: u8 = b'0';
const MASKED_LETTER: u8 = b'a';
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Pseudonyms {
seen: Vec<(String, Vec<Vec<u8>>)>,
}
impl Pseudonyms {
fn name(&mut self, kind: &str, value: &[u8]) -> String {
let values = match self.seen.iter().position(|(k, _)| k == kind) {
Some(at) => &mut self.seen[at].1,
None => {
self.seen.push((kind.to_string(), Vec::new()));
let last = self.seen.len() - 1;
&mut self.seen[last].1
}
};
let at = match values.iter().position(|v| v == value) {
Some(at) => at,
None => {
values.push(value.to_vec());
values.len() - 1
}
};
format!("{}_{}", kind.to_uppercase(), at + 1)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Mask {
PerChar(char),
Token(String),
Shape,
Pseudonym(Pseudonyms),
}
impl Mask {
pub fn parse(src: &str) -> Result<Mask, String> {
if src == "shape" {
return Ok(Mask::Shape);
}
if src == "pseudonym" {
return Ok(Mask::Pseudonym(Pseudonyms::default()));
}
let mut chars = src.chars();
match (chars.next(), chars.next()) {
(None, _) => Err("the mask is empty; give a character or a token".to_string()),
(Some(c), None) => Ok(Mask::PerChar(c)),
(Some(_), Some(_)) => Ok(Mask::Token(src.to_string())),
}
}
fn kind_of(run: &[u8], lexing: &crate::ShapeSet) -> String {
let toks = if lexing.is_empty() {
crate::lexer::lex(run)
} else {
crate::lexer::lex_with_shapes(run, &crate::lexer::blob_runs(run), lexing, 0)
};
let mut significant = toks.iter().filter(|t| t.is_significant());
match (significant.next(), significant.next()) {
(Some(one), None) => match one.kind {
crate::token::TokenKind::Custom(id) => match lexing.name_of(id) {
Some(name) => name.to_string(),
None => one.kind.name().to_string(),
},
other => other.name().to_string(),
},
_ => "value".to_string(),
}
}
fn cover(&mut self, run: &[u8], out: &mut Vec<u8>, lexing: &crate::ShapeSet) {
match self {
Mask::PerChar(c) => {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf).as_bytes();
let chars = run.iter().filter(|&&b| b & 0xC0 != 0x80).count();
for _ in 0..chars {
out.extend_from_slice(encoded);
}
}
Mask::Token(t) => out.extend_from_slice(t.as_bytes()),
Mask::Shape => out.extend(run.iter().map(|&b| match b {
b'0'..=b'9' => MASKED_DIGIT,
b'A'..=b'Z' | b'a'..=b'z' => MASKED_LETTER,
other => other,
})),
Mask::Pseudonym(book) => {
let kind = Mask::kind_of(run, lexing);
out.extend_from_slice(book.name(&kind, run).as_bytes());
}
}
}
}
#[must_use]
pub fn redactions<M: Spanned>(
input: &[u8],
matches: &[M],
keeps: &[Keep],
mask: &mut Mask,
) -> Vec<crate::files::Edit> {
redactions_under(input, matches, keeps, mask, &crate::ShapeSet::new())
}
#[must_use]
pub fn redactions_with_shapes<M: Spanned>(
input: &[u8],
matches: &[M],
keeps: &[Keep],
mask: &mut Mask,
pattern: &Pattern,
shapes: &crate::ShapeSet,
) -> Vec<crate::files::Edit> {
redactions_under(input, matches, keeps, mask, &shapes.with_library_shapes(&pattern.library_kinds()))
}
fn redactions_under<M: Spanned>(
input: &[u8],
matches: &[M],
keeps: &[Keep],
mask: &mut Mask,
lexing: &crate::ShapeSet,
) -> Vec<crate::files::Edit> {
matches
.iter()
.map(|m| {
let (start, end) = (m.start(), m.end());
let mut kept: Vec<ByteRange> = keeps
.iter()
.filter_map(|k| k.locate(m, input))
.map(|r| r.start.max(start)..r.end.min(end))
.filter(|r| !r.is_empty())
.collect();
kept.sort_by_key(|r| (r.start, r.end));
let mut replacement = Vec::with_capacity(end - start);
let mut cursor = start;
for r in kept {
if r.start > cursor {
mask.cover(&input[cursor..r.start], &mut replacement, lexing);
}
if r.end > cursor {
replacement.extend_from_slice(&input[r.start.max(cursor)..r.end]);
cursor = r.end;
}
}
if cursor < end {
mask.cover(&input[cursor..end], &mut replacement, lexing);
}
crate::files::Edit { start, end, replacement }
})
.collect()
}
pub trait Spanned: Sync {
fn start(&self) -> usize;
fn end(&self) -> usize;
fn captures(&self) -> &[Span];
fn names(&self) -> &[String];
fn history(&self, _name: &str) -> Option<&[Span]> {
None
}
}
impl Spanned for Span {
fn start(&self) -> usize {
Span::start(self)
}
fn end(&self) -> usize {
Span::end(self)
}
fn captures(&self) -> &[Span] {
&[]
}
fn names(&self) -> &[String] {
&[]
}
}
impl Spanned for Match {
fn start(&self) -> usize {
self.start
}
fn end(&self) -> usize {
self.end
}
fn captures(&self) -> &[Span] {
&self.captures
}
fn names(&self) -> &[String] {
Match::names(self)
}
fn history(&self, name: &str) -> Option<&[Span]> {
self.list(name)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Field {
Whole,
Register(String),
Item(String, usize),
All(String),
}
fn every_binding<M: Spanned>(m: &M, input: &[u8], name: &str, accs: &[Accessor]) -> String {
let read = |range: std::ops::Range<usize>| apply_all(accs, &String::from_utf8_lossy(&input[range]));
match m.history(name) {
Some(all) => all.iter().map(|s| read(s.range())).collect::<Vec<_>>().join(","),
None => match m.names().iter().position(|k| k == name).and_then(|i| m.captures().get(i)) {
Some(s) => read(s.range()),
None => String::new(),
},
}
}
fn parse_explain(body: &str, pos: usize, allowed: bool) -> Result<Option<Part>, TemplateError> {
if !body.starts_with('@') {
return Ok(None);
}
if !allowed {
return Err(TemplateError {
pos,
msg: format!(
"${{{body}}} reads an axis, which a scan's --format renders; \
the bytes a rewrite splices in have no explanation to read"
),
});
}
let (name, accs) = match body.split_once(':') {
Some((n, a)) => (n, parse_accessors(a, pos)?),
None => (body, Vec::new()),
};
let (name, at) = match split_index(&name[1..], pos)? {
(name, Some(Pick::All)) => {
return Err(TemplateError {
pos,
msg: format!("${{@{name}[*]}}: an axis written bare already joins every reading; [i] picks one"),
});
}
(name, Some(Pick::At(i))) => (name, Some(i)),
(name, None) => (name, None),
};
let (axis, piece) = match name.split_once('.') {
Some((axis, piece)) => (axis, Some(piece)),
None => (name.as_str(), None),
};
crate::explain::check_explain_field(axis, piece)
.map_err(|msg| TemplateError { pos, msg })?;
let named =
ExplainRef { axis: axis.to_string(), piece: piece.map(str::to_string), at };
Ok(Some(Part::Explain(named, accs)))
}
fn parse_ref(
body: &str,
pos: usize,
bound: &[String],
axes: bool,
) -> Result<Part, TemplateError> {
if let Some(part) = parse_explain(body, pos, axes)? {
return Ok(part);
}
let (field, accs) = parse_field(body, pos, bound)?;
Ok(match field {
Field::Whole => Part::WholeMatch(accs),
Field::Register(name) => Part::Capture(name, accs),
Field::Item(name, index) => Part::Item(name, index, accs),
Field::All(name) => Part::All(name, accs),
})
}
enum Pick {
At(usize),
All,
}
fn split_index(name: &str, pos: usize) -> Result<(String, Option<Pick>), TemplateError> {
let mut out = String::new();
let mut index = None;
for segment in name.split('.') {
let (base, at) = match segment.split_once('[') {
Some((base, rest)) => {
let digits = rest.strip_suffix(']').ok_or_else(|| TemplateError {
pos,
msg: format!("{segment:?}: an index closes with ], as in {base}[0]"),
})?;
if digits == "*" {
(base, Some(Pick::All))
} else {
let i = digits.parse::<usize>().map_err(|e| TemplateError {
pos,
msg: format!("{segment:?}: an index is a number counted from 0, or * for every one: {e}"),
})?;
(base, Some(Pick::At(i)))
}
}
None => (segment, None),
};
if let Some(i) = at {
if index.is_some() {
return Err(TemplateError {
pos,
msg: format!("{name:?} carries two indexes; a reference carries one"),
});
}
index = Some(i);
}
if !out.is_empty() {
out.push('.');
}
out.push_str(base);
}
Ok((out, index))
}
fn parse_field(
body: &str,
pos: usize,
bound: &[String],
) -> Result<(Field, Vec<Accessor>), TemplateError> {
let (name, accs) = match body.split_once(':') {
Some((n, a)) => (n, parse_accessors(a, pos)?),
None => (body, Vec::new()),
};
if name.is_empty() {
return Err(TemplateError { pos, msg: "empty capture name in ${...}".into() });
}
if let Some(axis) = name.strip_prefix('@') {
return Err(TemplateError {
pos,
msg: format!(
"${{@{axis}}} reads an axis, which a --format or report template renders and a reference to the match's bytes cannot"
),
});
}
let (name, index) = split_index(name, pos)?;
if name == "0" {
return match index {
None => Ok((Field::Whole, accs)),
Some(_) => Err(TemplateError { pos, msg: "${0} is the whole match and takes no index".into() }),
};
}
let field = |name: String| match index {
Some(Pick::At(i)) => Field::Item(name, i),
Some(Pick::All) => Field::All(name),
None => Field::Register(name),
};
if name.bytes().all(|b| b.is_ascii_digit()) {
let n: usize = match name.parse() {
Ok(n) => n,
Err(e) => {
return Err(TemplateError {
pos,
msg: format!("capture number {name:?} is out of range: {e}"),
});
}
};
return match bound.get(n.wrapping_sub(1)) {
Some(found) => Ok((field(found.clone()), accs)),
None => Err(TemplateError {
pos,
msg: format!(
"template references ${{{n}}} but the pattern binds {} capture(s)",
bound.len()
),
}),
};
}
if !bound.contains(&name) {
return Err(TemplateError {
pos,
msg: format!("template references ${{{name}}} but the pattern binds no such capture"),
});
}
Ok((field(name), accs))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Reference {
field: Field,
accessors: Vec<Accessor>,
}
impl Reference {
pub fn parse(body: &str, bound: &[String]) -> Result<Reference, TemplateError> {
let (field, accessors) = parse_field(body, 0, bound)?;
Ok(Reference { field, accessors })
}
#[must_use]
pub fn field(&self) -> &Field {
&self.field
}
#[must_use]
pub fn apply(&self, text: &str) -> String {
apply_all(&self.accessors, text)
}
#[must_use]
pub fn read<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
let bytes = match &self.field {
Field::All(name) => return every_binding(m, input, name, &self.accessors),
Field::Whole => &input[m.start()..m.end()],
Field::Register(name) => m
.names()
.iter()
.position(|k| k == name)
.and_then(|i| m.captures().get(i))
.map_or(&[] as &[u8], |s| &input[s.range()]),
Field::Item(name, index) => m
.history(name)
.and_then(|all| all.get(*index))
.map_or(&[] as &[u8], |s| &input[s.range()]),
};
self.apply(&String::from_utf8_lossy(bytes))
}
}
fn parse_accessors(s: &str, pos: usize) -> Result<Vec<Accessor>, TemplateError> {
s.split('|').map(|t| parse_accessor(t.trim(), pos)).collect()
}
fn parse_accessor(t: &str, pos: usize) -> Result<Accessor, TemplateError> {
let a = match t {
"upper" => Accessor::Upper,
"lower" => Accessor::Lower,
"trim" => Accessor::Trim,
"scheme" => Accessor::UrlScheme,
"host" => Accessor::UrlHost,
"port" => Accessor::UrlPort,
"path" => Accessor::UrlPath,
"query" => Accessor::UrlQuery,
"user" => Accessor::EmailUser,
"domain" => Accessor::EmailDomain,
"major" => Accessor::VerMajor,
"minor" => Accessor::VerMinor,
"patch" => Accessor::VerPatch,
"year" => Accessor::TsField(0),
"month" => Accessor::TsField(1),
"day" => Accessor::TsField(2),
"hour" => Accessor::TsField(3),
"minute" => Accessor::TsField(4),
"second" => Accessor::TsField(5),
"dir" => Accessor::PathDir,
"name" => Accessor::PathName,
"ext" => Accessor::PathExt,
"value" => Accessor::QtyValue,
"unit" => Accessor::QtyUnit,
_ => {
if let Some(r) = t.strip_prefix("octet") {
let (x, y) = parse_range(r, pos)?;
Accessor::Octet(x, y)
} else if let Some(r) = t.strip_prefix("group") {
let (x, y) = parse_range(r, pos)?;
Accessor::Group(x, y)
} else if let Some(r) = t.strip_prefix("first") {
Accessor::First(parse_count(r, "first", pos)?)
} else if let Some(r) = t.strip_prefix("last") {
Accessor::Last(parse_count(r, "last", pos)?)
} else {
return Err(TemplateError {
pos,
msg: format!(
"unknown accessor ':{t}' (use upper/lower/trim; firstN/lastN; octetN[-M]; groupN[-M]; \
scheme/host/port/path/query; user/domain; major/minor/patch; \
year/month/day/hour/minute/second; dir/name/ext; value/unit)"
),
});
}
}
};
Ok(a)
}
fn parse_range(r: &str, pos: usize) -> Result<(usize, usize), TemplateError> {
let bad = || TemplateError { pos, msg: format!("bad index '{r}' (use N or N-M, 1-based)") };
match r.split_once('-') {
Some((a, b)) => {
let a = a.parse::<usize>().map_err(|_| bad())?;
let b = b.parse::<usize>().map_err(|_| bad())?;
if a == 0 || b < a {
return Err(bad());
}
Ok((a, b))
}
None => {
let a = r.parse::<usize>().map_err(|_| bad())?;
if a == 0 {
return Err(bad());
}
Ok((a, a))
}
}
}
fn resolve(pattern: &Pattern, template: &Template, input: &[u8], spans: &[Span]) -> Vec<Match> {
if template.reads_lists() {
captures_with_lists(pattern, input, spans)
} else {
captures(pattern, input, spans)
}
}
fn resolve_with_shapes(
pattern: &Pattern,
template: &Template,
input: &[u8],
shapes: &crate::custom::ShapeSet,
spans: &[Span],
) -> Vec<Match> {
if template.reads_lists() {
crate::engine::captures_with_shapes_and_lists(pattern, input, shapes, spans)
} else {
crate::engine::captures_with_shapes(pattern, input, shapes, spans)
}
}
#[must_use]
pub fn edits_with_shapes(
pattern: &Pattern,
template: &Template,
input: &[u8],
shapes: &crate::custom::ShapeSet,
) -> Vec<crate::files::Edit> {
edits_at(pattern, template, input, shapes, &crate::engine::scan_with_shapes(pattern, input, shapes))
}
#[must_use]
pub fn edits_at(
pattern: &Pattern,
template: &Template,
input: &[u8],
shapes: &crate::custom::ShapeSet,
spans: &[Span],
) -> Vec<crate::files::Edit> {
if template.reads_captures() {
resolve_with_shapes(pattern, template, input, shapes, spans)
.iter()
.map(|m| crate::files::Edit {
start: m.start,
end: m.end,
replacement: template.render(m, input).into_bytes(),
})
.collect()
} else {
spans
.iter()
.map(|s| crate::files::Edit {
start: s.start(),
end: s.end(),
replacement: template.render(s, input).into_bytes(),
})
.collect()
}
}
#[must_use]
pub fn rewrite_with_shapes(
pattern: &Pattern,
template: &Template,
input: &[u8],
shapes: &crate::custom::ShapeSet,
) -> Vec<u8> {
if shapes.is_empty() && pattern.library_kinds().is_empty() {
return rewrite(pattern, template, input);
}
let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
if template.reads_captures() {
return splice_parallel(input, &resolve_with_shapes(pattern, template, input, shapes, &spans), template);
}
splice_parallel(input, &spans, template)
}
#[must_use]
pub fn edits(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<crate::files::Edit> {
let spans = scan(pattern, input);
if template.reads_captures() {
resolve(pattern, template, input, &spans)
.iter()
.map(|m| crate::files::Edit {
start: m.start,
end: m.end,
replacement: template.render(m, input).into_bytes(),
})
.collect()
} else {
spans
.iter()
.map(|s| crate::files::Edit {
start: s.start(),
end: s.end(),
replacement: template.render(s, input).into_bytes(),
})
.collect()
}
}
#[must_use]
pub fn rewrite(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
let spans = scan(pattern, input);
if template.reads_captures() {
return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
}
splice_parallel(input, &spans, template)
}
#[must_use]
pub fn rewrite_n(pattern: &Pattern, template: &Template, input: &[u8], n: usize) -> Vec<u8> {
let spans: Vec<crate::engine::Span> = if n == 1 {
crate::cursor::find(pattern, input).into_iter().collect()
} else {
crate::cursor::find_iter(pattern, input).take(n).collect()
};
if template.reads_captures() {
return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
}
splice_parallel(input, &spans, template)
}
#[must_use]
pub fn rewrite_first(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
rewrite_n(pattern, template, input, 1)
}
#[derive(Clone, Copy, Debug)]
pub struct Matched<'a> {
input: &'a [u8],
m: &'a Match,
bound: &'a [String],
kinds: &'a [(String, Option<crate::token::TokenKind>)],
}
impl<'a> Matched<'a> {
#[must_use]
pub fn new(m: &'a Match, input: &'a [u8], bound: &'a [String]) -> Self {
Matched { input, m, bound, kinds: &[] }
}
#[must_use]
pub fn with_kinds(
m: &'a Match,
input: &'a [u8],
bound: &'a [String],
kinds: &'a [(String, Option<crate::token::TokenKind>)],
) -> Self {
Matched { input, m, bound, kinds }
}
#[must_use]
pub fn value(&self, name: &str) -> Option<crate::typed::TypedValue> {
let kind = self.kinds.iter().find(|(n, _)| n == name).and_then(|(_, k)| *k)?;
let text = self.group(name)?;
crate::typed::value_of(kind, &String::from_utf8_lossy(text))
}
#[must_use]
pub fn start(&self) -> usize {
self.m.start
}
#[must_use]
pub fn end(&self) -> usize {
self.m.end
}
#[must_use]
pub fn as_bytes(&self) -> &'a [u8] {
&self.input[self.m.start..self.m.end]
}
#[must_use]
pub fn text(&self) -> Cow<'a, str> {
String::from_utf8_lossy(self.as_bytes())
}
#[must_use]
pub fn input(&self) -> &'a [u8] {
self.input
}
#[must_use]
pub fn inner(&self) -> &'a Match {
self.m
}
#[must_use]
pub fn group(&self, name: &str) -> Option<&'a [u8]> {
self.m.group(name, self.input)
}
#[must_use]
pub fn names(&self) -> &'a [String] {
self.m.names()
}
pub fn get(&self, reference: &str) -> Result<String, TemplateError> {
Ok(Reference::parse(reference, self.bound)?.read(self.m, self.input))
}
}
fn splice_with<F, R>(
input: &[u8],
matches: &[Match],
bound: &[String],
kinds: &[(String, Option<crate::token::TokenKind>)],
mut replace: F,
) -> Vec<u8>
where
F: FnMut(&Matched<'_>) -> R,
R: AsRef<[u8]>,
{
let mut out = Vec::with_capacity(input.len());
let mut at = 0;
for m in matches {
out.extend_from_slice(&input[at..m.start]);
out.extend_from_slice(replace(&Matched { input, m, bound, kinds }).as_ref());
at = m.end;
}
out.extend_from_slice(&input[at..]);
out
}
pub fn rewrite_with<F, R>(pattern: &Pattern, input: &[u8], replace: F) -> Vec<u8>
where
F: FnMut(&Matched<'_>) -> R,
R: AsRef<[u8]>,
{
let spans = scan(pattern, input);
let bound = pattern.capture_names();
let kinds = pattern.capture_kinds();
splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
}
pub fn rewrite_n_with<F, R>(pattern: &Pattern, input: &[u8], n: usize, replace: F) -> Vec<u8>
where
F: FnMut(&Matched<'_>) -> R,
R: AsRef<[u8]>,
{
let spans: Vec<Span> = if n == 1 {
crate::cursor::find(pattern, input).into_iter().collect()
} else {
crate::cursor::find_iter(pattern, input).take(n).collect()
};
let bound = pattern.capture_names();
let kinds = pattern.capture_kinds();
splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
}
#[must_use]
pub fn rewrite_gpu(pattern: &Pattern, template: &Template, input: &[u8]) -> Option<Vec<u8>> {
let spans = scan_gpu(pattern, input)?;
if template.reads_captures() {
return Some(splice_parallel(input, &resolve(pattern, template, input, &spans), template));
}
Some(splice_parallel(input, &spans, template))
}
#[must_use]
pub fn rewrite_with_backend(
pattern: &Pattern,
template: &Template,
input: &[u8],
backend: Backend,
) -> (Vec<u8>, BackendUsed) {
match backend {
Backend::Cpu => (rewrite(pattern, template, input), BackendUsed::Cpu),
Backend::Gpu => match rewrite_gpu(pattern, template, input) {
Some(o) => (o, BackendUsed::Gpu),
None => (rewrite(pattern, template, input), BackendUsed::Cpu),
},
Backend::Auto => {
let (spans, used) = scan_with_backend(pattern, input, Backend::Auto);
let out = if template.reads_captures() {
splice_parallel(input, &resolve(pattern, template, input, &spans), template)
} else {
splice_parallel(input, &spans, template)
};
(out, used)
}
}
}
#[must_use]
pub(crate) fn splice<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
let mut out: Vec<u8> = Vec::with_capacity(input.len());
let mut pos = 0;
for m in matches {
out.extend_from_slice(&input[pos..m.start()]);
out.extend_from_slice(template.render(m, input).as_bytes());
pos = m.end();
}
out.extend_from_slice(&input[pos..]);
out
}
#[must_use]
fn splice_one_string<M: Spanned>(input: &[u8], matches: &[M], rendered: &[u8]) -> Vec<u8> {
let replaced: usize = matches.iter().map(|m| m.end() - m.start()).sum();
let mut out: Vec<u8> =
Vec::with_capacity(input.len() - replaced + matches.len() * rendered.len());
let mut pos = 0;
for m in matches {
out.extend_from_slice(&input[pos..m.start()]);
out.extend_from_slice(rendered);
pos = m.end();
}
out.extend_from_slice(&input[pos..]);
out
}
const PARALLEL_REWRITE_THRESHOLD: usize = 1024;
#[must_use]
pub(crate) fn splice_parallel<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
if template.renders_one_string() {
return splice_one_string(input, matches, template.one_string().as_bytes());
}
let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
if matches.len() < PARALLEL_REWRITE_THRESHOLD || cores <= 1 {
return splice(input, matches, template);
}
let mut renders: Vec<Vec<u8>> = matches.iter().map(|_| Vec::new()).collect();
let min_leaf = matches.len().div_ceil(cores * 4).max(64);
let plan = flynnel::JobPlan::new(0, matches.len() as u32)
.with_leaf_shape(flynnel::LeafShape::PortCompute);
flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf(
&plan,
&mut renders,
min_leaf,
|start, slots| {
for (i, slot) in slots.iter_mut().enumerate() {
*slot = template.render(&matches[start + i], input).into_bytes();
}
},
);
let total: usize =
input.len() + renders.iter().map(Vec::len).sum::<usize>() - spanned_len(matches);
let mut out: Vec<u8> = Vec::with_capacity(total);
let mut pos = 0;
for (m, r) in matches.iter().zip(&renders) {
out.extend_from_slice(&input[pos..m.start()]);
out.extend_from_slice(r);
pos = m.end();
}
out.extend_from_slice(&input[pos..]);
out
}
fn spanned_len<M: Spanned>(matches: &[M]) -> usize {
matches.iter().map(|m| m.end() - m.start()).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
#[test]
fn a_timestamp_field_is_read_where_its_form_puts_it() {
let fields =
|v: &str| (0..6).map(|i| Accessor::TsField(i).apply(v)).collect::<Vec<_>>().join("|");
assert_eq!(fields("2026-09-15T10:11:12"), "2026|09|15|10|11|12");
assert_eq!(fields("2026/09/15 10:11:12"), "2026|09|15|10|11|12");
assert_eq!(fields("15/09/2026 10:11:12"), "2026|09|15|10|11|12");
assert_eq!(fields("09/15/2026 10:11:12"), "2026|09|15|10|11|12");
assert_eq!(fields("15/Sep/2026:10:11:12"), "2026||15|10|11|12");
assert_eq!(fields("Sep 15 10:11:12"), "||15|10|11|12");
assert_eq!(fields("10:11:12"), "|||10|11|12");
}
#[test]
fn a_template_of_literals_splices_what_rendering_each_match_splices() {
let mut text = String::new();
for i in 0..2000u32 {
text.push_str(&format!("let value_{i} = {} ; call_{i}(alpha, beta) ;\n", i * 7));
}
let input = text.as_bytes();
let pat = parse("\"let\" \\W:v \"=\"").expect("pattern parses");
let names = pat.capture_names();
let spans = crate::scan(&pat, input);
let ms = crate::captures(&pat, input, &spans);
assert!(ms.len() > PARALLEL_REWRITE_THRESHOLD, "the corpus crosses the parallel threshold");
for (src, one) in [
("X", true),
("", true),
("<>", true),
("[${0}]", false),
("${v}", false),
("a${v}b", false),
("${0}${v}", false),
] {
let tpl = Template::parse(src, &names).expect("template parses");
assert_eq!(tpl.renders_one_string(), one, "{src:?}");
assert_eq!(splice_parallel(input, &ms, &tpl), splice(input, &ms, &tpl), "{src:?}");
if !tpl.reads_captures() {
assert_eq!(
splice_parallel(input, &spans, &tpl),
splice(input, &spans, &tpl),
"{src:?} over spans"
);
}
}
}
#[test]
fn rewriting_one_match_takes_a_different_path_and_the_same_answer() {
let inputs = [
"alpha beta alpha gamma alpha",
"let a = 1 ; let b = 2 ; let c = 3 ;",
"nothing here matches at all",
];
for src in ["\"alpha\"", "\\W \"=\"", "\\N", "\\W:k \"=\""] {
for input in inputs {
let pat = parse(src).expect("pattern parses");
let tpl = Template::parse("X", &pat.capture_names()).expect("template parses");
let one = rewrite_first(&pat, &tpl, input.as_bytes());
let by_n = rewrite_n(&pat, &tpl, input.as_bytes(), 1);
assert_eq!(one, by_n, "{src} over {input:?}");
let first: Vec<_> = crate::cursor::find_iter(&pat, input.as_bytes()).take(1).collect();
let want = splice_parallel(input.as_bytes(), &first, &tpl);
assert_eq!(one, want, "{src} over {input:?}: the paths disagree");
}
}
}
fn rw(pattern_src: &str, template_src: &str, input: &str) -> String {
let pat = parse(pattern_src).expect("pattern parses");
let tpl = Template::parse(template_src, &pat.capture_names()).expect("template parses");
String::from_utf8(rewrite(&pat, &tpl, input.as_bytes())).expect("utf8")
}
#[test]
fn a_capture_can_be_referenced_by_position() {
assert_eq!(
rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"),
rw("\\W:first \\W:second", "${second} ${first}", "alpha beta"),
);
assert_eq!(rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"), "beta alpha");
assert_eq!(rw("\\W:a \\W:b", "${1:upper}", "alpha beta"), "ALPHA");
assert_eq!(rw("\\W:a \\W:b", "[${0}]", "alpha beta"), "[alpha beta]");
}
#[test]
fn a_position_past_the_last_capture_is_a_template_error() {
let pat = parse("\\W:only").expect("parses");
let err = Template::parse("${2}", &pat.capture_names()).expect_err("refused");
let msg = format!("{err:?}");
assert!(msg.contains('2'), "names the position: {msg}");
assert!(msg.contains('1'), "and says how many there are: {msg}");
}
#[test]
fn redacts_typed_atom() {
assert_eq!(rw("\\E:e", "[redacted]", "mail bob@x.com now"), "mail [redacted] now");
}
#[test]
fn a_pseudonym_names_a_declared_or_library_kind_by_its_own_name() {
fn replaced(pattern: &Pattern, shapes: &crate::ShapeSet, input: &[u8]) -> Vec<Vec<u8>> {
let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
let mut mask = Mask::parse("pseudonym").expect("a mask");
redactions_with_shapes(input, &spans, &[], &mut mask, pattern, shapes)
.into_iter()
.map(|e| e.replacement)
.collect()
}
let mut shapes = crate::ShapeSet::new();
shapes.declare_text("shape customer = `C\\d{5}`").expect("declares");
let customer = crate::parser::parse_with_shapes("\\{customer}", &shapes).expect("parses");
assert_eq!(
replaced(&customer, &shapes, b"for C00042 and C00077 then C00042"),
[b"CUSTOMER_1".to_vec(), b"CUSTOMER_2".to_vec(), b"CUSTOMER_1".to_vec()]
);
let iban = parse("\\{iban}").expect("parses");
assert_eq!(
replaced(&iban, &crate::ShapeSet::new(), b"pay DE89370400440532013000 now"),
[b"IBAN_1".to_vec()]
);
}
#[test]
fn renames_balanced_tag_and_uppercases_body() {
assert_eq!(
rw("<\\W:t>(.*):body</=t>", "<${t}>${body:upper}</${t}>", "<div>hi there</div>"),
"<div>HI THERE</div>"
);
}
#[test]
fn reorders_captures() {
assert_eq!(rw("\\W:a \\N:b", "${b}=${a}", "width 50"), "50=width");
}
#[test]
fn whole_match_reference() {
assert_eq!(rw("\\N", "[${0}]", "a 12 b 34"), "a [12] b [34]");
}
#[test]
fn literal_dollar_and_gaps_preserved() {
assert_eq!(rw("\\N:n", "$$${n}", "cost 5 dollars"), "cost $5 dollars");
}
#[test]
fn unbound_capture_is_a_template_error() {
let pat = parse("\\W:a").unwrap();
let e = Template::parse("${b}", &pat.capture_names()).unwrap_err();
assert!(e.msg.contains("binds no such capture"));
}
#[test]
fn unknown_accessor_is_an_error() {
let pat = parse("\\W:a").unwrap();
let e = Template::parse("${a:shout}", &pat.capture_names()).unwrap_err();
assert!(e.msg.contains("unknown accessor"));
}
#[test]
fn ipv4_octet_slice() {
assert_eq!(rw("\\I:ip", "${ip:octet1-2}.0.0/16", "from 192.168.5.9"), "from 192.168.0.0/16");
assert_eq!(rw("\\I:ip", "${ip:octet4}", "from 192.168.5.9"), "from 9");
}
#[test]
fn ipv6_group_slice() {
assert_eq!(
rw("\\I:ip", "${ip:group1-3}", "addr 2001:db8:85a3:0:0:8a2e:370:7334"),
"addr 2001:db8:85a3"
);
}
#[test]
fn url_email_version_fields() {
assert_eq!(rw("\\U:u", "${u:host}", "get https://example.com:8080/a?q=1"), "get example.com");
assert_eq!(rw("\\U:u", "${u:port}", "get https://example.com:8080/a"), "get 8080");
assert_eq!(rw("\\E:e", "${e:user}@X", "to bob@x.com"), "to bob@X");
assert_eq!(rw("\\V:v", "${v:major}", "v 1.2.3-rc1"), "v 1");
}
#[test]
fn accessor_pipeline_chains() {
assert_eq!(rw("\\E:e", "${e:domain|upper}", "to bob@x.com"), "to X.COM");
}
#[test]
fn no_match_leaves_input_unchanged() {
assert_eq!(rw("\\N", "X", "no digits here"), "no digits here");
}
#[test]
fn parallel_splice_matches_serial_on_many_matches() {
let mut input = String::new();
for i in 0..5000 {
input.push_str(&format!("row {i} val {} end\n", i * 3));
}
let pat = parse("\\W:k \\N:v").expect("pattern parses");
let tpl = Template::parse("${k:upper}=${v}", &pat.capture_names()).expect("template");
let matches = captures(&pat, input.as_bytes(), &scan(&pat, input.as_bytes()));
let serial = splice(input.as_bytes(), &matches, &tpl);
let parallel = splice_parallel(input.as_bytes(), &matches, &tpl);
assert_eq!(serial, parallel);
assert_eq!(rewrite(&pat, &tpl, input.as_bytes()), serial);
assert!(
serial.starts_with(b"ROW=0 VAL=0 end\nROW=1 VAL=3 end\n"),
"{}",
String::from_utf8_lossy(&serial[..32])
);
}
}