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, 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,
498 End,
500 Pattern,
503 Rule,
506 Severity,
508 Message,
510 Fix,
512}
513
514impl ReportField {
515 fn parse(name: &str) -> Option<ReportField> {
516 Some(match name {
517 "path" => ReportField::Path,
518 "line" => ReportField::Line,
519 "col" => ReportField::Col,
520 "start" => ReportField::Start,
521 "end" => ReportField::End,
522 "pattern" => ReportField::Pattern,
523 "rule" => ReportField::Rule,
524 "severity" => ReportField::Severity,
525 "message" => ReportField::Message,
526 "fix" => ReportField::Fix,
527 _ => return None,
528 })
529 }
530}
531
532#[derive(Clone, Copy, Debug, PartialEq, Eq)]
535pub struct ReportRule<'a> {
536 pub name: &'a str,
537 pub severity: &'a str,
538 pub message: &'a str,
539 pub fix: &'a str,
540}
541
542#[derive(Clone, Copy, Debug, PartialEq, Eq)]
544pub struct ReportAt<'a> {
545 pub path: &'a str,
547 pub line: usize,
549 pub col: usize,
551 pub base: Option<usize>,
555 pub pattern: Option<&'a str>,
557 pub rule: Option<ReportRule<'a>>,
559}
560
561impl ReportAt<'_> {
562 fn placed(&self) -> usize {
570 self.base.expect("a window is placed wherever a template writes its offsets")
571 }
572
573 fn value<M: Spanned>(&self, field: ReportField, m: &M) -> String {
574 match field {
575 ReportField::Path => self.path.to_string(),
576 ReportField::Line => self.line.to_string(),
577 ReportField::Col => self.col.to_string(),
578 ReportField::Start => (m.start() + self.placed()).to_string(),
579 ReportField::End => (m.end() + self.placed()).to_string(),
580 ReportField::Pattern => self.pattern.unwrap_or("").to_string(),
581 ReportField::Rule => self.rule.map_or("", |r| r.name).to_string(),
582 ReportField::Severity => self.rule.map_or("", |r| r.severity).to_string(),
583 ReportField::Message => self.rule.map_or("", |r| r.message).to_string(),
584 ReportField::Fix => self.rule.map_or("", |r| r.fix).to_string(),
585 }
586 }
587}
588
589#[derive(Debug)]
591pub struct Template {
592 parts: Vec<Part>,
593}
594
595#[derive(Clone, Debug, PartialEq, Eq)]
598pub struct TemplateError {
599 pub pos: usize,
601 pub msg: String,
603}
604
605impl Template {
606 pub fn parse(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
614 Self::parse_full(src, bound, false)
615 }
616
617 pub fn parse_report(src: &str, bound: &[String]) -> Result<Template, TemplateError> {
630 Self::parse_full(src, bound, true)
631 }
632
633 fn parse_full(src: &str, bound: &[String], report: bool) -> Result<Template, TemplateError> {
634 let b = src.as_bytes();
635 let mut parts: Vec<Part> = Vec::new();
636 let mut lit = String::new();
637 let mut i = 0;
638 while i < b.len() {
639 match b[i] {
640 b'$' if i + 1 < b.len() && b[i + 1] == b'$' => {
641 lit.push('$');
642 i += 2;
643 }
644 b'$' if i + 1 < b.len() && b[i + 1] == b'{' => {
645 if !lit.is_empty() {
646 parts.push(Part::Literal(std::mem::take(&mut lit)));
647 }
648 let start = i;
649 let close = b[i + 2..]
650 .iter()
651 .position(|&c| c == b'}')
652 .map(|p| i + 2 + p)
653 .ok_or(TemplateError { pos: start, msg: "unterminated ${...}".into() })?;
654 let body = &src[i + 2..close];
655 let (name, accs) = match body.split_once(':') {
656 Some((n, a)) => (n, a),
657 None => (body, ""),
658 };
659 match ReportField::parse(name) {
660 Some(field) if report => {
661 let accs = if accs.is_empty() { Vec::new() } else { parse_accessors(accs, start)? };
662 parts.push(Part::Where(field, accs));
663 }
664 _ => parts.push(parse_ref(body, start, bound, report)?),
665 }
666 i = close + 1;
667 }
668 b'\\' => {
669 let escaped = match b.get(i + 1) {
670 Some(b'n') => '\n',
671 Some(b't') => '\t',
672 Some(b'\\') => '\\',
673 Some(&other) => {
674 return Err(TemplateError {
675 pos: i,
676 msg: format!(
677 "unknown escape \\{}; a template knows \\n, \\t and \\\\",
678 other as char
679 ),
680 });
681 }
682 None => {
683 return Err(TemplateError {
684 pos: i,
685 msg: "a backslash ends the template; write \\\\ for a backslash".into(),
686 });
687 }
688 };
689 lit.push(escaped);
690 i += 2;
691 }
692 c => {
693 lit.push(c as char);
694 i += 1;
695 }
696 }
697 }
698 if !lit.is_empty() {
699 parts.push(Part::Literal(lit));
700 }
701 Ok(Template { parts })
702 }
703
704 #[must_use]
709 pub fn render<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
710 self.render_at(m, input, None, None)
711 }
712
713 #[must_use]
716 pub fn render_report<M: Spanned>(&self, m: &M, input: &[u8], at: &ReportAt<'_>) -> String {
717 self.render_at(m, input, Some(at), None)
718 }
719
720 #[must_use]
727 pub fn render_explained<M: Spanned>(
728 &self,
729 m: &M,
730 input: &[u8],
731 at: Option<&ReportAt<'_>>,
732 why: &Explanation,
733 ) -> String {
734 self.render_at(m, input, at, Some(why))
735 }
736
737 fn render_at<M: Spanned>(
738 &self,
739 m: &M,
740 input: &[u8],
741 at: Option<&ReportAt<'_>>,
742 why: Option<&Explanation>,
743 ) -> String {
744 let mut out = String::new();
745 for part in &self.parts {
746 match part {
747 Part::Literal(s) => out.push_str(s),
748 Part::Where(field, accs) => {
751 let value = at.map_or_else(String::new, |a| a.value(*field, m));
752 out.push_str(&apply_all(accs, &value));
753 }
754 Part::Explain(named, accs) => {
757 let value = why.map_or_else(String::new, |e| {
758 e.field(&named.axis, named.piece.as_deref(), named.at)
759 });
760 out.push_str(&apply_all(accs, &value));
761 }
762 Part::WholeMatch(accs) => {
763 let whole = String::from_utf8_lossy(&input[m.start()..m.end()]);
764 out.push_str(&apply_all(accs, &whole));
765 }
766 Part::Capture(name, accs) => {
767 let bytes = m
768 .names()
769 .iter()
770 .position(|k| k == name)
771 .and_then(|i| m.captures().get(i))
772 .map_or(&[] as &[u8], |s| &input[s.range()]);
773 let v = String::from_utf8_lossy(bytes);
774 out.push_str(&apply_all(accs, &v));
775 }
776 Part::Item(name, index, accs) => {
777 let bytes = m
778 .history(name)
779 .and_then(|all| all.get(*index))
780 .map_or(&[] as &[u8], |s| &input[s.range()]);
781 let v = String::from_utf8_lossy(bytes);
782 out.push_str(&apply_all(accs, &v));
783 }
784 Part::All(name, accs) => out.push_str(&every_binding(m, input, name, accs)),
785 }
786 }
787 out
788 }
789
790 fn reads_captures(&self) -> bool {
793 self.parts.iter().any(|p| matches!(p, Part::Capture(..) | Part::Item(..) | Part::All(..)))
794 }
795
796 #[must_use]
800 pub fn reads_lists(&self) -> bool {
801 self.parts.iter().any(|p| matches!(p, Part::Item(..) | Part::All(..)))
802 }
803
804 #[must_use]
807 pub fn reads_place(&self) -> bool {
808 self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Line | ReportField::Col, _)))
809 }
810
811 #[must_use]
814 pub fn reads_offsets(&self) -> bool {
815 self.parts.iter().any(|p| matches!(p, Part::Where(ReportField::Start | ReportField::End, _)))
816 }
817
818 #[must_use]
825 pub fn reads_explanation(&self) -> bool {
826 self.parts.iter().any(|p| matches!(p, Part::Explain(..)))
827 }
828
829 fn renders_one_string(&self) -> bool {
837 self.parts.iter().all(|p| matches!(p, Part::Literal(_)))
838 }
839
840 fn one_string(&self) -> String {
843 self.parts
844 .iter()
845 .map(|p| match p {
846 Part::Literal(s) => s.as_str(),
847 Part::WholeMatch(_)
848 | Part::Capture(..)
849 | Part::Item(..)
850 | Part::All(..)
851 | Part::Where(..)
852 | Part::Explain(..) => "",
853 })
854 .collect()
855 }
856}
857
858fn parse_count(r: &str, name: &str, pos: usize) -> Result<usize, TemplateError> {
860 match r.parse::<usize>() {
861 Ok(n) if n > 0 => Ok(n),
862 Ok(_) => Err(TemplateError {
863 pos,
864 msg: format!("{name}0 names no characters; write {name}N with N at least 1"),
865 }),
866 Err(e) => Err(TemplateError {
867 pos,
868 msg: format!("{name}{r}: {e}; write {name}N, as in {name}4"),
869 }),
870 }
871}
872
873#[derive(Clone, Debug)]
878pub struct Keep {
879 field: Field,
880 accessors: Vec<Accessor>,
881}
882
883impl Keep {
884 pub fn parse_list(src: &str, bound: &[String]) -> Result<Vec<Keep>, TemplateError> {
893 let mut out = Vec::new();
894 let mut pos = 0;
895 for entry in src.split(',') {
896 let at = pos + (entry.len() - entry.trim_start().len());
897 pos += entry.len() + 1;
898 let body = entry.trim();
899 if body.is_empty() {
900 continue;
901 }
902 let (field, accessors) = parse_field(body, at, bound)?;
903 if let Some(transform) = accessors.iter().find(|a| !a.slices()) {
904 let name = if *transform == Accessor::Upper { "upper" } else { "lower" };
905 return Err(TemplateError {
906 pos: at,
907 msg: format!(
908 "`:{name}` transforms the text rather than slicing it, so it names nothing to keep in place"
909 ),
910 });
911 }
912 out.push(Keep { field, accessors });
913 }
914 Ok(out)
915 }
916
917 fn locate<M: Spanned>(&self, m: &M, input: &[u8]) -> Option<ByteRange> {
922 let base = match &self.field {
923 Field::All(_) => return None,
924 Field::Whole => m.start()..m.end(),
925 Field::Register(name) => {
926 let i = m.names().iter().position(|k| k == name)?;
927 m.captures().get(i)?.range()
928 }
929 Field::Item(name, index) => m.history(name)?.get(*index)?.range(),
930 };
931 let text = match String::from_utf8_lossy(&input[base.clone()]) {
932 Cow::Borrowed(t) => t,
933 Cow::Owned(_) => return None,
934 };
935 let r = locate_all(&self.accessors, text)?;
936 Some(base.start + r.start..base.start + r.end)
937 }
938}
939
940const MASKED_DIGIT: u8 = b'0';
951const MASKED_LETTER: u8 = b'a';
952
953#[derive(Clone, Debug, Default, PartialEq, Eq)]
960pub struct Pseudonyms {
961 seen: Vec<(String, Vec<Vec<u8>>)>,
962}
963
964impl Pseudonyms {
965 fn name(&mut self, kind: &str, value: &[u8]) -> String {
967 let values = match self.seen.iter().position(|(k, _)| k == kind) {
968 Some(at) => &mut self.seen[at].1,
969 None => {
970 self.seen.push((kind.to_string(), Vec::new()));
971 let last = self.seen.len() - 1;
972 &mut self.seen[last].1
973 }
974 };
975 let at = match values.iter().position(|v| v == value) {
976 Some(at) => at,
977 None => {
978 values.push(value.to_vec());
979 values.len() - 1
980 }
981 };
982 format!("{}_{}", kind.to_uppercase(), at + 1)
987 }
988}
989
990#[derive(Clone, Debug, PartialEq, Eq)]
992pub enum Mask {
993 PerChar(char),
996 Token(String),
998 Shape,
1002 Pseudonym(Pseudonyms),
1007}
1008
1009impl Mask {
1010 pub fn parse(src: &str) -> Result<Mask, String> {
1019 if src == "shape" {
1020 return Ok(Mask::Shape);
1021 }
1022 if src == "pseudonym" {
1023 return Ok(Mask::Pseudonym(Pseudonyms::default()));
1024 }
1025 let mut chars = src.chars();
1026 match (chars.next(), chars.next()) {
1027 (None, _) => Err("the mask is empty; give a character or a token".to_string()),
1028 (Some(c), None) => Ok(Mask::PerChar(c)),
1029 (Some(_), Some(_)) => Ok(Mask::Token(src.to_string())),
1030 }
1031 }
1032
1033 fn kind_of(run: &[u8], lexing: &crate::ShapeSet) -> String {
1038 let toks = if lexing.is_empty() {
1039 crate::lexer::lex(run)
1040 } else {
1041 crate::lexer::lex_with_shapes(run, &crate::lexer::blob_runs(run), lexing, 0)
1042 };
1043 let mut significant = toks.iter().filter(|t| t.is_significant());
1044 match (significant.next(), significant.next()) {
1045 (Some(one), None) => match one.kind {
1046 crate::token::TokenKind::Custom(id) => match lexing.name_of(id) {
1047 Some(name) => name.to_string(),
1048 None => one.kind.name().to_string(),
1049 },
1050 other => other.name().to_string(),
1051 },
1052 _ => "value".to_string(),
1053 }
1054 }
1055
1056 fn cover(&mut self, run: &[u8], out: &mut Vec<u8>, lexing: &crate::ShapeSet) {
1059 match self {
1060 Mask::PerChar(c) => {
1061 let mut buf = [0u8; 4];
1062 let encoded = c.encode_utf8(&mut buf).as_bytes();
1063 let chars = run.iter().filter(|&&b| b & 0xC0 != 0x80).count();
1064 for _ in 0..chars {
1065 out.extend_from_slice(encoded);
1066 }
1067 }
1068 Mask::Token(t) => out.extend_from_slice(t.as_bytes()),
1069 Mask::Shape => out.extend(run.iter().map(|&b| match b {
1070 b'0'..=b'9' => MASKED_DIGIT,
1071 b'A'..=b'Z' | b'a'..=b'z' => MASKED_LETTER,
1072 other => other,
1073 })),
1074 Mask::Pseudonym(book) => {
1075 let kind = Mask::kind_of(run, lexing);
1076 out.extend_from_slice(book.name(&kind, run).as_bytes());
1077 }
1078 }
1079 }
1080}
1081
1082#[must_use]
1089pub fn redactions<M: Spanned>(
1090 input: &[u8],
1091 matches: &[M],
1092 keeps: &[Keep],
1093 mask: &mut Mask,
1094) -> Vec<crate::files::Edit> {
1095 redactions_under(input, matches, keeps, mask, &crate::ShapeSet::new())
1096}
1097
1098#[must_use]
1102pub fn redactions_with_shapes<M: Spanned>(
1103 input: &[u8],
1104 matches: &[M],
1105 keeps: &[Keep],
1106 mask: &mut Mask,
1107 pattern: &Pattern,
1108 shapes: &crate::ShapeSet,
1109) -> Vec<crate::files::Edit> {
1110 redactions_under(input, matches, keeps, mask, &shapes.with_library_shapes(&pattern.library_kinds()))
1111}
1112
1113fn redactions_under<M: Spanned>(
1115 input: &[u8],
1116 matches: &[M],
1117 keeps: &[Keep],
1118 mask: &mut Mask,
1119 lexing: &crate::ShapeSet,
1120) -> Vec<crate::files::Edit> {
1121 matches
1122 .iter()
1123 .map(|m| {
1124 let (start, end) = (m.start(), m.end());
1125 let mut kept: Vec<ByteRange> = keeps
1126 .iter()
1127 .filter_map(|k| k.locate(m, input))
1128 .map(|r| r.start.max(start)..r.end.min(end))
1129 .filter(|r| !r.is_empty())
1130 .collect();
1131 kept.sort_by_key(|r| (r.start, r.end));
1132 let mut replacement = Vec::with_capacity(end - start);
1133 let mut cursor = start;
1134 for r in kept {
1135 if r.start > cursor {
1136 mask.cover(&input[cursor..r.start], &mut replacement, lexing);
1137 }
1138 if r.end > cursor {
1139 replacement.extend_from_slice(&input[r.start.max(cursor)..r.end]);
1140 cursor = r.end;
1141 }
1142 }
1143 if cursor < end {
1144 mask.cover(&input[cursor..end], &mut replacement, lexing);
1145 }
1146 crate::files::Edit { start, end, replacement }
1147 })
1148 .collect()
1149}
1150
1151pub trait Spanned: Sync {
1154 fn start(&self) -> usize;
1156 fn end(&self) -> usize;
1158 fn captures(&self) -> &[Span];
1160 fn names(&self) -> &[String];
1163 fn history(&self, _name: &str) -> Option<&[Span]> {
1167 None
1168 }
1169}
1170
1171impl Spanned for Span {
1172 fn start(&self) -> usize {
1173 Span::start(self)
1174 }
1175
1176 fn end(&self) -> usize {
1177 Span::end(self)
1178 }
1179
1180 fn captures(&self) -> &[Span] {
1181 &[]
1182 }
1183
1184 fn names(&self) -> &[String] {
1185 &[]
1186 }
1187}
1188
1189impl Spanned for Match {
1190 fn start(&self) -> usize {
1191 self.start
1192 }
1193
1194 fn end(&self) -> usize {
1195 self.end
1196 }
1197
1198 fn captures(&self) -> &[Span] {
1199 &self.captures
1200 }
1201
1202 fn names(&self) -> &[String] {
1203 Match::names(self)
1204 }
1205
1206 fn history(&self, name: &str) -> Option<&[Span]> {
1207 self.list(name)
1208 }
1209}
1210
1211#[derive(Clone, Debug, PartialEq, Eq)]
1215pub enum Field {
1216 Whole,
1218 Register(String),
1221 Item(String, usize),
1224 All(String),
1227}
1228
1229fn every_binding<M: Spanned>(m: &M, input: &[u8], name: &str, accs: &[Accessor]) -> String {
1233 let read = |range: std::ops::Range<usize>| apply_all(accs, &String::from_utf8_lossy(&input[range]));
1234 match m.history(name) {
1235 Some(all) => all.iter().map(|s| read(s.range())).collect::<Vec<_>>().join(","),
1236 None => match m.names().iter().position(|k| k == name).and_then(|i| m.captures().get(i)) {
1237 Some(s) => read(s.range()),
1238 None => String::new(),
1239 },
1240 }
1241}
1242
1243fn parse_explain(body: &str, pos: usize, allowed: bool) -> Result<Option<Part>, TemplateError> {
1251 if !body.starts_with('@') {
1252 return Ok(None);
1253 }
1254 if !allowed {
1255 return Err(TemplateError {
1256 pos,
1257 msg: format!(
1258 "${{{body}}} reads an axis, which a scan's --format renders; \
1259 the bytes a rewrite splices in have no explanation to read"
1260 ),
1261 });
1262 }
1263 let (name, accs) = match body.split_once(':') {
1264 Some((n, a)) => (n, parse_accessors(a, pos)?),
1265 None => (body, Vec::new()),
1266 };
1267 let (name, at) = match split_index(&name[1..], pos)? {
1268 (name, Some(Pick::All)) => {
1269 return Err(TemplateError {
1270 pos,
1271 msg: format!("${{@{name}[*]}}: an axis written bare already joins every reading; [i] picks one"),
1272 });
1273 }
1274 (name, Some(Pick::At(i))) => (name, Some(i)),
1275 (name, None) => (name, None),
1276 };
1277 let (axis, piece) = match name.split_once('.') {
1278 Some((axis, piece)) => (axis, Some(piece)),
1279 None => (name.as_str(), None),
1280 };
1281 crate::explain::check_explain_field(axis, piece)
1282 .map_err(|msg| TemplateError { pos, msg })?;
1283 let named =
1284 ExplainRef { axis: axis.to_string(), piece: piece.map(str::to_string), at };
1285 Ok(Some(Part::Explain(named, accs)))
1286}
1287
1288fn parse_ref(
1289 body: &str,
1290 pos: usize,
1291 bound: &[String],
1292 axes: bool,
1293) -> Result<Part, TemplateError> {
1294 if let Some(part) = parse_explain(body, pos, axes)? {
1295 return Ok(part);
1296 }
1297 let (field, accs) = parse_field(body, pos, bound)?;
1298 Ok(match field {
1299 Field::Whole => Part::WholeMatch(accs),
1300 Field::Register(name) => Part::Capture(name, accs),
1301 Field::Item(name, index) => Part::Item(name, index, accs),
1302 Field::All(name) => Part::All(name, accs),
1303 })
1304}
1305
1306enum Pick {
1309 At(usize),
1310 All,
1311}
1312
1313fn split_index(name: &str, pos: usize) -> Result<(String, Option<Pick>), TemplateError> {
1317 let mut out = String::new();
1318 let mut index = None;
1319 for segment in name.split('.') {
1320 let (base, at) = match segment.split_once('[') {
1321 Some((base, rest)) => {
1322 let digits = rest.strip_suffix(']').ok_or_else(|| TemplateError {
1323 pos,
1324 msg: format!("{segment:?}: an index closes with ], as in {base}[0]"),
1325 })?;
1326 if digits == "*" {
1327 (base, Some(Pick::All))
1328 } else {
1329 let i = digits.parse::<usize>().map_err(|e| TemplateError {
1330 pos,
1331 msg: format!("{segment:?}: an index is a number counted from 0, or * for every one: {e}"),
1332 })?;
1333 (base, Some(Pick::At(i)))
1334 }
1335 }
1336 None => (segment, None),
1337 };
1338 if let Some(i) = at {
1339 if index.is_some() {
1340 return Err(TemplateError {
1341 pos,
1342 msg: format!("{name:?} carries two indexes; a reference carries one"),
1343 });
1344 }
1345 index = Some(i);
1346 }
1347 if !out.is_empty() {
1348 out.push('.');
1349 }
1350 out.push_str(base);
1351 }
1352 Ok((out, index))
1353}
1354
1355fn parse_field(
1358 body: &str,
1359 pos: usize,
1360 bound: &[String],
1361) -> Result<(Field, Vec<Accessor>), TemplateError> {
1362 let (name, accs) = match body.split_once(':') {
1363 Some((n, a)) => (n, parse_accessors(a, pos)?),
1364 None => (body, Vec::new()),
1365 };
1366 if name.is_empty() {
1367 return Err(TemplateError { pos, msg: "empty capture name in ${...}".into() });
1368 }
1369 if let Some(axis) = name.strip_prefix('@') {
1373 return Err(TemplateError {
1374 pos,
1375 msg: format!(
1376 "${{@{axis}}} reads an axis, which a --format or report template renders and a reference to the match's bytes cannot"
1377 ),
1378 });
1379 }
1380 let (name, index) = split_index(name, pos)?;
1381 if name == "0" {
1382 return match index {
1383 None => Ok((Field::Whole, accs)),
1384 Some(_) => Err(TemplateError { pos, msg: "${0} is the whole match and takes no index".into() }),
1385 };
1386 }
1387 let field = |name: String| match index {
1388 Some(Pick::At(i)) => Field::Item(name, i),
1389 Some(Pick::All) => Field::All(name),
1390 None => Field::Register(name),
1391 };
1392 if name.bytes().all(|b| b.is_ascii_digit()) {
1398 let n: usize = match name.parse() {
1399 Ok(n) => n,
1400 Err(e) => {
1401 return Err(TemplateError {
1402 pos,
1403 msg: format!("capture number {name:?} is out of range: {e}"),
1404 });
1405 }
1406 };
1407 return match bound.get(n.wrapping_sub(1)) {
1408 Some(found) => Ok((field(found.clone()), accs)),
1409 None => Err(TemplateError {
1410 pos,
1411 msg: format!(
1412 "template references ${{{n}}} but the pattern binds {} capture(s)",
1413 bound.len()
1414 ),
1415 }),
1416 };
1417 }
1418 if !bound.contains(&name) {
1419 return Err(TemplateError {
1420 pos,
1421 msg: format!("template references ${{{name}}} but the pattern binds no such capture"),
1422 });
1423 }
1424 Ok((field(name), accs))
1425}
1426
1427#[derive(Clone, Debug, PartialEq, Eq)]
1432pub struct Reference {
1433 field: Field,
1434 accessors: Vec<Accessor>,
1435}
1436
1437impl Reference {
1438 pub fn parse(body: &str, bound: &[String]) -> Result<Reference, TemplateError> {
1446 let (field, accessors) = parse_field(body, 0, bound)?;
1447 Ok(Reference { field, accessors })
1448 }
1449
1450 #[must_use]
1452 pub fn field(&self) -> &Field {
1453 &self.field
1454 }
1455
1456 #[must_use]
1459 pub fn apply(&self, text: &str) -> String {
1460 apply_all(&self.accessors, text)
1461 }
1462
1463 #[must_use]
1466 pub fn read<M: Spanned>(&self, m: &M, input: &[u8]) -> String {
1467 let bytes = match &self.field {
1468 Field::All(name) => return every_binding(m, input, name, &self.accessors),
1469 Field::Whole => &input[m.start()..m.end()],
1470 Field::Register(name) => m
1471 .names()
1472 .iter()
1473 .position(|k| k == name)
1474 .and_then(|i| m.captures().get(i))
1475 .map_or(&[] as &[u8], |s| &input[s.range()]),
1476 Field::Item(name, index) => m
1477 .history(name)
1478 .and_then(|all| all.get(*index))
1479 .map_or(&[] as &[u8], |s| &input[s.range()]),
1480 };
1481 self.apply(&String::from_utf8_lossy(bytes))
1482 }
1483}
1484
1485fn parse_accessors(s: &str, pos: usize) -> Result<Vec<Accessor>, TemplateError> {
1487 s.split('|').map(|t| parse_accessor(t.trim(), pos)).collect()
1488}
1489
1490fn parse_accessor(t: &str, pos: usize) -> Result<Accessor, TemplateError> {
1491 let a = match t {
1492 "upper" => Accessor::Upper,
1493 "lower" => Accessor::Lower,
1494 "trim" => Accessor::Trim,
1495 "scheme" => Accessor::UrlScheme,
1496 "host" => Accessor::UrlHost,
1497 "port" => Accessor::UrlPort,
1498 "path" => Accessor::UrlPath,
1499 "query" => Accessor::UrlQuery,
1500 "user" => Accessor::EmailUser,
1501 "domain" => Accessor::EmailDomain,
1502 "major" => Accessor::VerMajor,
1503 "minor" => Accessor::VerMinor,
1504 "patch" => Accessor::VerPatch,
1505 "year" => Accessor::TsField(0),
1506 "month" => Accessor::TsField(1),
1507 "day" => Accessor::TsField(2),
1508 "hour" => Accessor::TsField(3),
1509 "minute" => Accessor::TsField(4),
1510 "second" => Accessor::TsField(5),
1511 "dir" => Accessor::PathDir,
1512 "name" => Accessor::PathName,
1513 "ext" => Accessor::PathExt,
1514 "value" => Accessor::QtyValue,
1515 "unit" => Accessor::QtyUnit,
1516 _ => {
1517 if let Some(r) = t.strip_prefix("octet") {
1518 let (x, y) = parse_range(r, pos)?;
1519 Accessor::Octet(x, y)
1520 } else if let Some(r) = t.strip_prefix("group") {
1521 let (x, y) = parse_range(r, pos)?;
1522 Accessor::Group(x, y)
1523 } else if let Some(r) = t.strip_prefix("first") {
1524 Accessor::First(parse_count(r, "first", pos)?)
1525 } else if let Some(r) = t.strip_prefix("last") {
1526 Accessor::Last(parse_count(r, "last", pos)?)
1527 } else {
1528 return Err(TemplateError {
1529 pos,
1530 msg: format!(
1531 "unknown accessor ':{t}' (use upper/lower/trim; firstN/lastN; octetN[-M]; groupN[-M]; \
1532 scheme/host/port/path/query; user/domain; major/minor/patch; \
1533 year/month/day/hour/minute/second; dir/name/ext; value/unit)"
1534 ),
1535 });
1536 }
1537 }
1538 };
1539 Ok(a)
1540}
1541
1542fn parse_range(r: &str, pos: usize) -> Result<(usize, usize), TemplateError> {
1544 let bad = || TemplateError { pos, msg: format!("bad index '{r}' (use N or N-M, 1-based)") };
1545 match r.split_once('-') {
1546 Some((a, b)) => {
1547 let a = a.parse::<usize>().map_err(|_| bad())?;
1548 let b = b.parse::<usize>().map_err(|_| bad())?;
1549 if a == 0 || b < a {
1550 return Err(bad());
1551 }
1552 Ok((a, b))
1553 }
1554 None => {
1555 let a = r.parse::<usize>().map_err(|_| bad())?;
1556 if a == 0 {
1557 return Err(bad());
1558 }
1559 Ok((a, a))
1560 }
1561 }
1562}
1563
1564fn resolve(pattern: &Pattern, template: &Template, input: &[u8], spans: &[Span]) -> Vec<Match> {
1568 if template.reads_lists() {
1569 captures_with_lists(pattern, input, spans)
1570 } else {
1571 captures(pattern, input, spans)
1572 }
1573}
1574
1575fn resolve_with_shapes(
1578 pattern: &Pattern,
1579 template: &Template,
1580 input: &[u8],
1581 shapes: &crate::custom::ShapeSet,
1582 spans: &[Span],
1583) -> Vec<Match> {
1584 if template.reads_lists() {
1585 crate::engine::captures_with_shapes_and_lists(pattern, input, shapes, spans)
1586 } else {
1587 crate::engine::captures_with_shapes(pattern, input, shapes, spans)
1588 }
1589}
1590
1591#[must_use]
1596pub fn edits_with_shapes(
1597 pattern: &Pattern,
1598 template: &Template,
1599 input: &[u8],
1600 shapes: &crate::custom::ShapeSet,
1601) -> Vec<crate::files::Edit> {
1602 edits_at(pattern, template, input, shapes, &crate::engine::scan_with_shapes(pattern, input, shapes))
1603}
1604
1605#[must_use]
1610pub fn edits_at(
1611 pattern: &Pattern,
1612 template: &Template,
1613 input: &[u8],
1614 shapes: &crate::custom::ShapeSet,
1615 spans: &[Span],
1616) -> Vec<crate::files::Edit> {
1617 if template.reads_captures() {
1618 resolve_with_shapes(pattern, template, input, shapes, spans)
1619 .iter()
1620 .map(|m| crate::files::Edit {
1621 start: m.start,
1622 end: m.end,
1623 replacement: template.render(m, input).into_bytes(),
1624 })
1625 .collect()
1626 } else {
1627 spans
1628 .iter()
1629 .map(|s| crate::files::Edit {
1630 start: s.start(),
1631 end: s.end(),
1632 replacement: template.render(s, input).into_bytes(),
1633 })
1634 .collect()
1635 }
1636}
1637
1638#[must_use]
1642pub fn rewrite_with_shapes(
1643 pattern: &Pattern,
1644 template: &Template,
1645 input: &[u8],
1646 shapes: &crate::custom::ShapeSet,
1647) -> Vec<u8> {
1648 if shapes.is_empty() && pattern.library_kinds().is_empty() {
1649 return rewrite(pattern, template, input);
1650 }
1651 let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
1652 if template.reads_captures() {
1653 return splice_parallel(input, &resolve_with_shapes(pattern, template, input, shapes, &spans), template);
1654 }
1655 splice_parallel(input, &spans, template)
1656}
1657
1658#[must_use]
1661pub fn edits(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<crate::files::Edit> {
1662 let spans = scan(pattern, input);
1663 if template.reads_captures() {
1664 resolve(pattern, template, input, &spans)
1665 .iter()
1666 .map(|m| crate::files::Edit {
1667 start: m.start,
1668 end: m.end,
1669 replacement: template.render(m, input).into_bytes(),
1670 })
1671 .collect()
1672 } else {
1673 spans
1674 .iter()
1675 .map(|s| crate::files::Edit {
1676 start: s.start(),
1677 end: s.end(),
1678 replacement: template.render(s, input).into_bytes(),
1679 })
1680 .collect()
1681 }
1682}
1683
1684#[must_use]
1688pub fn rewrite(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
1689 let spans = scan(pattern, input);
1690 if template.reads_captures() {
1691 return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
1692 }
1693 splice_parallel(input, &spans, template)
1694}
1695
1696#[must_use]
1709pub fn rewrite_n(pattern: &Pattern, template: &Template, input: &[u8], n: usize) -> Vec<u8> {
1710 let spans: Vec<crate::engine::Span> = if n == 1 {
1714 crate::cursor::find(pattern, input).into_iter().collect()
1715 } else {
1716 crate::cursor::find_iter(pattern, input).take(n).collect()
1717 };
1718 if template.reads_captures() {
1719 return splice_parallel(input, &resolve(pattern, template, input, &spans), template);
1720 }
1721 splice_parallel(input, &spans, template)
1722}
1723
1724#[must_use]
1726pub fn rewrite_first(pattern: &Pattern, template: &Template, input: &[u8]) -> Vec<u8> {
1727 rewrite_n(pattern, template, input, 1)
1728}
1729
1730#[derive(Clone, Copy, Debug)]
1734pub struct Matched<'a> {
1735 input: &'a [u8],
1736 m: &'a Match,
1737 bound: &'a [String],
1740 kinds: &'a [(String, Option<crate::token::TokenKind>)],
1743}
1744
1745impl<'a> Matched<'a> {
1746 #[must_use]
1753 pub fn new(m: &'a Match, input: &'a [u8], bound: &'a [String]) -> Self {
1754 Matched { input, m, bound, kinds: &[] }
1755 }
1756
1757 #[must_use]
1760 pub fn with_kinds(
1761 m: &'a Match,
1762 input: &'a [u8],
1763 bound: &'a [String],
1764 kinds: &'a [(String, Option<crate::token::TokenKind>)],
1765 ) -> Self {
1766 Matched { input, m, bound, kinds }
1767 }
1768
1769 #[must_use]
1778 pub fn value(&self, name: &str) -> Option<crate::typed::TypedValue> {
1779 let kind = self.kinds.iter().find(|(n, _)| n == name).and_then(|(_, k)| *k)?;
1780 let text = self.group(name)?;
1781 crate::typed::value_of(kind, &String::from_utf8_lossy(text))
1782 }
1783
1784 #[must_use]
1786 pub fn start(&self) -> usize {
1787 self.m.start
1788 }
1789
1790 #[must_use]
1792 pub fn end(&self) -> usize {
1793 self.m.end
1794 }
1795
1796 #[must_use]
1798 pub fn as_bytes(&self) -> &'a [u8] {
1799 &self.input[self.m.start..self.m.end]
1800 }
1801
1802 #[must_use]
1805 pub fn text(&self) -> Cow<'a, str> {
1806 String::from_utf8_lossy(self.as_bytes())
1807 }
1808
1809 #[must_use]
1811 pub fn input(&self) -> &'a [u8] {
1812 self.input
1813 }
1814
1815 #[must_use]
1818 pub fn inner(&self) -> &'a Match {
1819 self.m
1820 }
1821
1822 #[must_use]
1825 pub fn group(&self, name: &str) -> Option<&'a [u8]> {
1826 self.m.group(name, self.input)
1827 }
1828
1829 #[must_use]
1832 pub fn names(&self) -> &'a [String] {
1833 self.m.names()
1834 }
1835
1836 pub fn get(&self, reference: &str) -> Result<String, TemplateError> {
1845 Ok(Reference::parse(reference, self.bound)?.read(self.m, self.input))
1846 }
1847}
1848
1849fn splice_with<F, R>(
1853 input: &[u8],
1854 matches: &[Match],
1855 bound: &[String],
1856 kinds: &[(String, Option<crate::token::TokenKind>)],
1857 mut replace: F,
1858) -> Vec<u8>
1859where
1860 F: FnMut(&Matched<'_>) -> R,
1861 R: AsRef<[u8]>,
1862{
1863 let mut out = Vec::with_capacity(input.len());
1864 let mut at = 0;
1865 for m in matches {
1866 out.extend_from_slice(&input[at..m.start]);
1867 out.extend_from_slice(replace(&Matched { input, m, bound, kinds }).as_ref());
1868 at = m.end;
1869 }
1870 out.extend_from_slice(&input[at..]);
1871 out
1872}
1873
1874pub fn rewrite_with<F, R>(pattern: &Pattern, input: &[u8], replace: F) -> Vec<u8>
1881where
1882 F: FnMut(&Matched<'_>) -> R,
1883 R: AsRef<[u8]>,
1884{
1885 let spans = scan(pattern, input);
1886 let bound = pattern.capture_names();
1887 let kinds = pattern.capture_kinds();
1888 splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1890}
1891
1892pub fn rewrite_n_with<F, R>(pattern: &Pattern, input: &[u8], n: usize, replace: F) -> Vec<u8>
1895where
1896 F: FnMut(&Matched<'_>) -> R,
1897 R: AsRef<[u8]>,
1898{
1899 let spans: Vec<Span> = if n == 1 {
1900 crate::cursor::find(pattern, input).into_iter().collect()
1901 } else {
1902 crate::cursor::find_iter(pattern, input).take(n).collect()
1903 };
1904 let bound = pattern.capture_names();
1905 let kinds = pattern.capture_kinds();
1906 splice_with(input, &captures_with_lists(pattern, input, &spans), &bound, &kinds, replace)
1908}
1909
1910#[must_use]
1916pub fn rewrite_gpu(pattern: &Pattern, template: &Template, input: &[u8]) -> Option<Vec<u8>> {
1917 let spans = scan_gpu(pattern, input)?;
1918 if template.reads_captures() {
1919 return Some(splice_parallel(input, &resolve(pattern, template, input, &spans), template));
1920 }
1921 Some(splice_parallel(input, &spans, template))
1922}
1923
1924#[must_use]
1930pub fn rewrite_with_backend(
1931 pattern: &Pattern,
1932 template: &Template,
1933 input: &[u8],
1934 backend: Backend,
1935) -> (Vec<u8>, BackendUsed) {
1936 match backend {
1937 Backend::Cpu => (rewrite(pattern, template, input), BackendUsed::Cpu),
1938 Backend::Gpu => match rewrite_gpu(pattern, template, input) {
1939 Some(o) => (o, BackendUsed::Gpu),
1940 None => (rewrite(pattern, template, input), BackendUsed::Cpu),
1941 },
1942 Backend::Auto => {
1943 let (spans, used) = scan_with_backend(pattern, input, Backend::Auto);
1944 let out = if template.reads_captures() {
1945 splice_parallel(input, &resolve(pattern, template, input, &spans), template)
1946 } else {
1947 splice_parallel(input, &spans, template)
1948 };
1949 (out, used)
1950 }
1951 }
1952}
1953
1954#[must_use]
1957pub(crate) fn splice<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
1958 let mut out: Vec<u8> = Vec::with_capacity(input.len());
1959 let mut pos = 0;
1960 for m in matches {
1961 out.extend_from_slice(&input[pos..m.start()]);
1962 out.extend_from_slice(template.render(m, input).as_bytes());
1963 pos = m.end();
1964 }
1965 out.extend_from_slice(&input[pos..]);
1966 out
1967}
1968
1969#[must_use]
1976fn splice_one_string<M: Spanned>(input: &[u8], matches: &[M], rendered: &[u8]) -> Vec<u8> {
1977 let replaced: usize = matches.iter().map(|m| m.end() - m.start()).sum();
1978 let mut out: Vec<u8> =
1979 Vec::with_capacity(input.len() - replaced + matches.len() * rendered.len());
1980 let mut pos = 0;
1981 for m in matches {
1982 out.extend_from_slice(&input[pos..m.start()]);
1983 out.extend_from_slice(rendered);
1984 pos = m.end();
1985 }
1986 out.extend_from_slice(&input[pos..]);
1987 out
1988}
1989
1990const PARALLEL_REWRITE_THRESHOLD: usize = 1024;
1995
1996#[must_use]
2001pub(crate) fn splice_parallel<M: Spanned>(input: &[u8], matches: &[M], template: &Template) -> Vec<u8> {
2002 if template.renders_one_string() {
2007 return splice_one_string(input, matches, template.one_string().as_bytes());
2008 }
2009 let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
2010 if matches.len() < PARALLEL_REWRITE_THRESHOLD || cores <= 1 {
2011 return splice(input, matches, template);
2012 }
2013
2014 let mut renders: Vec<Vec<u8>> = matches.iter().map(|_| Vec::new()).collect();
2020 let min_leaf = matches.len().div_ceil(cores * 4).max(64);
2021 let plan = flynnel::JobPlan::new(0, matches.len() as u32)
2022 .with_leaf_shape(flynnel::LeafShape::PortCompute);
2023 flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf(
2024 &plan,
2025 &mut renders,
2026 min_leaf,
2027 |start, slots| {
2028 for (i, slot) in slots.iter_mut().enumerate() {
2029 *slot = template.render(&matches[start + i], input).into_bytes();
2030 }
2031 },
2032 );
2033
2034 let total: usize =
2036 input.len() + renders.iter().map(Vec::len).sum::<usize>() - spanned_len(matches);
2037 let mut out: Vec<u8> = Vec::with_capacity(total);
2038 let mut pos = 0;
2039 for (m, r) in matches.iter().zip(&renders) {
2040 out.extend_from_slice(&input[pos..m.start()]);
2041 out.extend_from_slice(r);
2042 pos = m.end();
2043 }
2044 out.extend_from_slice(&input[pos..]);
2045 out
2046}
2047
2048fn spanned_len<M: Spanned>(matches: &[M]) -> usize {
2051 matches.iter().map(|m| m.end() - m.start()).sum()
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056 use super::*;
2057 use crate::parser::parse;
2058
2059 #[test]
2063 fn a_timestamp_field_is_read_where_its_form_puts_it() {
2064 let fields =
2065 |v: &str| (0..6).map(|i| Accessor::TsField(i).apply(v)).collect::<Vec<_>>().join("|");
2066 assert_eq!(fields("2026-09-15T10:11:12"), "2026|09|15|10|11|12");
2067 assert_eq!(fields("2026/09/15 10:11:12"), "2026|09|15|10|11|12");
2068 assert_eq!(fields("15/09/2026 10:11:12"), "2026|09|15|10|11|12");
2069 assert_eq!(fields("09/15/2026 10:11:12"), "2026|09|15|10|11|12");
2070 assert_eq!(fields("15/Sep/2026:10:11:12"), "2026||15|10|11|12");
2073 assert_eq!(fields("Sep 15 10:11:12"), "||15|10|11|12");
2074 assert_eq!(fields("10:11:12"), "|||10|11|12");
2075 }
2076
2077 #[test]
2081 fn a_template_of_literals_splices_what_rendering_each_match_splices() {
2082 let mut text = String::new();
2083 for i in 0..2000u32 {
2084 text.push_str(&format!("let value_{i} = {} ; call_{i}(alpha, beta) ;\n", i * 7));
2085 }
2086 let input = text.as_bytes();
2087 let pat = parse("\"let\" \\W:v \"=\"").expect("pattern parses");
2088 let names = pat.capture_names();
2089 let spans = crate::scan(&pat, input);
2090 let ms = crate::captures(&pat, input, &spans);
2091 assert!(ms.len() > PARALLEL_REWRITE_THRESHOLD, "the corpus crosses the parallel threshold");
2092 for (src, one) in [
2093 ("X", true),
2094 ("", true),
2095 ("<>", true),
2096 ("[${0}]", false),
2097 ("${v}", false),
2098 ("a${v}b", false),
2099 ("${0}${v}", false),
2100 ] {
2101 let tpl = Template::parse(src, &names).expect("template parses");
2102 assert_eq!(tpl.renders_one_string(), one, "{src:?}");
2103 assert_eq!(splice_parallel(input, &ms, &tpl), splice(input, &ms, &tpl), "{src:?}");
2104 if !tpl.reads_captures() {
2107 assert_eq!(
2108 splice_parallel(input, &spans, &tpl),
2109 splice(input, &spans, &tpl),
2110 "{src:?} over spans"
2111 );
2112 }
2113 }
2114 }
2115
2116 #[test]
2117 fn rewriting_one_match_takes_a_different_path_and_the_same_answer() {
2118 let inputs = [
2123 "alpha beta alpha gamma alpha",
2124 "let a = 1 ; let b = 2 ; let c = 3 ;",
2125 "nothing here matches at all",
2126 ];
2127 for src in ["\"alpha\"", "\\W \"=\"", "\\N", "\\W:k \"=\""] {
2128 for input in inputs {
2129 let pat = parse(src).expect("pattern parses");
2130 let tpl = Template::parse("X", &pat.capture_names()).expect("template parses");
2131 let one = rewrite_first(&pat, &tpl, input.as_bytes());
2132 let by_n = rewrite_n(&pat, &tpl, input.as_bytes(), 1);
2133 assert_eq!(one, by_n, "{src} over {input:?}");
2134
2135 let first: Vec<_> = crate::cursor::find_iter(&pat, input.as_bytes()).take(1).collect();
2138 let want = splice_parallel(input.as_bytes(), &first, &tpl);
2139 assert_eq!(one, want, "{src} over {input:?}: the paths disagree");
2140 }
2141 }
2142 }
2143
2144 fn rw(pattern_src: &str, template_src: &str, input: &str) -> String {
2145 let pat = parse(pattern_src).expect("pattern parses");
2146 let tpl = Template::parse(template_src, &pat.capture_names()).expect("template parses");
2147 String::from_utf8(rewrite(&pat, &tpl, input.as_bytes())).expect("utf8")
2148 }
2149
2150 #[test]
2151 fn a_capture_can_be_referenced_by_position() {
2152 assert_eq!(
2155 rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"),
2156 rw("\\W:first \\W:second", "${second} ${first}", "alpha beta"),
2157 );
2158 assert_eq!(rw("\\W:first \\W:second", "${2} ${1}", "alpha beta"), "beta alpha");
2159 assert_eq!(rw("\\W:a \\W:b", "${1:upper}", "alpha beta"), "ALPHA");
2161 assert_eq!(rw("\\W:a \\W:b", "[${0}]", "alpha beta"), "[alpha beta]");
2163 }
2164
2165 #[test]
2166 fn a_position_past_the_last_capture_is_a_template_error() {
2167 let pat = parse("\\W:only").expect("parses");
2168 let err = Template::parse("${2}", &pat.capture_names()).expect_err("refused");
2169 let msg = format!("{err:?}");
2170 assert!(msg.contains('2'), "names the position: {msg}");
2171 assert!(msg.contains('1'), "and says how many there are: {msg}");
2172 }
2173
2174 #[test]
2175 fn redacts_typed_atom() {
2176 assert_eq!(rw("\\E:e", "[redacted]", "mail bob@x.com now"), "mail [redacted] now");
2177 }
2178
2179 #[test]
2180 fn a_pseudonym_names_a_declared_or_library_kind_by_its_own_name() {
2181 fn replaced(pattern: &Pattern, shapes: &crate::ShapeSet, input: &[u8]) -> Vec<Vec<u8>> {
2182 let spans = crate::engine::scan_with_shapes(pattern, input, shapes);
2183 let mut mask = Mask::parse("pseudonym").expect("a mask");
2184 redactions_with_shapes(input, &spans, &[], &mut mask, pattern, shapes)
2185 .into_iter()
2186 .map(|e| e.replacement)
2187 .collect()
2188 }
2189 let mut shapes = crate::ShapeSet::new();
2190 shapes.declare_text("shape customer = `C\\d{5}`").expect("declares");
2191 let customer = crate::parser::parse_with_shapes("\\{customer}", &shapes).expect("parses");
2192 assert_eq!(
2193 replaced(&customer, &shapes, b"for C00042 and C00077 then C00042"),
2194 [b"CUSTOMER_1".to_vec(), b"CUSTOMER_2".to_vec(), b"CUSTOMER_1".to_vec()]
2195 );
2196 let iban = parse("\\{iban}").expect("parses");
2197 assert_eq!(
2198 replaced(&iban, &crate::ShapeSet::new(), b"pay DE89370400440532013000 now"),
2199 [b"IBAN_1".to_vec()]
2200 );
2201 }
2202
2203 #[test]
2204 fn renames_balanced_tag_and_uppercases_body() {
2205 assert_eq!(
2208 rw("<\\W:t>(.*):body</=t>", "<${t}>${body:upper}</${t}>", "<div>hi there</div>"),
2209 "<div>HI THERE</div>"
2210 );
2211 }
2212
2213 #[test]
2214 fn reorders_captures() {
2215 assert_eq!(rw("\\W:a \\N:b", "${b}=${a}", "width 50"), "50=width");
2216 }
2217
2218 #[test]
2219 fn whole_match_reference() {
2220 assert_eq!(rw("\\N", "[${0}]", "a 12 b 34"), "a [12] b [34]");
2221 }
2222
2223 #[test]
2224 fn literal_dollar_and_gaps_preserved() {
2225 assert_eq!(rw("\\N:n", "$$${n}", "cost 5 dollars"), "cost $5 dollars");
2226 }
2227
2228 #[test]
2229 fn unbound_capture_is_a_template_error() {
2230 let pat = parse("\\W:a").unwrap();
2231 let e = Template::parse("${b}", &pat.capture_names()).unwrap_err();
2232 assert!(e.msg.contains("binds no such capture"));
2233 }
2234
2235 #[test]
2236 fn unknown_accessor_is_an_error() {
2237 let pat = parse("\\W:a").unwrap();
2238 let e = Template::parse("${a:shout}", &pat.capture_names()).unwrap_err();
2239 assert!(e.msg.contains("unknown accessor"));
2240 }
2241
2242 #[test]
2243 fn ipv4_octet_slice() {
2244 assert_eq!(rw("\\I:ip", "${ip:octet1-2}.0.0/16", "from 192.168.5.9"), "from 192.168.0.0/16");
2246 assert_eq!(rw("\\I:ip", "${ip:octet4}", "from 192.168.5.9"), "from 9");
2247 }
2248
2249 #[test]
2250 fn ipv6_group_slice() {
2251 assert_eq!(
2253 rw("\\I:ip", "${ip:group1-3}", "addr 2001:db8:85a3:0:0:8a2e:370:7334"),
2254 "addr 2001:db8:85a3"
2255 );
2256 }
2257
2258 #[test]
2259 fn url_email_version_fields() {
2260 assert_eq!(rw("\\U:u", "${u:host}", "get https://example.com:8080/a?q=1"), "get example.com");
2261 assert_eq!(rw("\\U:u", "${u:port}", "get https://example.com:8080/a"), "get 8080");
2262 assert_eq!(rw("\\E:e", "${e:user}@X", "to bob@x.com"), "to bob@X");
2263 assert_eq!(rw("\\V:v", "${v:major}", "v 1.2.3-rc1"), "v 1");
2264 }
2265
2266 #[test]
2267 fn accessor_pipeline_chains() {
2268 assert_eq!(rw("\\E:e", "${e:domain|upper}", "to bob@x.com"), "to X.COM");
2269 }
2270
2271 #[test]
2272 fn no_match_leaves_input_unchanged() {
2273 assert_eq!(rw("\\N", "X", "no digits here"), "no digits here");
2274 }
2275
2276 #[test]
2277 fn parallel_splice_matches_serial_on_many_matches() {
2278 let mut input = String::new();
2281 for i in 0..5000 {
2282 input.push_str(&format!("row {i} val {} end\n", i * 3));
2283 }
2284 let pat = parse("\\W:k \\N:v").expect("pattern parses");
2285 let tpl = Template::parse("${k:upper}=${v}", &pat.capture_names()).expect("template");
2286 let matches = captures(&pat, input.as_bytes(), &scan(&pat, input.as_bytes()));
2287 let serial = splice(input.as_bytes(), &matches, &tpl);
2288 let parallel = splice_parallel(input.as_bytes(), &matches, &tpl);
2289 assert_eq!(serial, parallel);
2290 assert_eq!(rewrite(&pat, &tpl, input.as_bytes()), serial);
2291 assert!(
2292 serial.starts_with(b"ROW=0 VAL=0 end\nROW=1 VAL=3 end\n"),
2293 "{}",
2294 String::from_utf8_lossy(&serial[..32])
2295 );
2296 }
2297}