1use std::collections::{HashMap, VecDeque};
38
39pub const N_CLASSES: usize = 5;
41pub const N_DECAYS: usize = 5;
43pub const N_BANDS: usize = N_CLASSES * N_DECAYS;
45
46pub const TAUS: [f32; N_DECAYS] = [2.0, 8.0, 32.0, 128.0, 512.0];
49
50#[must_use]
55pub fn classify(b: u8) -> usize {
56 if b.is_ascii_digit() {
57 0
58 } else if b == b'_' || b.is_ascii_alphabetic() {
59 1
60 } else if b.is_ascii_whitespace() {
61 2
62 } else if b.is_ascii_graphic() {
63 3
64 } else {
65 4
66 }
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub enum Texture {
72 Prose,
74 Code,
76 Math,
78 Data,
80 Mixed,
82}
83
84impl Texture {
85 #[must_use]
87 pub fn label(self) -> &'static str {
88 match self {
89 Texture::Prose => "prose",
90 Texture::Code => "code",
91 Texture::Math => "math",
92 Texture::Data => "data",
93 Texture::Mixed => "mixed",
94 }
95 }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq)]
100pub struct SpectralFrame {
101 pub bands: [f32; N_BANDS],
105 pub entropy: f32,
107 pub period: u16,
109 pub period_strength: f32,
111 pub novelty: f32,
113}
114
115impl SpectralFrame {
116 #[must_use]
119 pub fn texture_mix(&self) -> [f32; N_CLASSES] {
120 let base = 2 * N_CLASSES;
121 let mut out = [0.0f32; N_CLASSES];
122 out.copy_from_slice(&self.bands[base..base + N_CLASSES]);
123 out
124 }
125}
126
127#[derive(Clone, Copy, Debug)]
129pub struct SpectralConfig {
130 pub hop: usize,
133 pub entropy_window: usize,
135 pub period_window: usize,
137 pub max_lag: usize,
139 pub period_hop: usize,
142 pub ngram: usize,
144 pub novelty_window: usize,
146 pub cp_threshold: f32,
148 pub cp_floor: f32,
156 pub cp_min_gap: usize,
158}
159
160impl Default for SpectralConfig {
161 fn default() -> Self {
162 Self {
163 hop: 16,
164 entropy_window: 64,
165 period_window: 512,
166 max_lag: 128,
167 period_hop: 64,
168 ngram: 4,
169 novelty_window: 4096,
170 cp_threshold: 4.0,
171 cp_floor: 0.05,
172 cp_min_gap: 4,
173 }
174 }
175}
176
177#[derive(Clone, Debug, Default)]
181pub struct SpectralField {
182 pub len: usize,
184 pub hop: usize,
186 pub frames: Vec<SpectralFrame>,
189 pub boundaries: Vec<usize>,
191 pub needs: Needs,
196}
197
198impl Default for Needs {
199 fn default() -> Needs {
201 Needs::all()
202 }
203}
204
205impl SpectralField {
206 pub fn assert_carries(&self, want: Needs, who: &str) {
214 debug_assert!(
215 !(want.entropy && !self.needs.entropy)
216 && !(want.period && !self.needs.period)
217 && !(want.bands && !self.needs.bands)
218 && !(want.onset && !self.needs.onset)
219 && !(want.novelty && !self.needs.novelty),
220 "{who} reads {want:?} from a field built for {:?}",
221 self.needs
222 );
223 }
224
225 fn idx_of(&self, byte: usize) -> usize {
227 if self.frames.is_empty() {
228 return 0;
229 }
230 (byte / self.hop.max(1)).min(self.frames.len() - 1)
231 }
232
233 #[must_use]
235 pub fn frame_at(&self, byte: usize) -> SpectralFrame {
236 if self.frames.is_empty() {
237 return SpectralFrame::default();
238 }
239 self.frames[self.idx_of(byte)]
240 }
241
242 #[must_use]
246 pub fn signature(&self, start: usize, end: usize) -> SpectralFrame {
247 if self.frames.is_empty() || end <= start {
248 return SpectralFrame::default();
249 }
250 let lo = self.idx_of(start);
251 let hi = self.idx_of(end.saturating_sub(1));
252 let mut acc = SpectralFrame::default();
253 let mut n = 0.0f32;
254 let mut best_strength = -1.0f32;
255 for f in &self.frames[lo..=hi] {
256 for (a, b) in acc.bands.iter_mut().zip(f.bands.iter()) {
257 *a += *b;
258 }
259 acc.entropy += f.entropy;
260 acc.novelty += f.novelty;
261 if f.period_strength > best_strength {
262 best_strength = f.period_strength;
263 acc.period = f.period;
264 acc.period_strength = f.period_strength;
265 }
266 n += 1.0;
267 }
268 if n > 0.0 {
269 for a in &mut acc.bands {
270 *a /= n;
271 }
272 acc.entropy /= n;
273 acc.novelty /= n;
274 }
275 acc
276 }
277
278 #[must_use]
282 pub fn boundary_near(&self, byte: usize, tol: usize) -> bool {
283 let i = self.boundaries.partition_point(|&b| b.saturating_add(tol) < byte);
284 self.boundaries.get(i).is_some_and(|&b| b <= byte.saturating_add(tol))
285 }
286
287 #[must_use]
291 pub fn boundary_in(&self, start: usize, end: usize) -> bool {
292 let i = self.boundaries.partition_point(|&b| b < start);
293 self.boundaries.get(i).is_some_and(|&b| b < end)
294 }
295}
296
297#[must_use]
299pub fn texture_of(f: &SpectralFrame) -> Texture {
300 let m = f.texture_mix();
301 let (digit, alpha, _space, punct, high) = (m[0], m[1], m[2], m[3], m[4]);
302 if f.entropy > 0.85 || high > 0.30 {
303 return Texture::Data;
304 }
305 let total = digit + alpha + punct + high + m[2] + 1e-6;
306 let punct_frac = punct / total;
307 let alpha_frac = alpha / total;
308 let digit_frac = digit / total;
309 if alpha_frac > 0.55 && punct_frac < 0.12 {
310 Texture::Prose
311 } else if punct_frac > 0.20 && alpha_frac > 0.12 {
312 Texture::Code
313 } else if punct_frac > 0.12 && digit_frac > 0.10 {
314 Texture::Math
315 } else {
316 Texture::Mixed
317 }
318}
319
320#[derive(Clone, Copy, PartialEq, Eq, Debug)]
329pub enum CodeTexture {
330 Code,
332 Blob,
334 Prose,
337 Numeric,
339 Mixed,
341}
342
343impl CodeTexture {
344 #[must_use]
349 pub fn key_suffix(self) -> Option<char> {
350 match self {
351 CodeTexture::Code | CodeTexture::Mixed => None,
352 CodeTexture::Blob => Some('b'),
353 CodeTexture::Prose => Some('p'),
354 CodeTexture::Numeric => Some('n'),
355 }
356 }
357}
358
359#[must_use]
361pub fn code_texture(f: &SpectralFrame) -> CodeTexture {
362 let m = f.texture_mix();
363 let (digit, alpha, _space, punct, high) = (m[0], m[1], m[2], m[3], m[4]);
364 if f.entropy > 0.78 || high > 0.30 {
368 return CodeTexture::Blob;
369 }
370 let total = digit + alpha + punct + high + m[2] + 1e-6;
371 let punct_frac = punct / total;
372 let alpha_frac = alpha / total;
373 let digit_frac = digit / total;
374 if digit_frac > 0.45 && alpha_frac < 0.30 {
375 CodeTexture::Numeric
376 } else if alpha_frac > 0.60 && punct_frac < 0.08 {
377 CodeTexture::Prose
378 } else if punct_frac > 0.15 {
379 CodeTexture::Code
380 } else {
381 CodeTexture::Mixed
382 }
383}
384
385pub(crate) fn log_table() -> &'static [f64] {
393 const N: usize = 4096;
394 static LUT: std::sync::OnceLock<Vec<f64>> = std::sync::OnceLock::new();
395 LUT.get_or_init(|| {
396 (0..N as u32).map(|c| if c > 1 { f64::from(c) * f64::from(c).log2() } else { 0.0 }).collect()
397 })
398}
399
400pub(crate) const FIXED_LOG_SCALE: f64 = 4_294_967_296.0;
411
412pub(crate) fn fixed_log_table() -> &'static [i64] {
415 const N: usize = 4096;
416 static LUT: std::sync::OnceLock<Vec<i64>> = std::sync::OnceLock::new();
417 LUT.get_or_init(|| (0..N as u32).map(fixed_term_computed).collect())
418}
419
420pub(crate) fn fixed_log_delta_table() -> &'static [i64] {
424 const N: usize = 4096;
425 static LUT: std::sync::OnceLock<Vec<i64>> = std::sync::OnceLock::new();
426 LUT.get_or_init(|| (0..N as u32).map(|c| fixed_term_computed(c + 1) - fixed_term_computed(c)).collect())
427}
428
429#[inline]
441pub(crate) fn fixed_term(lut: &[i64], c: u32) -> i64 {
442 if let Some(&v) = lut.get(c as usize) {
443 return v;
444 }
445 fixed_term_computed(c)
446}
447
448fn fixed_term_computed(c: u32) -> i64 {
449 if c > 1 {
450 let cf = f64::from(c);
451 (cf * cf.log2() * FIXED_LOG_SCALE).round() as i64
452 } else {
453 0
454 }
455}
456
457#[inline]
458pub fn log_term(c: u32) -> f64 {
459 let lut = log_table();
460 if let Some(&v) = lut.get(c as usize) {
461 return v;
462 }
463 if c > 1 {
464 let cf = f64::from(c);
465 cf * cf.log2()
466 } else {
467 0.0
468 }
469}
470
471struct PeriodScanner {
499 win: usize,
500 max_lag: usize,
501 cross: Vec<i32>,
503 prefix: Vec<i64>,
505 prefix_sq: Vec<i64>,
506 at: Option<usize>,
508}
509
510impl PeriodScanner {
511 fn new(input: &[u8], win: usize, max_lag: usize) -> Self {
512 let mut prefix = Vec::with_capacity(input.len() + 1);
513 let mut prefix_sq = Vec::with_capacity(input.len() + 1);
514 let (mut s, mut q) = (0i64, 0i64);
515 prefix.push(0);
516 prefix_sq.push(0);
517 for &b in input {
518 let v = i64::from(b);
519 s += v;
520 q += v * v;
521 prefix.push(s);
522 prefix_sq.push(q);
523 }
524 let hi = max_lag.min(win / 2);
525 PeriodScanner {
526 win,
527 max_lag,
528 cross: vec![0; hi.saturating_sub(1)],
529 prefix,
530 prefix_sq,
531 at: None,
532 }
533 }
534
535 fn hi(&self) -> usize {
537 self.max_lag.min(self.win / 2)
538 }
539
540 fn seed(&mut self, input: &[u8], end: usize) {
542 let start = end - self.win;
543 let hi = self.hi();
544 self.cross.fill(0);
545 for t in start..end.saturating_sub(2) {
548 let x = i32::from(input[t]);
549 let last = hi.min(end - 1 - t);
550 let partners = &input[t + 2..=t + last];
551 for (c, &y) in self.cross.iter_mut().zip(partners) {
552 *c += x * i32::from(y);
553 }
554 }
555 self.at = Some(end);
556 }
557
558 fn advance(&mut self, input: &[u8], end: usize) {
560 let Some(prev) = self.at else {
561 self.seed(input, end);
562 return;
563 };
564 let step = end - prev;
565 if step >= self.win {
569 self.seed(input, end);
570 return;
571 }
572 let old_start = prev - self.win;
573 let new_start = end - self.win;
574 let hi = self.hi();
575 for t in old_start..new_start {
578 let x = i32::from(input[t]);
579 let partners = &input[t + 2..=t + hi];
580 for (c, &y) in self.cross.iter_mut().zip(partners) {
581 *c -= x * i32::from(y);
582 }
583 }
584 for u in prev..end {
588 let x = i32::from(input[u]);
589 let partners = &input[u - hi..=u - 2];
590 for (c, &y) in self.cross.iter_mut().zip(partners.iter().rev()) {
591 *c += x * i32::from(y);
592 }
593 }
594 self.at = Some(end);
595 }
596
597 fn eval(&mut self, input: &[u8], end: usize) -> (u16, f32) {
599 if self.win < 4 || end < self.win {
600 return (0, 0.0);
601 }
602 self.advance(input, end);
603 let start = end - self.win;
604 let n = self.win as f64;
605 let total = (self.prefix[end] - self.prefix[start]) as f64;
606 let m = total / n;
607 let sumsq = (self.prefix_sq[end] - self.prefix_sq[start]) as f64;
608 let denom = sumsq - n * m * m;
609 if denom <= f64::EPSILON {
610 return (0, 0.0);
611 }
612 let (mut best_lag, mut best) = (0usize, 0.0f64);
613 for lag in 2..=self.hi() {
614 let a = (self.prefix[end - lag] - self.prefix[start]) as f64;
617 let b = (self.prefix[end] - self.prefix[start + lag]) as f64;
618 let len = (self.win - lag) as f64;
619 let corr = f64::from(self.cross[lag - 2]) - m * (a + b) + len * m * m;
620 let r = corr / denom;
621 if r > best {
622 best = r;
623 best_lag = lag;
624 }
625 }
626 let best = best as f32;
627 if best < PERIOD_FLOOR { (0, best.max(0.0)) } else { (best_lag as u16, best) }
628 }
629}
630
631pub(crate) const PERIOD_FLOOR: f32 = 0.20;
633
634const LANES: usize = 16;
641
642pub fn dominant_period(win: &[u8], max_lag: usize) -> (u16, f32) {
645 let n = win.len();
646 if n < 4 {
647 return (0, 0.0);
648 }
649 let mean = win.iter().map(|&b| f32::from(b)).sum::<f32>() / n as f32;
650 let c: Vec<f32> = win.iter().map(|&b| f32::from(b) - mean).collect();
654 let denom: f32 = c.iter().map(|&x| x * x).sum();
655 if denom <= f32::EPSILON {
656 return (0, 0.0);
657 }
658 let hi = max_lag.min(n / 2);
659 let mut best_lag = 0usize;
660 let mut best = 0.0f32;
661 for lag in 2..=hi {
662 let a = &c[lag..];
668 let b = &c[..n - lag];
669 let (a_ch, a_rest) = a.as_chunks::<LANES>();
670 let (b_ch, b_rest) = b.as_chunks::<LANES>();
671 let mut acc = [0.0f32; LANES];
672 for (ca, cb) in a_ch.iter().zip(b_ch) {
673 for ((acck, &x), &y) in acc.iter_mut().zip(ca).zip(cb) {
674 *acck += x * y;
675 }
676 }
677 let mut sum: f32 = acc.iter().sum();
678 for (&x, &y) in a_rest.iter().zip(b_rest) {
679 sum += x * y;
680 }
681 let r = sum / denom;
682 if r > best {
683 best = r;
684 best_lag = lag;
685 }
686 }
687 if best < PERIOD_FLOOR {
688 (0, best.max(0.0))
689 } else {
690 (best_lag as u16, best)
691 }
692}
693
694#[derive(Clone, Copy, Debug, PartialEq, Eq)]
702pub struct Needs {
703 pub entropy: bool,
705 pub period: bool,
707 pub bands: bool,
710 pub onset: bool,
712 pub novelty: bool,
719}
720
721impl Needs {
722 #[must_use]
724 pub fn all() -> Needs {
725 Needs { entropy: true, period: true, bands: true, onset: true, novelty: true }
726 }
727
728 #[must_use]
730 pub fn none() -> Needs {
731 Needs { entropy: false, period: false, bands: false, onset: false, novelty: false }
732 }
733
734 #[must_use]
738 pub fn of(pred: &crate::ast::SpectralPred) -> Needs {
739 use crate::ast::SpectralPred as P;
740 let mut needs = Needs::none();
741 match pred {
742 P::EntropyGe(_) | P::EntropyLe(_) => needs.entropy = true,
743 P::PeriodEq(_) | P::PeriodAny => needs.period = true,
744 P::Texture(_) => {
745 needs.entropy = true;
746 needs.bands = true;
747 }
748 P::Onset => {
749 needs.entropy = true;
750 needs.bands = true;
751 needs.onset = true;
752 }
753 }
754 needs
755 }
756
757 #[must_use]
759 pub fn and(self, other: Needs) -> Needs {
760 Needs {
761 entropy: self.entropy || other.entropy,
762 period: self.period || other.period,
763 bands: self.bands || other.bands,
764 onset: self.onset || other.onset,
765 novelty: self.novelty || other.novelty,
766 }
767 }
768
769 #[must_use]
771 pub fn any(self) -> bool {
772 self != Needs::none()
773 }
774}
775
776#[must_use]
778pub fn analyze(input: &[u8]) -> SpectralField {
779 analyze_with(input, &SpectralConfig::default())
780}
781
782#[must_use]
785pub fn analyze_with(input: &[u8], cfg: &SpectralConfig) -> SpectralField {
786 analyze_needing(input, cfg, Needs::all())
787}
788
789#[must_use]
796pub fn analyze_needing(input: &[u8], cfg: &SpectralConfig, needs: Needs) -> SpectralField {
797 let n = input.len();
798 let hop = cfg.hop.max(1);
799 if n == 0 {
800 return SpectralField { len: 0, hop, frames: Vec::new(), boundaries: Vec::new(), needs };
801 }
802
803 let mut a = [0.0f32; N_DECAYS];
806 let mut oma = [0.0f32; N_DECAYS];
807 for ((ad, od), &tau) in a.iter_mut().zip(oma.iter_mut()).zip(TAUS.iter()) {
808 *ad = (-1.0 / tau).exp();
809 *od = 1.0 - *ad;
810 }
811 let mut bands = [0.0f32; N_BANDS];
812 let mut decay = [0.0f32; N_BANDS];
816 for (d, &ad) in a.iter().enumerate() {
817 decay[d * N_CLASSES..(d + 1) * N_CLASSES].fill(ad);
818 }
819
820 let w = cfg.entropy_window.max(1);
823 let mut hist = [0u32; 256];
824 let mut s = 0i64;
826 let entropy_norm = (w.min(256) as f32).max(2.0).log2();
827
828 let k = cfg.ngram.max(1);
832 let nov_win = cfg.novelty_window.max(1);
833 let mut ngram_counts: HashMap<u64, u32, std::hash::BuildHasherDefault<crate::tokutil::IdHash>> =
834 HashMap::default();
835 let mut ngram_ring: VecDeque<u64> = VecDeque::with_capacity(nov_win + 1);
836 let mut cur_novelty = 0.0f32;
837
838 let pwin = cfg.period_window.max(8);
840 let max_lag = cfg.max_lag.clamp(2, pwin / 2);
841 let per_eval = pwin.saturating_mul(max_lag).max(1);
842 const PERIOD_OP_BUDGET: usize = 1_000_000_000;
843 let max_evals = (PERIOD_OP_BUDGET / per_eval).max(1);
844 let period_hop = (n / max_evals).max(cfg.period_hop).max(1);
845 let mut period_evals = 0u64;
846 let mut cur_period = 0u16;
847 let mut cur_strength = 0.0f32;
848 let mut period_scanner = PeriodScanner::new(input, pwin, max_lag);
849 let ent_lut = fixed_log_table();
852 let ent_full_log2 = (w as f64).log2();
853 let ent_full_recip = 1.0 / (w as f64 * FIXED_LOG_SCALE);
854
855 const CP_ALPHA: f32 = 0.02;
860 const CP_WARMUP: usize = 32;
861 const ENT_SLOW_A: f32 = 0.992;
862 const ENT_WEIGHT: f32 = 2.0;
863 let mut ent_slow = 0.0f32;
864 let mut pow_f = 1.0f32;
865 let mut pow_s = 1.0f32;
866 let mut pow_e = 1.0f32;
867 let mut settled = false;
870 let mut cp_mean = 0.0f32;
871 let mut cp_mad = 0.0f32;
872 let mut cp_armed = true;
873 let mut last_boundary: isize = -(cfg.cp_min_gap as isize);
874 let mut boundaries: Vec<usize> = Vec::new();
875
876 let mut frames: Vec<SpectralFrame> = Vec::with_capacity(n / hop + 1);
877
878 for i in 0..n {
879 let b = input[i];
880 let c = classify(b);
881
882 if needs.bands {
884 for (v, &dk) in bands.iter_mut().zip(decay.iter()) {
885 *v *= dk;
886 }
887 for (d, &od) in oma.iter().enumerate() {
888 bands[d * N_CLASSES + c] += od;
889 }
890 }
891
892 let cur_entropy = if needs.entropy {
897 let nb = hist[b as usize];
898 hist[b as usize] = nb + 1;
899 let mut delta = fixed_term(ent_lut, nb + 1) - fixed_term(ent_lut, nb);
900 if i >= w {
901 let ob = input[i - w] as usize;
902 let oc = hist[ob];
903 hist[ob] = oc - 1;
904 delta -= fixed_term(ent_lut, oc) - fixed_term(ent_lut, oc - 1);
905 }
906 s += delta;
907 let (log_nwin, recip) = if i + 1 >= w {
908 (ent_full_log2, ent_full_recip)
909 } else {
910 let nwin = f64::from((i + 1) as u32);
911 (nwin.log2(), 1.0 / (nwin * FIXED_LOG_SCALE))
912 };
913 let hbits = (log_nwin - s as f64 * recip) as f32;
914 (hbits / entropy_norm).clamp(0.0, 1.0)
915 } else {
916 0.0
917 };
918
919 if needs.novelty && i + 1 >= k {
921 let mut h = 1_469_598_103_934_665_603u64;
922 for &x in &input[i + 1 - k..=i] {
923 h ^= u64::from(x);
924 h = h.wrapping_mul(1_099_511_628_211);
925 }
926 let count = ngram_counts.entry(h).or_insert(0);
928 cur_novelty = 1.0 / (1.0 + *count as f32);
929 *count += 1;
930 ngram_ring.push_back(h);
931 if ngram_ring.len() > nov_win
932 && let Some(old) = ngram_ring.pop_front()
933 && let Some(cc) = ngram_counts.get_mut(&old)
934 {
935 *cc -= 1;
936 if *cc == 0 {
937 ngram_counts.remove(&old);
938 }
939 }
940 }
941
942 if needs.period && i + 1 >= pwin && i % period_hop == 0 {
946 period_evals += 1;
953 let (p, st) = period_scanner.eval(input, i + 1);
954 cur_period = p;
955 cur_strength = st;
956 }
957
958 if needs.onset {
967 let (corr_f, corr_s, corr_e);
975 if settled {
976 corr_f = 1.0;
977 corr_s = 1.0;
978 corr_e = 1.0;
979 } else {
980 pow_f *= a[1];
981 pow_s *= a[3];
982 pow_e *= ENT_SLOW_A;
983 corr_f = 1.0 / (1.0 - pow_f).max(1e-4);
987 corr_s = 1.0 / (1.0 - pow_s).max(1e-4);
988 corr_e = (1.0 - pow_e).max(1e-4);
989 settled = corr_f == 1.0 && corr_s == 1.0 && corr_e == 1.0;
990 }
991 ent_slow += (1.0 - ENT_SLOW_A) * (cur_entropy - ent_slow);
992 let ent_slow_c = if corr_e == 1.0 { ent_slow } else { ent_slow / corr_e };
993 let fast_mix = &bands[N_CLASSES..2 * N_CLASSES];
994 let slow_mix = &bands[3 * N_CLASSES..4 * N_CLASSES];
995 let pairs = fast_mix.iter().zip(slow_mix.iter());
996 let class_div: f32 = if settled {
997 pairs.map(|(x, y)| (x - y).powi(2)).sum()
998 } else {
999 pairs.map(|(x, y)| (x * corr_f - y * corr_s).powi(2)).sum()
1000 };
1001 let dist = class_div.sqrt() + ENT_WEIGHT * (cur_entropy - ent_slow_c).abs();
1002 if i == 0 {
1003 cp_mean = dist;
1004 cp_mad = dist * 0.5 + 0.01;
1005 } else {
1006 let dev = (dist - cp_mean).abs();
1007 cp_mean += CP_ALPHA * (dist - cp_mean);
1008 cp_mad += CP_ALPHA * (dev - cp_mad);
1009 }
1010 let spread = cfg.cp_threshold * cp_mad;
1018 let thresh = cp_mean
1019 + if cfg.cp_floor > 0.0 { spread.max(cfg.cp_floor * cp_mean) } else { spread };
1020 if i >= CP_WARMUP {
1021 if cp_armed && dist > thresh && i as isize - last_boundary >= cfg.cp_min_gap as isize
1022 {
1023 boundaries.push(i);
1024 last_boundary = i as isize;
1025 cp_armed = false;
1026 } else if !cp_armed && dist < thresh * 0.5 {
1027 cp_armed = true;
1028 }
1029 }
1030 }
1031
1032 if (i + 1) % hop == 0 || i + 1 == n {
1034 frames.push(SpectralFrame {
1035 bands,
1036 entropy: cur_entropy,
1037 period: cur_period,
1038 period_strength: cur_strength,
1039 novelty: cur_novelty,
1040 });
1041 }
1042 }
1043
1044 crate::trace::counted("the spectral field: period evaluations", period_evals);
1048 SpectralField { len: n, hop, frames, boundaries, needs }
1049}
1050
1051pub const BLOB_ENTROPY_PCT: u8 = 85;
1054pub const BLOB_MIN_LEN: usize = 48;
1056
1057#[must_use]
1072pub fn high_entropy_runs(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1073 let n = input.len();
1074 if n < min_len {
1075 return Vec::new();
1076 }
1077 runs_over_spans(input, &long_spans(input, 0, n, min_len), threshold_pct, min_len)
1078}
1079
1080#[must_use]
1085pub fn high_entropy_runs_in_span(
1086 input: &[u8],
1087 a: usize,
1088 b: usize,
1089 threshold_pct: u8,
1090 min_len: usize,
1091) -> Vec<(usize, usize)> {
1092 if b - a < min_len {
1093 return Vec::new();
1094 }
1095 runs_over_spans(input, &[(a, b)], threshold_pct, min_len)
1096}
1097
1098#[must_use]
1104pub fn high_entropy_runs_within(
1105 input: &[u8],
1106 from: usize,
1107 to: usize,
1108 threshold_pct: u8,
1109 min_len: usize,
1110) -> Vec<(usize, usize)> {
1111 if to - from < min_len {
1112 return Vec::new();
1113 }
1114 runs_over_spans(input, &long_spans(input, from, to, min_len), threshold_pct, min_len)
1115}
1116
1117fn runs_over_spans(
1123 input: &[u8],
1124 spans: &[(usize, usize)],
1125 threshold_pct: u8,
1126 min_len: usize,
1127) -> Vec<(usize, usize)> {
1128 let mut raw: Vec<(usize, usize)> = Vec::new();
1129 for &(a, b) in spans {
1130 if holds_a_run(input, a, b, threshold_pct, min_len) {
1138 raw.push(whitespace_free_span(input, a, b));
1139 }
1140 }
1141 merge_spans(raw)
1142}
1143
1144fn holds_a_run(input: &[u8], from: usize, to: usize, threshold_pct: u8, min_len: usize) -> bool {
1148 let mut qualifies = false;
1149 let mut run_start: Option<usize> = None;
1150 entropy_flags(input, from, to, threshold_pct, |i, hi| match (hi, run_start) {
1151 (true, None) => run_start = Some(i),
1152 (false, Some(rs)) => {
1153 qualifies |= i - rs >= min_len;
1154 run_start = None;
1155 }
1156 _ => {}
1157 });
1158 if let Some(rs) = run_start {
1159 qualifies |= to - rs >= min_len;
1160 }
1161 qualifies
1162}
1163
1164fn runs_over_spans_across(
1172 input: &[u8],
1173 spans: &[(usize, usize)],
1174 threshold_pct: u8,
1175 min_len: usize,
1176 least_a_leaf: usize,
1177) -> Vec<(usize, usize)> {
1178 use flynnel::JobPlan;
1179 use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
1180
1181 let span_bytes: usize = spans.iter().map(|&(a, b)| b - a).sum();
1182 let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1183 let leaf_bytes = span_bytes.div_ceil(cores * 4).max(least_a_leaf).max(1);
1184 let mut pieces: Vec<(usize, usize, usize)> = Vec::new();
1187 for (k, &(a, b)) in spans.iter().enumerate() {
1188 let mut p = a;
1189 while p < b {
1190 let q = (p + leaf_bytes).min(b);
1191 pieces.push((k, p, q));
1192 p = q;
1193 }
1194 }
1195 let mut leaves: Vec<(usize, usize)> = Vec::new();
1198 let mut held = 0usize;
1199 let mut first = 0usize;
1200 for (i, &(_, p, q)) in pieces.iter().enumerate() {
1201 held += q - p;
1202 if held >= leaf_bytes {
1203 leaves.push((first, i + 1));
1204 first = i + 1;
1205 held = 0;
1206 }
1207 }
1208 if first < pieces.len() {
1209 leaves.push((first, pieces.len()));
1210 }
1211 if leaves.len() <= 1 {
1212 return runs_over_spans(input, spans, threshold_pct, min_len);
1213 }
1214 let mut found: Vec<Vec<usize>> = vec![Vec::new(); leaves.len()];
1215 let per_leaf_ns = (leaf_bytes as u64 * 4320 / 1000).min(u64::from(u32::MAX)) as u32;
1219 let plan = JobPlan::new(0, leaves.len() as u32)
1220 .with_leaf_shape(flynnel::LeafShape::Streaming)
1221 .with_estimated_per_item_ns(per_leaf_ns);
1222 for_each_chunk_indexed_min_leaf(&plan, &mut found, 1, |start, slots| {
1223 for (i, slot) in slots.iter_mut().enumerate() {
1224 let (lo, hi) = leaves[start + i];
1225 for &(k, p, q) in &pieces[lo..hi] {
1226 let reach = (q + min_len).min(spans[k].1);
1227 if slot.last() != Some(&k) && holds_a_run(input, p, reach, threshold_pct, min_len) {
1228 slot.push(k);
1229 }
1230 }
1231 }
1232 });
1233 let mut qualifies = vec![false; spans.len()];
1234 for k in found.into_iter().flatten() {
1235 qualifies[k] = true;
1236 }
1237 let raw = spans
1238 .iter()
1239 .zip(qualifies)
1240 .filter(|&(_, q)| q)
1241 .map(|(&(a, b), _)| whitespace_free_span(input, a, b))
1242 .collect();
1243 merge_spans(raw)
1244}
1245
1246const ENTROPY_FLAGS_MIN_LEAF: usize = 64 * 1024;
1250
1251const ENTROPY_RUNS_MIN_LEAF: usize = 8 * 1024;
1259
1260#[must_use]
1267pub fn high_entropy_runs_parallel(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1268 let n = input.len();
1269 if n < min_len {
1270 return Vec::new();
1271 }
1272 let spanning = crate::trace::phase("the blob table: the long spans");
1275 let spans = long_spans_across(input, min_len, ENTROPY_FLAGS_MIN_LEAF);
1276 let span_bytes: usize = spans.iter().map(|&(a, b)| b - a).sum();
1277 drop(spanning);
1278 crate::trace::counted("the blob table: bytes in the long spans", span_bytes as u64);
1279 let _running = crate::trace::phase("the blob table: the runs over the spans");
1280 runs_over_spans_across(input, &spans, threshold_pct, min_len, ENTROPY_RUNS_MIN_LEAF)
1281}
1282
1283#[allow(unsafe_code)]
1313fn entropy_flags(
1314 input: &[u8],
1315 from: usize,
1316 to: usize,
1317 threshold_pct: u8,
1318 emit: impl FnMut(usize, bool),
1319) {
1320 #[cfg(target_arch = "x86_64")]
1321 {
1322 match crate::isa::tier() {
1326 crate::isa::Tier::Avx512 => {
1327 return unsafe { entropy_flags_avx512(input, from, to, threshold_pct, emit) };
1328 }
1329 crate::isa::Tier::Avx2 => {
1330 return unsafe { entropy_flags_avx2(input, from, to, threshold_pct, emit) };
1331 }
1332 crate::isa::Tier::Sse2 => {
1333 return unsafe { entropy_flags_sse2(input, from, to, threshold_pct, emit) };
1334 }
1335 crate::isa::Tier::Scalar => {}
1336 }
1337 }
1338 entropy_flags_impl(input, from, to, threshold_pct, emit);
1339}
1340
1341#[cfg(target_arch = "x86_64")]
1342#[allow(unsafe_code)]
1343#[target_feature(enable = "avx512f", enable = "avx512bw")]
1344unsafe fn entropy_flags_avx512(
1345 input: &[u8],
1346 from: usize,
1347 to: usize,
1348 threshold_pct: u8,
1349 emit: impl FnMut(usize, bool),
1350) {
1351 entropy_flags_impl(input, from, to, threshold_pct, emit);
1352}
1353
1354#[cfg(target_arch = "x86_64")]
1355#[allow(unsafe_code)]
1356#[target_feature(enable = "avx2", enable = "bmi2")]
1357unsafe fn entropy_flags_avx2(
1358 input: &[u8],
1359 from: usize,
1360 to: usize,
1361 threshold_pct: u8,
1362 emit: impl FnMut(usize, bool),
1363) {
1364 entropy_flags_impl(input, from, to, threshold_pct, emit);
1365}
1366
1367#[cfg(target_arch = "x86_64")]
1368#[allow(unsafe_code)]
1369#[target_feature(enable = "sse2")]
1370unsafe fn entropy_flags_sse2(
1371 input: &[u8],
1372 from: usize,
1373 to: usize,
1374 threshold_pct: u8,
1375 emit: impl FnMut(usize, bool),
1376) {
1377 entropy_flags_impl(input, from, to, threshold_pct, emit);
1378}
1379
1380#[inline(always)]
1383fn entropy_flags_impl(input: &[u8], from: usize, to: usize, threshold_pct: u8, mut emit: impl FnMut(usize, bool)) {
1384 let w = 64usize;
1385 let mut hist = [0u32; 256];
1386 let mut s = 0i64;
1388 let entropy_norm = (w.min(256) as f32).max(2.0).log2();
1389 let thr = f32::from(threshold_pct) / 100.0;
1390 let full_log2 = (w as f64).log2();
1396 let full_recip = 1.0 / (w as f64 * FIXED_LOG_SCALE);
1397 let full_high = |s: i64| (full_log2 - s as f64 * full_recip) as f32 / entropy_norm >= thr;
1405 let s_high_max: i64 = {
1406 let (mut lo, mut hi) = (-1i64, (w as i64) * 6 * FIXED_LOG_SCALE as i64 + 1);
1410 while hi - lo > 1 {
1411 let mid = lo + (hi - lo) / 2;
1412 if full_high(mid) { lo = mid } else { hi = mid }
1413 }
1414 lo
1415 };
1416 let lut = fixed_log_table();
1419 let dlut = fixed_log_delta_table();
1420 for &b in &input[from.saturating_sub(w)..from] {
1421 let nb = hist[b as usize];
1422 hist[b as usize] = nb + 1;
1423 s += fixed_term(lut, nb + 1) - fixed_term(lut, nb);
1424 }
1425 for i in from..to {
1426 let b = input[i] as usize;
1427 let nb = hist[b];
1428 hist[b] = nb + 1;
1429 let mut delta = dlut[nb as usize];
1431 if i >= w {
1432 let ob = input[i - w] as usize;
1433 let oc = hist[ob];
1434 hist[ob] = oc - 1;
1435 delta -= dlut[(oc - 1) as usize];
1436 }
1437 s += delta;
1438 let high = if i + 1 >= w {
1439 s <= s_high_max
1440 } else {
1441 let nwin = f64::from((i + 1) as u32);
1442 (nwin.log2() - s as f64 * (1.0 / (nwin * FIXED_LOG_SCALE))) as f32 / entropy_norm >= thr
1443 };
1444 emit(i, high && !input[i].is_ascii_whitespace());
1450 }
1451}
1452
1453fn whitespace_free_span(input: &[u8], rs: usize, re: usize) -> (usize, usize) {
1457 let mut a = rs;
1458 while a > 0 && !input[a - 1].is_ascii_whitespace() {
1459 a -= 1;
1460 }
1461 let mut b = re;
1462 while b < input.len() && !input[b].is_ascii_whitespace() {
1463 b += 1;
1464 }
1465 (a, b)
1466}
1467
1468fn long_spans(input: &[u8], from: usize, to: usize, min_len: usize) -> Vec<(usize, usize)> {
1475 let lo = from.saturating_sub(min_len);
1476 let hi = (to + min_len).min(input.len());
1477 crate::byte_simd::nonspace_spans_at_least(&input[lo..hi], min_len)
1478 .into_iter()
1479 .filter_map(|(a, b)| {
1480 let (a, b) = ((lo + a).max(from), (lo + b).min(to));
1481 (a < b).then_some((a, b))
1482 })
1483 .collect()
1484}
1485
1486fn long_spans_across(input: &[u8], min_len: usize, least_a_leaf: usize) -> Vec<(usize, usize)> {
1496 use flynnel::JobPlan;
1497 use flynnel::sched::par_iter::for_each_chunk_indexed_min_leaf;
1498
1499 let n = input.len();
1500 let cores = std::thread::available_parallelism().map_or(1, std::num::NonZero::get);
1501 let leaf_bytes = n.div_ceil(cores * 4).max(least_a_leaf).max(1);
1502 let leaves = n.div_ceil(leaf_bytes);
1503 if leaves <= 1 {
1504 return long_spans(input, 0, n, min_len);
1505 }
1506 let mut found: Vec<Vec<(usize, usize)>> = vec![Vec::new(); leaves];
1507 let per_leaf_ns = (leaf_bytes as u64 * 146 / 1000).min(u64::from(u32::MAX)) as u32;
1511 let plan = JobPlan::new(0, leaves as u32)
1512 .with_leaf_shape(flynnel::LeafShape::Streaming)
1513 .with_estimated_per_item_ns(per_leaf_ns);
1514 for_each_chunk_indexed_min_leaf(&plan, &mut found, 1, |start, slots| {
1515 for (i, slot) in slots.iter_mut().enumerate() {
1516 let from = (start + i) * leaf_bytes;
1517 let to = (from + leaf_bytes).min(n);
1518 *slot = long_spans(input, from, to, min_len);
1519 }
1520 });
1521 merge_spans(found.into_iter().flatten().collect())
1522}
1523
1524fn merge_spans(raw: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
1526 let mut runs: Vec<(usize, usize)> = Vec::new();
1527 for (a, b) in raw {
1528 if let Some(last) = runs.last_mut()
1529 && a <= last.1
1530 {
1531 last.1 = last.1.max(b);
1532 } else {
1533 runs.push((a, b));
1534 }
1535 }
1536 runs
1537}
1538
1539#[must_use]
1546pub fn regions(field: &SpectralField) -> Vec<(usize, usize, Texture)> {
1547 if field.len == 0 {
1548 return Vec::new();
1549 }
1550 let mut cuts: Vec<usize> = Vec::with_capacity(field.boundaries.len() + 2);
1551 cuts.push(0);
1552 cuts.extend(field.boundaries.iter().copied());
1553 cuts.push(field.len);
1554 cuts.dedup();
1555 let mut out: Vec<(usize, usize, Texture)> = Vec::new();
1556 for w in cuts.windows(2) {
1557 let (s, e) = (w[0], w[1]);
1558 if e <= s {
1559 continue;
1560 }
1561 let tex = texture_of(&field.signature(s, e));
1562 if let Some(last) = out.last_mut()
1563 && last.2 == tex
1564 {
1565 last.1 = e;
1566 } else {
1567 out.push((s, e, tex));
1568 }
1569 }
1570 out
1571}
1572
1573#[must_use]
1583pub fn code_regions(input: &[u8]) -> Vec<(usize, usize, CodeTexture)> {
1584 let field = analyze(input);
1585 if field.len == 0 {
1586 return Vec::new();
1587 }
1588 let mut cuts: Vec<usize> = Vec::with_capacity(field.boundaries.len() + 4);
1589 cuts.push(0);
1590 cuts.extend(field.boundaries.iter().copied());
1591 for (s, e) in high_entropy_runs(input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN) {
1595 cuts.push(s);
1596 cuts.push(e);
1597 }
1598 cuts.push(field.len);
1599 cuts.sort_unstable();
1600 cuts.dedup();
1601 let mut out: Vec<(usize, usize, CodeTexture)> = Vec::new();
1602 for w in cuts.windows(2) {
1603 let (s, e) = (w[0], w[1]);
1604 if e <= s {
1605 continue;
1606 }
1607 let tex = code_texture(&analyze(&input[s..e]).signature(0, e - s));
1608 if let Some(last) = out.last_mut()
1609 && last.2 == tex
1610 {
1611 last.1 = e;
1612 } else {
1613 out.push((s, e, tex));
1614 }
1615 }
1616 out
1617}
1618
1619#[cfg(test)]
1620mod tests {
1621 use super::*;
1622
1623 #[test]
1624 fn the_sliding_scanner_agrees_with_the_one_shot_reading() {
1625 let csv: Vec<u8> = (0..300)
1630 .flat_map(|i| format!("{:03},{:03},{:03}\n", i % 1000, (i * 7) % 1000, (i * 13) % 1000).into_bytes())
1631 .collect();
1632 let prose: Vec<u8> = "it was the best of times it was the worst of times it was the age of wisdom "
1633 .bytes().cycle().take(9000).collect();
1634 let mut x = 0x1234_5678u32;
1635 let noise: Vec<u8> = (0..9000)
1636 .map(|_| {
1637 x ^= x << 13;
1638 x ^= x >> 17;
1639 x ^= x << 5;
1640 (x & 0xff) as u8
1641 })
1642 .collect();
1643
1644 for (tag, data) in [("csv", csv), ("prose", prose), ("noise", noise)] {
1645 let (win, lag, hop) = (512usize, 128usize, 64usize);
1646 let mut scan = PeriodScanner::new(&data, win, lag);
1647 let mut end = win;
1648 let mut checked = 0u32;
1649 while end <= data.len() {
1650 let (want, want_st) = dominant_period(&data[end - win..end], lag);
1651 let (got, got_st) = scan.eval(&data, end);
1652 assert_eq!(got, want, "{tag} at {end}: lag disagrees");
1653 assert!(
1654 (got_st - want_st).abs() < 1e-3,
1655 "{tag} at {end}: strength {got_st} vs {want_st}"
1656 );
1657 checked += 1;
1658 end += hop;
1659 }
1660 assert!(checked > 10, "{tag}: the sweep must actually compare windows");
1661 }
1662 }
1663
1664 #[test]
1665 fn the_scanner_reseeds_rather_than_sliding_past_a_whole_window() {
1666 let data: Vec<u8> =
1669 (0..4000).map(|i: usize| b"abcd12"[i % 6]).collect();
1670 let win = 256usize;
1671 let mut scan = PeriodScanner::new(&data, win, 64);
1672 let a = scan.eval(&data, win);
1674 let far = scan.eval(&data, 3000);
1675 let fresh = dominant_period(&data[3000 - win..3000], 64);
1676 assert_eq!(far.0, fresh.0, "a reseeded window reads as a fresh one");
1677 assert_eq!(a.0, 6, "the six-byte cycle is found either way");
1678 }
1679
1680 #[test]
1681 fn csv_rows_have_a_dominant_period() {
1682 let mut input = Vec::new();
1684 for _ in 0..200 {
1685 input.extend_from_slice(b"a,b,c\n");
1686 }
1687 let field = analyze(&input);
1688 assert!(
1689 field.frames.iter().any(|f| f.period == 6 && f.period_strength > 0.3),
1690 "expected a frame with period 6; got periods {:?}",
1691 field.frames.iter().map(|f| f.period).collect::<Vec<_>>()
1692 );
1693 }
1694
1695 #[test]
1696 fn base64_blob_reads_high_entropy() {
1697 let alpha = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1698 let blob: Vec<u8> = (0..800).map(|i| alpha[(i * 7 + i * i * 13) % 64]).collect();
1699 let field = analyze(&blob);
1700 let max_e = field.frames.iter().map(|f| f.entropy).fold(0.0f32, f32::max);
1701 assert!(max_e > 0.7, "base64 blob should read high entropy; max was {max_e}");
1702 }
1703
1704 #[test]
1705 fn prose_and_code_classify_distinctly() {
1706 let prose = b"the quick brown fox jumps over the lazy dog and then the dog sleeps well into the warm afternoon while the fox wanders off across the wide green field again";
1707 let code = b"fn f(x){let y=x+1;return y*2;} fn g(a,b){if(a>b){a-=b;}else{b-=a;} return a;} struct P{x:i32,y:i32}";
1708 let pf = analyze(prose);
1709 let cf = analyze(code);
1710 let pt = texture_of(&pf.signature(0, prose.len()));
1711 let ct = texture_of(&cf.signature(0, code.len()));
1712 assert_eq!(pt, Texture::Prose, "prose misclassified as {}", pt.label());
1713 assert_eq!(ct, Texture::Code, "code misclassified as {}", ct.label());
1714 }
1715
1716 #[test]
1717 fn repeated_template_drops_in_novelty() {
1718 let mut input = Vec::new();
1719 for _ in 0..200 {
1720 input.extend_from_slice(b"INFO: request handled ok\n");
1721 }
1722 let field = analyze(&input);
1723 let n = field.frames.len();
1724 assert!(n >= 4, "need several frames");
1725 let first: f32 = field.frames[..n / 4].iter().map(|f| f.novelty).sum::<f32>()
1726 / (n / 4).max(1) as f32;
1727 let last: f32 = field.frames[3 * n / 4..].iter().map(|f| f.novelty).sum::<f32>()
1728 / (n - 3 * n / 4).max(1) as f32;
1729 assert!(last < first, "novelty should fall on repetition: first {first}, last {last}");
1730 }
1731
1732 #[test]
1733 fn regime_switch_records_a_change_point() {
1734 let mut input = Vec::new();
1737 input.extend(std::iter::repeat_n(b'a', 300));
1738 input.extend(std::iter::repeat_n(b'7', 300));
1739 input.extend(std::iter::repeat_n(b'#', 300));
1740 let field = analyze(&input);
1741 assert!(field.boundary_near(300, 24), "no change-point near 300: {:?}", field.boundaries);
1742 assert!(field.boundary_near(600, 24), "no change-point near 600: {:?}", field.boundaries);
1743 }
1744
1745 #[test]
1746 fn a_regime_switch_is_found_past_the_settling_of_the_bias_corrections() {
1747 let mut input = Vec::new();
1754 for b in *b"a7#a7" {
1755 input.extend(std::iter::repeat_n(b, 4000));
1756 }
1757 let field = analyze(&input);
1758 for at in [4000usize, 8000, 12000, 16000] {
1759 assert!(
1760 field.boundary_near(at, 64),
1761 "no change-point near {at}: {:?}",
1762 field.boundaries
1763 );
1764 }
1765 for &b in &field.boundaries {
1774 assert!(
1775 [4000usize, 8000, 12000, 16000].iter().any(|&s| b.abs_diff(s) <= 64),
1776 "a change-point at {b} is inside a regime: {:?}",
1777 field.boundaries
1778 );
1779 }
1780 }
1781
1782 #[test]
1783 fn boundary_lookups_answer_as_a_scan_over_every_change_point_does() {
1784 let boundaries = vec![0usize, 3, 4, 10, 27, 28, 64, 100];
1788 let field = SpectralField {
1789 len: 101,
1790 hop: 1,
1791 frames: Vec::new(),
1792 boundaries: boundaries.clone(),
1793 needs: Needs::all(),
1794 };
1795 for start in 0..=104 {
1796 for end in 0..=104 {
1797 let scan = boundaries.iter().any(|&b| b >= start && b < end);
1798 assert_eq!(field.boundary_in(start, end), scan, "boundary_in({start}, {end})");
1799 }
1800 }
1801 for byte in 0..=104 {
1802 for tol in 0..8 {
1803 let scan = boundaries.iter().any(|&b| b.abs_diff(byte) <= tol);
1804 assert_eq!(field.boundary_near(byte, tol), scan, "boundary_near({byte}, {tol})");
1805 }
1806 }
1807 let none = SpectralField {
1808 len: 0,
1809 hop: 1,
1810 frames: Vec::new(),
1811 boundaries: Vec::new(),
1812 needs: Needs::all(),
1813 };
1814 assert!(!none.boundary_in(0, 10));
1815 assert!(!none.boundary_near(5, 5));
1816 }
1817
1818 #[test]
1819 fn empty_input_is_safe() {
1820 let field = analyze(b"");
1821 assert_eq!(field.len, 0);
1822 assert!(field.frames.is_empty());
1823 assert_eq!(field.signature(0, 0), SpectralFrame::default());
1824 }
1825
1826 fn hi_entropy_blob(n: usize) -> Vec<u8> {
1829 const ALPHA: &[u8; 64] =
1830 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1831 let mut x = 0x2545_f491_4f6c_dd1du64;
1832 (0..n)
1833 .map(|_| {
1834 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
1835 ALPHA[((x >> 58) % 64) as usize]
1836 })
1837 .collect()
1838 }
1839
1840 #[test]
1841 fn blob_runs_never_hold_whitespace() {
1842 let mut input = Vec::new();
1851 for i in 0..40 {
1852 input.extend_from_slice(format!("line {i} the quick brown fox jumps over\n").as_bytes());
1853 input.extend(hi_entropy_blob(300));
1854 input.push(b'\n');
1855 }
1856 let runs = high_entropy_runs(&input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN);
1857 assert!(!runs.is_empty(), "blobs should be detected at all");
1858 for &(a, b) in &runs {
1859 assert!(
1860 !input[a..b].iter().any(u8::is_ascii_whitespace),
1861 "run [{a}..{b}] holds whitespace: {:?}",
1862 String::from_utf8_lossy(&input[a..b])
1863 );
1864 }
1865 let toks = crate::lexer::lex(&input);
1867 let words = toks
1868 .iter()
1869 .filter(|t| t.kind == crate::token::TokenKind::Word)
1870 .filter(|t| &input[t.span()] == b"quick")
1871 .count();
1872 assert_eq!(words, 40, "every line's prose survives the blob gate");
1873 }
1874
1875 #[test]
1876 fn high_entropy_runs_find_a_base64_blob_not_prose() {
1877 let mut input = Vec::new();
1878 input.extend_from_slice(b"the quick brown fox jumps over the lazy dog near the old bridge today ");
1879 let blob_start = input.len();
1880 input.extend(hi_entropy_blob(300));
1881 let runs = high_entropy_runs(&input, BLOB_ENTROPY_PCT, BLOB_MIN_LEN);
1882 assert!(!runs.is_empty(), "a 300-byte base64 blob should be detected");
1883 let (a, b) = runs[0];
1884 assert!(b - a >= BLOB_MIN_LEN, "blob run too short: {:?}", runs[0]);
1885 assert!(a >= blob_start.saturating_sub(2), "blob should start at/after the prose: {a} vs {blob_start}");
1886 }
1887
1888 fn runs_from_every_flag(input: &[u8], threshold_pct: u8, min_len: usize) -> Vec<(usize, usize)> {
1891 let n = input.len();
1892 if n < min_len {
1893 return Vec::new();
1894 }
1895 let mut raw: Vec<(usize, usize)> = Vec::new();
1896 let mut run_start: Option<usize> = None;
1897 entropy_flags(input, 0, n, threshold_pct, |i, hi| match (hi, run_start) {
1898 (true, None) => run_start = Some(i),
1899 (false, Some(rs)) => {
1900 if i - rs >= min_len {
1901 raw.push(whitespace_free_span(input, rs, i));
1902 }
1903 run_start = None;
1904 }
1905 _ => {}
1906 });
1907 if let Some(rs) = run_start
1908 && n - rs >= min_len
1909 {
1910 raw.push(whitespace_free_span(input, rs, n));
1911 }
1912 merge_spans(raw)
1913 }
1914
1915 #[test]
1919 fn long_spans_across_leaves_are_the_one_pass_spans() {
1920 let text: &[u8] = include_bytes!("spectral.rs");
1923 let n = text.len();
1924 for min_len in [1usize, 3, 8, BLOB_MIN_LEN, 200] {
1925 let want = long_spans(text, 0, n, min_len);
1926 assert!(!want.is_empty() || min_len == 200, "min_len {min_len}: the file holds spans that long");
1927 for least in [1usize, 5, 64, 1000, 4096, ENTROPY_FLAGS_MIN_LEAF] {
1928 assert_eq!(long_spans_across(text, min_len, least), want, "min_len {min_len}, {least} bytes a leaf");
1929 }
1930 }
1931 }
1932
1933 #[test]
1938 fn runs_over_spans_across_leaves_are_the_whole_span_runs() {
1939 let text: &[u8] = include_bytes!("spectral.rs");
1940 let mut blobbed = Vec::new();
1941 for i in 0..12 {
1942 blobbed.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
1943 blobbed.extend(hi_entropy_blob(3000 + 700 * i));
1944 blobbed.push(b' ');
1945 }
1946 blobbed.extend(hi_entropy_blob(200_000));
1947 blobbed.push(b'\n');
1948 blobbed.extend_from_slice(text);
1949 for (name, input) in [("this file", text), ("blobs then this file", blobbed.as_slice())] {
1950 for min_len in [16usize, BLOB_MIN_LEN, 200] {
1951 let spans = long_spans(input, 0, input.len(), min_len);
1952 let want = runs_over_spans(input, &spans, BLOB_ENTROPY_PCT, min_len);
1953 assert!(
1954 !want.is_empty() || name == "this file",
1955 "{name}, min_len {min_len}: the blobs are runs to find"
1956 );
1957 for least in [1usize, 7, 100, 4096, ENTROPY_RUNS_MIN_LEAF] {
1958 assert_eq!(
1959 runs_over_spans_across(input, &spans, BLOB_ENTROPY_PCT, min_len, least),
1960 want,
1961 "{name}, min_len {min_len}, {least} bytes a leaf"
1962 );
1963 }
1964 }
1965 }
1966 }
1967
1968 #[test]
1969 fn long_spans_are_the_bytes_whose_span_reaches_min_len() {
1970 let mut input = Vec::new();
1974 for len in [1usize, 7, 47, 48, 49, 60, 100, 47, 48, 3] {
1975 input.extend(std::iter::repeat_n(b'x', len));
1976 input.push(if len % 2 == 0 { b' ' } else { b'\n' });
1977 }
1978 input.extend(std::iter::repeat_n(b'y', 130));
1979 let n = input.len();
1980 let in_long = |p: usize| {
1981 let mut a = p;
1982 while a > 0 && !input[a - 1].is_ascii_whitespace() {
1983 a -= 1;
1984 }
1985 let mut b = p;
1986 while b < n && !input[b].is_ascii_whitespace() {
1987 b += 1;
1988 }
1989 !input[p].is_ascii_whitespace() && b - a >= 48
1990 };
1991 for (from, to) in
1992 [(0, n), (0, 10), (5, 70), (60, 61), (100, 150), (120, n), (n - 20, n), (n - 1, n), (200, 200)]
1993 {
1994 let parts = long_spans(&input, from, to, 48);
1995 let mut covered = vec![false; n];
1996 for &(a, b) in &parts {
1997 assert!(from <= a && a < b && b <= to, "part [{a}, {b}) outside [{from}, {to})");
1998 for c in &mut covered[a..b] {
1999 *c = true;
2000 }
2001 }
2002 for (p, &c) in covered.iter().enumerate().take(to).skip(from) {
2003 assert_eq!(c, in_long(p), "byte {p} of [{from}, {to})");
2004 }
2005 }
2006 }
2007
2008 #[test]
2009 fn runs_over_long_spans_alone_equal_runs_over_every_byte() {
2010 let mut input = Vec::new();
2014 input.extend(hi_entropy_blob(300));
2015 for (i, len) in [40usize, 47, 48, 49, 60, 111, 112, 113, 200, 300].into_iter().enumerate() {
2016 input.extend_from_slice(b" the quick brown fox jumps over the lazy dog ");
2017 input.extend(hi_entropy_blob(len));
2018 if i % 3 == 1 {
2019 input.push(b' ');
2020 input.extend(hi_entropy_blob(len));
2021 }
2022 input.push(if i % 2 == 0 { b'\n' } else { b'\t' });
2023 }
2024 input.extend_from_slice(b"tail text");
2025 input.extend(hi_entropy_blob(300));
2026 for min_len in [16usize, 48, 64, 200] {
2027 assert_eq!(
2028 high_entropy_runs(&input, BLOB_ENTROPY_PCT, min_len),
2029 runs_from_every_flag(&input, BLOB_ENTROPY_PCT, min_len),
2030 "min_len {min_len}"
2031 );
2032 }
2033
2034 let mut unbroken = Vec::new();
2042 while unbroken.len() < 64 * 1024 {
2043 unbroken.extend(hi_entropy_blob(64));
2044 unbroken.extend_from_slice(b"plain.text.between.the.blobs");
2045 }
2046 assert!(
2047 !unbroken.iter().any(u8::is_ascii_whitespace),
2048 "the corpus must hold no whitespace, or it does not read this case"
2049 );
2050 for min_len in [16usize, 48, 200] {
2051 assert_eq!(
2052 high_entropy_runs(&unbroken, BLOB_ENTROPY_PCT, min_len),
2053 runs_from_every_flag(&unbroken, BLOB_ENTROPY_PCT, min_len),
2054 "no whitespace anywhere, min_len {min_len}"
2055 );
2056 }
2057 let mut big = Vec::new();
2058 while big.len() < 4 * ENTROPY_FLAGS_MIN_LEAF {
2059 big.extend_from_slice(&input);
2060 }
2061 assert_eq!(
2062 high_entropy_runs_parallel(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2063 runs_from_every_flag(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN)
2064 );
2065 }
2066
2067 #[cfg(target_arch = "x86_64")]
2068 #[test]
2069 #[allow(unsafe_code)]
2070 fn entropy_flags_ladder_agrees() {
2071 let mut input = Vec::new();
2077 for i in 0..60 {
2078 input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2079 if i % 5 == 2 {
2080 input.extend(hi_entropy_blob(48 + i));
2081 input.push(b' ');
2082 }
2083 }
2084 type FlagPass = dyn Fn(&[u8], usize, usize, u8, &mut dyn FnMut(usize, bool));
2087 let flags = |f: &FlagPass| {
2088 let mut v = Vec::new();
2089 f(&input, 0, input.len(), BLOB_ENTROPY_PCT, &mut |i, h| v.push((i, h)));
2090 v
2091 };
2092 let scalar = flags(&|inp, a, b, t, e| entropy_flags_impl(inp, a, b, t, e));
2093 assert!(!scalar.is_empty(), "the corpus must produce flags, or this asserts nothing");
2094 assert!(scalar.iter().any(|&(_, h)| h), "some byte must flag high, or this is vacuous");
2095
2096 let mut ran = vec!["scalar"];
2097 if std::is_x86_feature_detected!("sse2") {
2098 let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_sse2(inp, a, b, t, e) });
2100 assert_eq!(got, scalar, "sse2 rung disagrees with the scalar body");
2101 ran.push("sse2");
2102 }
2103 if std::is_x86_feature_detected!("avx2") && std::is_x86_feature_detected!("bmi2") {
2104 let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_avx2(inp, a, b, t, e) });
2105 assert_eq!(got, scalar, "avx2 rung disagrees with the scalar body");
2106 ran.push("avx2");
2107 }
2108 if std::is_x86_feature_detected!("avx512f") && std::is_x86_feature_detected!("avx512bw") {
2109 let got = flags(&|inp, a, b, t, e| unsafe { entropy_flags_avx512(inp, a, b, t, e) });
2110 assert_eq!(got, scalar, "avx512 rung disagrees with the scalar body");
2111 ran.push("avx512");
2112 }
2113 eprintln!("entropy_flags ladder rungs exercised on this host: {}", ran.join(", "));
2114 }
2115
2116 fn entropy_flags_by_expression(input: &[u8], threshold_pct: u8) -> Vec<(usize, bool)> {
2121 let w = 64usize;
2122 let mut hist = [0u32; 256];
2123 let mut s = 0i64;
2124 let entropy_norm = (w.min(256) as f32).max(2.0).log2();
2125 let thr = f32::from(threshold_pct) / 100.0;
2126 let lut = fixed_log_table();
2127 let mut out = Vec::with_capacity(input.len());
2128 for i in 0..input.len() {
2129 let b = input[i] as usize;
2130 let nb = hist[b];
2131 hist[b] = nb + 1;
2132 s += fixed_term(lut, nb + 1) - fixed_term(lut, nb);
2133 if i >= w {
2134 let ob = input[i - w] as usize;
2135 let oc = hist[ob];
2136 hist[ob] = oc - 1;
2137 s -= fixed_term(lut, oc) - fixed_term(lut, oc - 1);
2138 }
2139 let nwin = f64::from((i + 1).min(w) as u32);
2140 let h = (nwin.log2() - s as f64 * (1.0 / (nwin * FIXED_LOG_SCALE))) as f32 / entropy_norm;
2141 out.push((i, h >= thr && !input[i].is_ascii_whitespace()));
2142 }
2143 out
2144 }
2145
2146 #[test]
2147 fn the_full_window_boundary_flags_exactly_as_the_expression_does() {
2148 let mut input = Vec::new();
2152 for i in 0..80 {
2153 input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2154 match i % 4 {
2155 0 => input.extend(hi_entropy_blob(40 + i)),
2156 1 => input.extend(std::iter::repeat_n(b'x', i)),
2157 2 => input.extend((0..=255u8).skip(i % 7)),
2158 _ => input.extend(hi_entropy_blob(16).iter().chain(b"aaaa").copied()),
2159 }
2160 input.push(b' ');
2161 }
2162 for pct in [50u8, 70, BLOB_ENTROPY_PCT, 95, 100] {
2163 let want = entropy_flags_by_expression(&input, pct);
2164 let mut got = Vec::new();
2165 entropy_flags(&input, 0, input.len(), pct, |i, h| got.push((i, h)));
2166 assert_eq!(got, want, "threshold {pct}: the pass disagrees with the expression");
2167 let highs = want.iter().filter(|(_, h)| *h).count();
2168 assert!(pct == 100 || (highs > 0 && highs < want.len()), "threshold {pct} flags {highs} of {}, so the comparison asserts little", want.len());
2169 }
2170 }
2171
2172 #[test]
2173 fn entropy_flags_from_mid_stream_read_what_the_whole_stream_reads() {
2174 let mut input = Vec::new();
2179 for i in 0..40 {
2180 input.extend_from_slice(b"the quick brown fox jumps over the lazy dog ");
2181 if i % 7 == 3 {
2182 input.extend(hi_entropy_blob(60 + i));
2183 input.push(b' ');
2184 }
2185 }
2186 let n = input.len();
2187 let mut whole = vec![false; n];
2188 entropy_flags(&input, 0, n, BLOB_ENTROPY_PCT, |i, h| whole[i] = h);
2189 for from in [1, 17, 63, 64, 65, 200, 777, n / 2, n - 70, n - 1] {
2190 let mut part = vec![false; n];
2191 entropy_flags(&input, from, n, BLOB_ENTROPY_PCT, |i, h| part[i] = h);
2192 assert_eq!(&part[from..], &whole[from..], "flags from {from}");
2193 }
2194 let mut big = Vec::new();
2197 while big.len() < 4 * ENTROPY_FLAGS_MIN_LEAF {
2198 big.extend_from_slice(&input);
2199 }
2200 assert_eq!(
2201 high_entropy_runs_parallel(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2202 high_entropy_runs(&big, BLOB_ENTROPY_PCT, BLOB_MIN_LEN),
2203 );
2204 }
2205
2206 #[test]
2207 fn regions_surface_distinct_textures() {
2208 let mut doc = Vec::new();
2212 for _ in 0..3 {
2213 doc.extend_from_slice(b"the quick brown fox jumps over the lazy dog near the old stone bridge today ");
2214 }
2215 let mut x = 0x9e37_79b9_7f4a_7c15u64;
2216 for _ in 0..300 {
2217 x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
2218 doc.push(0x80 | ((x >> 56) as u8 & 0x7f)); }
2220 let field = analyze(&doc);
2221 let regs = regions(&field);
2222 let textures: std::collections::HashSet<Texture> = regs.iter().map(|r| r.2).collect();
2223 assert!(regs.len() >= 2, "expected multiple regions, got {regs:?}");
2224 assert!(textures.contains(&Texture::Data), "the blob region should read Data: {regs:?}");
2225 }
2226}