1use 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#[derive(Clone, Debug, PartialEq, Eq)]
96enum Accessor {
97 Upper,
98 Lower,
99 Trim,
100 First(usize),
102 Last(usize),
104 Octet(usize, usize),
106 Group(usize, usize),
108 UrlScheme,
109 UrlHost,
110 UrlPort,
111 UrlPath,
112 UrlQuery,
113 EmailUser,
114 EmailDomain,
115 VerMajor,
116 VerMinor,
117 VerPatch,
118 TsField(usize),
120 PathDir,
121 PathName,
122 PathExt,
123 QtyValue,
125 QtyUnit,
127}
128
129type ByteRange = Range<usize>;
131
132impl Accessor {
133 fn slices(&self) -> bool {
136 !matches!(self, Accessor::Upper | Accessor::Lower)
137 }
138
139 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
185pub(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
247pub(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
260pub(crate) fn is_report_field(name: &str) -> bool {
264 ReportField::parse(name).is_some()
265}
266
267fn 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
276fn 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
288fn 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
308pub(crate) enum UrlPart {
310 Scheme,
311 Host,
312 Port,
313 Path,
314 Query,
315}
316
317fn 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
344pub(crate) fn url_part(v: &str, part: UrlPart) -> String {
346 v[url_range(v, part)].to_string()
347}
348
349fn 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
356fn 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
375fn ts_range(v: &str, i: usize) -> Option<ByteRange> {
386 let b = v.as_bytes();
387 let runs = digit_runs(v);
388 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
413pub(crate) enum PathPart {
415 Dir,
416 Name,
417 Ext,
418}
419
420fn 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
435pub(crate) fn path_field(v: &str, part: PathPart) -> String {
437 v[path_range(v, part)].to_string()
438}
439
440#[derive(Debug)]
442enum Part {
443 Literal(String),
444 WholeMatch(Vec<Accessor>),
446 Capture(String, Vec<Accessor>),
448 Item(String, usize, Vec<Accessor>),
451 All(String, Vec<Accessor>),
454 Where(ReportField, Vec<Accessor>),
457 Explain(ExplainRef, Vec<Accessor>),
461}
462
463#[derive(Clone, Debug, PartialEq, Eq)]
474pub struct ExplainRef {
475 pub axis: String,
478 pub piece: Option<String>,
481 pub at: Option<usize>,
484}
485
486#[derive(Clone, Copy, Debug, PartialEq, Eq)]
488pub enum ReportField {
489 Path,
492 Line,
494 Col,
496 Start,
499 End,
501 Pattern,
504 Rule,
507 Severity,
509 Message,
511 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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
545pub struct ReportAt<'a> {
546 pub path: &'a str,
548 pub line: usize,
550 pub col: usize,
552 pub offsets: Option<(usize, usize)>,
558 pub pattern: Option<&'a str>,
560 pub rule: Option<ReportRule<'a>>,
562}
563
564impl ReportAt<'_> {
565 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#[derive(Debug)]
594pub struct Template {
595 parts: Vec<Part>,
596}
597
598#[derive(Clone, Debug, PartialEq, Eq)]
601pub struct TemplateError {
602 pub pos: usize,
604 pub msg: String,
606}
607
608impl Template {
609 pub fn parse(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
617 Self::parse_full(src, bound, false)
618 }
619
620 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 #[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 #[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 #[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 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 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 fn reads_captures(&self) -> bool {
796 self.parts.iter().any(|p| matches!(p, Part::Capture(..) | Part::Item(..) | Part::All(..)))
797 }
798
799 #[must_use]
803 pub fn reads_lists(&self) -> bool {
804 self.parts.iter().any(|p| matches!(p, Part::Item(..) | Part::All(..)))
805 }
806
807 #[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 #[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 #[must_use]
828 pub fn reads_explanation(&self) -> bool {
829 self.parts.iter().any(|p| matches!(p, Part::Explain(..)))
830 }
831
832 fn renders_one_string(&self) -> bool {
840 self.parts.iter().all(|p| matches!(p, Part::Literal(_)))
841 }
842
843 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
861fn 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#[derive(Clone, Debug)]
881pub struct Keep {
882 field: Field,
883 accessors: Vec<Accessor>,
884}
885
886impl Keep {
887 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 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
943const MASKED_DIGIT: u8 = b'0';
954const MASKED_LETTER: u8 = b'a';
955
956#[derive(Clone, Debug, Default, PartialEq, Eq)]
963pub struct Pseudonyms {
964 seen: Vec<(String, Vec<Vec<u8>>)>,
965}
966
967impl Pseudonyms {
968 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 format!("{}_{}", kind.to_uppercase(), at + 1)
990 }
991}
992
993#[derive(Clone, Debug, PartialEq, Eq)]
995pub enum Mask {
996 PerChar(char),
999 Token(String),
1001 Shape,
1005 Pseudonym(Pseudonyms),
1010}
1011
1012impl Mask {
1013 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 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 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#[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#[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
1116fn 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
1154pub trait Spanned: Sync {
1157 fn start(&self) -> usize;
1159 fn end(&self) -> usize;
1161 fn captures(&self) -> &[Span];
1163 fn names(&self) -> &[String];
1166 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#[derive(Clone, Debug, PartialEq, Eq)]
1218pub enum Field {
1219 Whole,
1221 Register(String),
1224 Item(String, usize),
1227 All(String),
1230}
1231
1232fn 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
1246fn 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
1309enum Pick {
1312 At(usize),
1313 All,
1314}
1315
1316fn 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
1358fn 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
1435pub struct Reference {
1436 field: Field,
1437 accessors: Vec<Accessor>,
1438}
1439
1440impl Reference {
1441 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 #[must_use]
1455 pub fn field(&self) -> &Field {
1456 &self.field
1457 }
1458
1459 #[must_use]
1462 pub fn apply(&self, text: &str) -> String {
1463 apply_all(&self.accessors, text)
1464 }
1465
1466 #[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
1488fn 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
1545fn 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
1567fn 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
1578fn 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#[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#[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#[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 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
1680fn 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#[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#[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#[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#[must_use]
1753pub fn rewrite_n(pattern: &Pattern, template: &Template, input: &[u8], n: usize) -> Vec<u8> {
1754 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#[must_use]
1770pub fn rewrite_first(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
1771 rewrite_n(pattern, template, input, 1)
1772}
1773
1774#[derive(Clone, Copy, Debug)]
1778pub struct Matched<'a> {
1779 input: &'a [u8],
1780 m: &'a Match,
1781 bound: &'a [String],
1784 kinds: &'a [(String, Option<crate::token::TokenKind>)],
1787}
1788
1789impl<'a> Matched<'a> {
1790 #[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 #[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 #[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 #[must_use]
1830 pub fn start(&self) -> usize {
1831 self.m.start
1832 }
1833
1834 #[must_use]
1836 pub fn end(&self) -> usize {
1837 self.m.end
1838 }
1839
1840 #[must_use]
1842 pub fn as_bytes(&self) -> &'a [u8] {
1843 &self.input[self.m.start..self.m.end]
1844 }
1845
1846 #[must_use]
1849 pub fn text(&self) -> Cow<'a, str> {
1850 String::from_utf8_lossy(self.as_bytes())
1851 }
1852
1853 #[must_use]
1855 pub fn input(&self) -> &'a [u8] {
1856 self.input
1857 }
1858
1859 #[must_use]
1862 pub fn inner(&self) -> &'a Match {
1863 self.m
1864 }
1865
1866 #[must_use]
1869 pub fn group(&self, name: &str) -> Option<&'a [u8]> {
1870 self.m.group(name, self.input)
1871 }
1872
1873 #[must_use]
1876 pub fn names(&self) -> &'a [String] {
1877 self.m.names()
1878 }
1879
1880 pub fn get(&self, reference: &str) -> Result<String, TemplateError> {
1889 Ok(Reference::parse(reference, self.bound)?.read(self.m, self.input))
1890 }
1891}
1892
1893fn 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
1918pub 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 splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1934}
1935
1936pub 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 splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1952}
1953
1954#[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#[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#[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#[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
2034const PARALLEL_REWRITE_THRESHOLD: usize = 1024;
2039
2040#[must_use]
2045pub(crate) fn splice_parallel<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
2046 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 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 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
2092fn 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 #[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 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 #[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 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 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 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 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 assert_eq!(rw("\\W:a \\W:b", "${1:upper}", "alpha beta"), "ALPHA");
2205 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 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 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 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 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}