1use crate::content::{
16 AnalysisSet, AnalyzerId, AnalyzerVersion, ContentProvenance, OptionsFingerprint,
17};
18use crate::control::ControlLimits;
19use crate::engine_contract::ScanScope;
20use crate::query::IgnoredEntries;
21
22pub(crate) const IGNORE_RULES_VERSION: u64 = 4;
43
44#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
54pub struct EntryScope {
55 pub max_depth: Option<usize>,
57 pub follow_symlinks: bool,
59 pub one_filesystem: bool,
61 pub hidden_fingerprint: u64,
63 pub exclude_special: bool,
65 pub population: IgnoredEntries,
67 pub control_fingerprint: u64,
69}
70
71#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
79pub struct EntryTierIdentity {
80 pub engine: u64,
83 pub scope: EntryScope,
85 pub type_rules_fingerprint: u64,
87 pub reducers_fingerprint: u64,
89}
90
91impl EntryTierIdentity {
92 pub fn of_scope(scope: ScanScope) -> Self {
94 Self {
95 engine: crate::snapshot::engine_fingerprint(),
96 scope: scope.entry_scope(),
97 type_rules_fingerprint: scope.type_rules_fingerprint,
98 reducers_fingerprint: scope.reducers_fingerprint,
99 }
100 }
101}
102
103#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
105pub enum ControlTierIdentity {
106 NotObserved,
109 Observed {
111 limits: ControlLimits,
113 },
114}
115
116impl ControlTierIdentity {
117 pub const fn is_observed(self) -> bool {
119 matches!(self, Self::Observed { .. })
120 }
121
122 pub fn ignore_rules_fingerprint(self) -> u64 {
129 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
130 const FNV_PRIME: u64 = 0x100_0000_01b3;
131 const UNBOUNDED: u8 = 0;
132 const BOUNDED: u8 = 1;
133
134 let Self::Observed { limits } = self else {
135 return 0;
136 };
137 let mut fingerprint = FNV_OFFSET_BASIS;
138 let mut mix = |bytes: &[u8]| {
139 for byte in bytes {
140 fingerprint ^= u64::from(*byte);
141 fingerprint = fingerprint.wrapping_mul(FNV_PRIME);
142 }
143 };
144 mix(&IGNORE_RULES_VERSION.to_le_bytes());
145 for limit in [limits.budget, limits.line_limit] {
146 match limit {
147 None => mix(&[UNBOUNDED]),
148 Some(limit) => {
149 mix(&[BOUNDED]);
150 mix(&u64::try_from(limit).unwrap_or(u64::MAX).to_le_bytes());
151 }
152 }
153 }
154 fingerprint.max(1)
155 }
156}
157
158#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
160pub struct SnapshotIdentity {
161 pub entries: EntryTierIdentity,
163 pub controls: ControlTierIdentity,
165}
166
167impl SnapshotIdentity {
168 pub fn scan_scope(self) -> ScanScope {
170 let EntryScope {
171 max_depth,
172 follow_symlinks,
173 one_filesystem,
174 hidden_fingerprint,
175 exclude_special,
176 population,
177 control_fingerprint: _,
178 } = self.entries.scope;
179 ScanScope {
180 max_depth,
181 follow_symlinks,
182 one_filesystem,
183 hidden_fingerprint,
184 exclude_special,
185 population,
186 ignore_rules_fingerprint: self.controls.ignore_rules_fingerprint(),
187 type_rules_fingerprint: self.entries.type_rules_fingerprint,
188 reducers_fingerprint: self.entries.reducers_fingerprint,
189 }
190 }
191}
192
193#[derive(Clone, PartialEq, Eq, Debug)]
204pub struct ContentTierIdentity {
205 pub entries: EntryTierIdentity,
207 pub analysis: AnalysisSet,
209 pub provenance: AnalyzerProvenance,
211}
212
213#[derive(Clone, PartialEq, Eq, Debug, Hash)]
218pub struct AnalyzerProvenance {
219 pub options_fingerprint: OptionsFingerprint,
221 pub analyzers: Vec<(AnalyzerId, AnalyzerVersion)>,
223}
224
225impl ContentTierIdentity {
226 pub(crate) fn record_provenance(&self) -> ContentProvenance {
228 ContentProvenance {
229 type_rules_fingerprint: self.entries.type_rules_fingerprint,
230 options_fingerprint: self.provenance.options_fingerprint,
231 analyzers: self.provenance.analyzers.clone(),
232 }
233 }
234
235 pub fn for_request(entries: EntryTierIdentity, analysis: AnalysisSet) -> Self {
237 let provenance = ContentProvenance::for_request(
238 crate::content::AnalysisRequest { profile: analysis, ..Default::default() },
239 entries.type_rules_fingerprint,
240 );
241 Self {
242 entries,
243 analysis,
244 provenance: AnalyzerProvenance {
245 options_fingerprint: provenance.options_fingerprint,
246 analyzers: provenance.analyzers,
247 },
248 }
249 }
250
251 #[must_use]
256 pub fn admit(&self, stored: &Self) -> Option<ContentAdmission<'_>> {
257 self.admit_parts(
258 stored.entries,
259 stored.analysis,
260 stored.provenance.options_fingerprint,
261 &stored.provenance.analyzers,
262 )
263 }
264
265 fn admit_parts(
266 &self,
267 entries: EntryTierIdentity,
268 analysis: AnalysisSet,
269 options: OptionsFingerprint,
270 analyzers: &[(AnalyzerId, AnalyzerVersion)],
271 ) -> Option<ContentAdmission<'_>> {
272 (entries == self.entries
273 && analysis == self.analysis
274 && options == self.provenance.options_fingerprint
275 && analyzers == self.provenance.analyzers)
276 .then_some(ContentAdmission { identity: self })
277 }
278
279 pub(crate) fn admit_record(
282 &self,
283 analysis: AnalysisSet,
284 provenance: &ContentProvenance,
285 ) -> Option<ContentAdmission<'_>> {
286 self.admit_parts(
287 EntryTierIdentity {
288 type_rules_fingerprint: provenance.type_rules_fingerprint,
289 ..self.entries
290 },
291 analysis,
292 provenance.options_fingerprint,
293 &provenance.analyzers,
294 )
295 }
296}
297
298#[must_use = "admission must be applied before consuming stored content"]
303#[derive(Clone, Copy, Debug)]
304pub struct ContentAdmission<'a> {
305 identity: &'a ContentTierIdentity,
306}
307
308impl<'a> ContentAdmission<'a> {
309 pub const fn identity(self) -> &'a ContentTierIdentity {
311 self.identity
312 }
313
314 pub(crate) fn record(self, record: crate::content::FileAnalysis) -> Option<AdmittedRecord<'a>> {
315 record
316 .matches_profile(self.identity.analysis)
317 .then_some(AdmittedRecord { identity: self.identity, record })
318 }
319
320 pub(crate) fn project(
321 self,
322 content: &crate::content::ContentIndex,
323 ) -> Option<ContentProjection<'_>> {
324 let _admission = self.identity.admit(content.identity()?)?;
325 Some(ContentProjection { content })
326 }
327}
328
329#[derive(Clone, Copy)]
331pub(crate) struct ContentProjection<'a> {
332 content: &'a crate::content::ContentIndex,
333}
334
335impl<'a> ContentProjection<'a> {
336 pub(crate) fn identity(self) -> &'a ContentTierIdentity {
337 self.content.identity().expect("admitted tier has an identity")
338 }
339 pub(crate) fn len(self) -> usize {
340 self.content.len()
341 }
342 pub(crate) fn state(self) -> Option<crate::content::ContentTierState> {
343 self.content.state()
344 }
345 pub(crate) fn file(self, path: &std::path::Path) -> Option<&'a crate::content::FileAnalysis> {
346 self.content.file(path)
347 }
348 pub(crate) fn records(
349 self,
350 ) -> impl Iterator<Item = (&'a std::path::Path, &'a crate::content::FileAnalysis)> {
351 self.content.records()
352 }
353}
354
355#[must_use]
357pub(crate) struct AdmittedRecord<'a> {
358 identity: &'a ContentTierIdentity,
359 record: crate::content::FileAnalysis,
360}
361
362impl AdmittedRecord<'_> {
363 pub(crate) fn value(&self) -> &crate::content::FileAnalysis {
364 &self.record
365 }
366
367 pub(crate) fn into_record(self) -> crate::content::FileAnalysis {
368 self.record
369 }
370
371 pub(crate) fn for_tier(
372 self,
373 wanted: &ContentTierIdentity,
374 ) -> Option<crate::content::FileAnalysis> {
375 wanted.admit(self.identity)?.record(self.record).map(AdmittedRecord::into_record)
376 }
377}
378
379#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
381pub enum Serves {
382 Exact,
385 ProjectControlsOff,
388 Refuse,
390}
391
392pub fn serves_snapshot(stored: SnapshotIdentity, wanted: SnapshotIdentity) -> Serves {
398 if stored == wanted {
399 Serves::Exact
400 } else if stored.entries == wanted.entries
401 && stored.controls.is_observed()
402 && wanted.controls == ControlTierIdentity::NotObserved
403 {
404 Serves::ProjectControlsOff
405 } else {
406 Serves::Refuse
407 }
408}
409
410pub(crate) fn entries_writable(index: &crate::Index) -> bool {
422 index.freshness() == crate::Freshness::Fresh
423 && index.state().coverage == crate::engine_contract::Coverage::Complete
424}
425
426pub(crate) fn content_record_writable(
436 index: &crate::Index,
437 path: &std::path::Path,
438 record: &crate::content::FileAnalysis,
439) -> bool {
440 if !record.is_reusable() {
441 return false;
442 }
443 if entries_writable(index) {
448 return true;
449 }
450 let crate::PathState::Present { kind: crate::EntryKind::File, attrs } = index.path_state(path)
451 else {
452 return false;
453 };
454 attrs.fingerprint() == record.fingerprint
455 && index.provenance(path).is_some_and(crate::Provenance::is_verified)
456}
457
458pub(crate) fn content_tier_writable(
466 index: &crate::Index,
467 stored_entries: impl FnOnce() -> Option<EntryTierIdentity>,
468) -> bool {
469 entries_writable(index)
470 || stored_entries().is_some_and(|stored| stored == index.snapshot_identity().entries)
471}
472
473pub(crate) const BOUND_BYTES: usize = 1 + 8;
484
485pub(crate) const ENTRY_TIER_BYTES: usize = BOUND_BYTES + 1 + 8 + 8 + 8 + 8;
489
490pub(crate) const CONTROL_TIER_BYTES: usize = 1 + 2 * BOUND_BYTES;
493
494pub(crate) const SNAPSHOT_IDENTITY_BYTES: usize = ENTRY_TIER_BYTES + CONTROL_TIER_BYTES;
496
497const SCOPE_FOLLOW_SYMLINKS: u8 = 1 << 0;
499const SCOPE_ONE_FILESYSTEM: u8 = 1 << 1;
501const SCOPE_EXCLUDE_SPECIAL: u8 = 1 << 2;
503const SCOPE_POPULATION_EXCLUDE: u8 = 1 << 3;
504const SCOPE_POPULATION_ONLY: u8 = 1 << 4;
505const SCOPE_KNOWN_FLAGS: u8 = SCOPE_FOLLOW_SYMLINKS
507 | SCOPE_ONE_FILESYSTEM
508 | SCOPE_EXCLUDE_SPECIAL
509 | SCOPE_POPULATION_EXCLUDE
510 | SCOPE_POPULATION_ONLY;
511
512const CONTROLS_NOT_OBSERVED: u8 = 0;
514const CONTROLS_OBSERVED: u8 = 1;
516
517const UNBOUNDED: u8 = 0;
519const BOUNDED: u8 = 1;
521
522const _: () = assert!(usize::BITS <= u64::BITS, "a bound fits its eight encoded bytes");
525
526struct FixedWriter<const N: usize> {
528 bytes: [u8; N],
529 at: usize,
530}
531
532impl<const N: usize> FixedWriter<N> {
533 const fn new() -> Self {
534 Self { bytes: [0; N], at: 0 }
535 }
536
537 fn put(&mut self, field: &[u8]) {
538 let end = self.at + field.len();
539 self.bytes[self.at..end].copy_from_slice(field);
540 self.at = end;
541 }
542
543 fn put_bound(&mut self, bound: Option<usize>) {
547 let (tag, value) = match bound {
548 None => (UNBOUNDED, 0),
549 Some(bound) => (BOUNDED, u64::try_from(bound).unwrap_or(u64::MAX)),
551 };
552 self.put(&[tag]);
553 self.put(&value.to_le_bytes());
554 }
555
556 fn finish(self) -> [u8; N] {
557 debug_assert_eq!(self.at, N, "every field of a fixed-width encoding is written");
558 self.bytes
559 }
560}
561
562struct NotEncoded;
564
565struct FixedReader<'a> {
567 rest: &'a [u8],
568}
569
570impl FixedReader<'_> {
571 fn take<const W: usize>(&mut self) -> [u8; W] {
572 let (field, rest) =
573 self.rest.split_first_chunk::<W>().expect("a fixed-width encoding holds every field");
574 self.rest = rest;
575 *field
576 }
577
578 fn u8(&mut self) -> u8 {
579 self.take::<1>()[0]
580 }
581
582 fn u64(&mut self) -> u64 {
583 u64::from_le_bytes(self.take())
584 }
585
586 fn bound(&mut self) -> Result<Option<usize>, NotEncoded> {
588 match (self.u8(), self.u64()) {
589 (UNBOUNDED, 0) => Ok(None),
590 (BOUNDED, value) => usize::try_from(value).map(Some).map_err(|_| NotEncoded),
591 _ => Err(NotEncoded),
592 }
593 }
594}
595
596impl EntryTierIdentity {
597 pub(crate) fn encode(self) -> [u8; ENTRY_TIER_BYTES] {
599 let scope = self.scope;
600 let mut flags = 0u8;
601 if scope.follow_symlinks {
602 flags |= SCOPE_FOLLOW_SYMLINKS;
603 }
604 if scope.one_filesystem {
605 flags |= SCOPE_ONE_FILESYSTEM;
606 }
607 if scope.exclude_special {
608 flags |= SCOPE_EXCLUDE_SPECIAL;
609 }
610 flags |= match scope.population {
611 IgnoredEntries::Include => 0,
612 IgnoredEntries::Exclude => SCOPE_POPULATION_EXCLUDE,
613 IgnoredEntries::Only => SCOPE_POPULATION_ONLY,
614 };
615 let mut out = FixedWriter::new();
616 out.put_bound(scope.max_depth);
617 out.put(&[flags]);
618 out.put(&scope.hidden_fingerprint.to_le_bytes());
619 out.put(&scope.control_fingerprint.to_le_bytes());
620 out.put(&self.type_rules_fingerprint.to_le_bytes());
621 out.put(&self.reducers_fingerprint.to_le_bytes());
622 out.finish()
623 }
624
625 pub(crate) fn decode(engine: u64, bytes: &[u8; ENTRY_TIER_BYTES]) -> Option<Self> {
628 let mut fields = FixedReader { rest: bytes };
629 let max_depth = fields.bound().ok()?;
630 let flags = fields.u8();
631 if flags & !SCOPE_KNOWN_FLAGS != 0 {
632 return None;
633 }
634 let population = match flags & (SCOPE_POPULATION_EXCLUDE | SCOPE_POPULATION_ONLY) {
635 0 => IgnoredEntries::Include,
636 SCOPE_POPULATION_EXCLUDE => IgnoredEntries::Exclude,
637 SCOPE_POPULATION_ONLY => IgnoredEntries::Only,
638 _ => return None,
639 };
640 let hidden_fingerprint = fields.u64();
641 let control_fingerprint = fields.u64();
642 if (population == IgnoredEntries::Include && control_fingerprint != 0)
643 || (population != IgnoredEntries::Include && control_fingerprint == 0)
644 {
645 return None;
646 }
647 let scope = EntryScope {
648 max_depth,
649 follow_symlinks: flags & SCOPE_FOLLOW_SYMLINKS != 0,
650 one_filesystem: flags & SCOPE_ONE_FILESYSTEM != 0,
651 hidden_fingerprint,
652 exclude_special: flags & SCOPE_EXCLUDE_SPECIAL != 0,
653 population,
654 control_fingerprint,
655 };
656 Some(Self {
657 engine,
658 scope,
659 type_rules_fingerprint: fields.u64(),
660 reducers_fingerprint: fields.u64(),
661 })
662 }
663}
664
665impl ControlTierIdentity {
666 pub(crate) fn encode(self) -> [u8; CONTROL_TIER_BYTES] {
668 let mut out = FixedWriter::new();
669 match self {
670 Self::NotObserved => out.put(&[CONTROLS_NOT_OBSERVED; CONTROL_TIER_BYTES]),
671 Self::Observed { limits } => {
672 out.put(&[CONTROLS_OBSERVED]);
673 out.put_bound(limits.budget);
674 out.put_bound(limits.line_limit);
675 }
676 }
677 out.finish()
678 }
679
680 pub(crate) fn decode(bytes: &[u8; CONTROL_TIER_BYTES]) -> Option<Self> {
682 let mut fields = FixedReader { rest: bytes };
683 match fields.u8() {
684 CONTROLS_NOT_OBSERVED => {
685 bytes[1..].iter().all(|byte| *byte == 0).then_some(Self::NotObserved)
686 }
687 CONTROLS_OBSERVED => {
688 let budget = fields.bound().ok()?;
689 let line_limit = fields.bound().ok()?;
690 Some(Self::Observed { limits: ControlLimits { budget, line_limit } })
691 }
692 _ => None,
693 }
694 }
695}
696
697impl SnapshotIdentity {
698 pub(crate) fn encode(self) -> [u8; SNAPSHOT_IDENTITY_BYTES] {
700 let mut out = FixedWriter::new();
701 out.put(&self.entries.encode());
702 out.put(&self.controls.encode());
703 out.finish()
704 }
705
706 pub(crate) fn decode(engine: u64, bytes: &[u8; SNAPSHOT_IDENTITY_BYTES]) -> Option<Self> {
708 let mut fields = FixedReader { rest: bytes };
709 let entries = EntryTierIdentity::decode(engine, &fields.take())?;
710 let controls = ControlTierIdentity::decode(&fields.take())?;
711 if entries.scope.population != IgnoredEntries::Include
712 && entries.scope.control_fingerprint != controls.ignore_rules_fingerprint()
713 {
714 return None;
715 }
716 Some(Self { entries, controls })
717 }
718}
719
720#[cfg(test)]
721mod tests {
722 use super::*;
723 use crate::ScanConfig;
724
725 fn limits(budget: Option<usize>, line_limit: Option<usize>) -> ControlLimits {
726 ControlLimits { budget, line_limit }
727 }
728
729 #[test]
730 fn snapshot_serving_is_equality_plus_observation_on_to_off() {
731 let base = ScanConfig::default().snapshot_identity();
732 assert_eq!(serves_snapshot(base, base), Serves::Exact);
733
734 let entries = base.entries;
735 let mut refused = vec![
736 SnapshotIdentity {
737 entries: EntryTierIdentity { engine: entries.engine ^ 1, ..entries },
738 ..base
739 },
740 SnapshotIdentity {
741 entries: EntryTierIdentity {
742 type_rules_fingerprint: entries.type_rules_fingerprint ^ 1,
743 ..entries
744 },
745 ..base
746 },
747 SnapshotIdentity {
748 entries: EntryTierIdentity {
749 reducers_fingerprint: entries.reducers_fingerprint ^ 1,
750 ..entries
751 },
752 ..base
753 },
754 SnapshotIdentity {
755 controls: ControlTierIdentity::Observed { limits: limits(None, None) },
756 ..base
757 },
758 ];
759 for config in [
760 ScanConfig { max_depth: Some(1), ..ScanConfig::default() },
761 ScanConfig { one_filesystem: true, ..ScanConfig::default() },
762 ScanConfig { exclude_special: true, ..ScanConfig::default() },
763 ScanConfig { population: IgnoredEntries::Exclude, ..ScanConfig::default() },
764 ScanConfig { population: IgnoredEntries::Only, ..ScanConfig::default() },
765 ScanConfig {
766 hidden: Some(std::sync::Arc::new(crate::HiddenPolicy::prune_hidden(
767 std::iter::empty::<std::ffi::OsString>(),
768 ))),
769 ..ScanConfig::default()
770 },
771 ] {
772 refused.push(config.snapshot_identity());
773 }
774 for wanted in refused {
775 assert_eq!(serves_snapshot(base, wanted), Serves::Refuse, "{wanted:?}");
776 assert_eq!(serves_snapshot(wanted, base), Serves::Refuse, "{wanted:?}");
777 }
778 let blind = SnapshotIdentity { controls: ControlTierIdentity::NotObserved, ..base };
779 assert_eq!(serves_snapshot(base, blind), Serves::ProjectControlsOff);
780 assert_eq!(serves_snapshot(blind, base), Serves::Refuse);
781 }
782
783 #[test]
784 fn control_settings_change_only_the_control_tier() {
785 let base = ScanConfig::default();
786 for config in [
787 ScanConfig { read_controls: false, ..base.clone() },
788 ScanConfig { control_limits: limits(None, Some(1)), ..base.clone() },
789 ScanConfig {
790 read_controls: false,
791 control_limits: limits(Some(1), None),
792 ..base.clone()
793 },
794 ] {
795 assert_eq!(config.snapshot_identity().entries, base.snapshot_identity().entries);
796 assert_ne!(config.snapshot_identity().controls, base.snapshot_identity().controls);
797 }
798 let blind = ScanConfig { read_controls: false, ..base.clone() };
800 let blind_other_limits = ScanConfig { control_limits: limits(None, None), ..blind.clone() };
801 assert_eq!(blind.snapshot_identity(), blind_other_limits.snapshot_identity());
802 assert_eq!(blind.control_identity(), ControlTierIdentity::NotObserved);
803 }
804
805 #[test]
806 fn the_ignore_rules_fingerprint_reserves_zero_for_an_unobserved_tier() {
807 assert_eq!(ControlTierIdentity::NotObserved.ignore_rules_fingerprint(), 0);
808 let defaults = ControlLimits::default();
809 let observed = [
810 defaults,
811 limits(None, defaults.line_limit),
812 limits(defaults.budget, None),
813 limits(None, None),
814 limits(defaults.line_limit, defaults.budget),
816 ]
817 .map(|limits| ControlTierIdentity::Observed { limits }.ignore_rules_fingerprint());
818 for (index, fingerprint) in observed.iter().enumerate() {
819 assert_ne!(*fingerprint, 0);
820 assert!(!observed[index + 1..].contains(fingerprint), "{observed:?}");
821 }
822 }
823
824 #[test]
825 fn the_scope_an_identity_composes_is_the_one_a_scan_records() {
826 for config in [
827 ScanConfig::default(),
828 ScanConfig { read_controls: false, ..ScanConfig::default() },
829 ScanConfig { control_limits: limits(None, None), ..ScanConfig::default() },
830 ScanConfig { max_depth: Some(3), exclude_special: true, ..ScanConfig::default() },
831 ScanConfig { population: IgnoredEntries::Exclude, ..ScanConfig::default() },
832 ScanConfig { population: IgnoredEntries::Only, ..ScanConfig::default() },
833 ] {
834 let identity = config.snapshot_identity();
835 let scope = identity.scan_scope();
836 assert_eq!(scope, config.scope());
837 assert_eq!(EntryTierIdentity::of_scope(scope), identity.entries);
838 assert_eq!(scope.observes_controls(), identity.controls.is_observed());
839
840 let index = crate::Index::new_with_config("/root", &config);
841 assert_eq!(index.snapshot_identity(), identity);
842 assert_eq!(index.control_identity(), config.control_identity());
843 }
844 }
845
846 #[test]
847 fn each_tier_is_writable_by_its_own_rule() {
848 let config = ScanConfig::default();
849 let entries = config.snapshot_identity().entries;
850 let other =
851 ScanConfig { max_depth: Some(2), ..ScanConfig::default() }.snapshot_identity().entries;
852
853 let mut complete = crate::Index::new_with_config("/root", &config);
854 complete.set_initial_freshness(true);
855 assert!(entries_writable(&complete));
856 for stored in [None, Some(entries), Some(other)] {
857 assert!(
858 content_tier_writable(&complete, || stored),
859 "a complete pass writes: {stored:?}"
860 );
861 }
862
863 let mut partial = crate::Index::new_with_config("/root", &config);
864 partial.set_initial_freshness(false);
865 assert!(!entries_writable(&partial), "an absent entry would change totals");
866 assert!(content_tier_writable(&partial, || Some(entries)));
867 for stored in [None, Some(other)] {
868 assert!(!content_tier_writable(&partial, || stored), "mismatched pair: {stored:?}");
869 }
870
871 let mut unverified = complete.clone();
872 unverified.mark_unverified();
873 assert!(!entries_writable(&unverified), "a cache-only index verified nothing");
874 }
875
876 fn identities() -> Vec<SnapshotIdentity> {
879 let base = ScanConfig::default().snapshot_identity();
880 let entries = base.entries;
881 let defaults = ControlLimits::default();
882 assert_eq!(entries.scope.max_depth, None);
883 assert!(defaults.budget.is_some() && defaults.line_limit.is_some());
884 let mut all = vec![
885 base,
886 ScanConfig { population: IgnoredEntries::Exclude, ..ScanConfig::default() }
887 .snapshot_identity(),
888 ScanConfig { population: IgnoredEntries::Only, ..ScanConfig::default() }
889 .snapshot_identity(),
890 ScanConfig {
891 population: IgnoredEntries::Exclude,
892 control_limits: limits(None, None),
893 ..ScanConfig::default()
894 }
895 .snapshot_identity(),
896 ];
897 for scope in [
898 EntryScope { max_depth: Some(0), ..entries.scope },
899 EntryScope { max_depth: Some(usize::MAX), ..entries.scope },
901 EntryScope { follow_symlinks: true, ..entries.scope },
902 EntryScope { one_filesystem: true, ..entries.scope },
903 EntryScope { exclude_special: true, ..entries.scope },
904 EntryScope { hidden_fingerprint: u64::MAX, ..entries.scope },
905 ] {
906 all.push(SnapshotIdentity { entries: EntryTierIdentity { scope, ..entries }, ..base });
907 }
908 for entries in [
909 EntryTierIdentity { type_rules_fingerprint: 0, ..entries },
910 EntryTierIdentity { reducers_fingerprint: u64::MAX, ..entries },
911 ] {
912 all.push(SnapshotIdentity { entries, ..base });
913 }
914 for controls in [
915 ControlTierIdentity::NotObserved,
916 ControlTierIdentity::Observed { limits: limits(None, defaults.line_limit) },
917 ControlTierIdentity::Observed { limits: limits(Some(0), defaults.line_limit) },
918 ControlTierIdentity::Observed { limits: limits(defaults.budget, None) },
919 ControlTierIdentity::Observed { limits: limits(defaults.budget, Some(usize::MAX)) },
920 ] {
921 all.push(SnapshotIdentity { controls, ..base });
922 }
923 all
924 }
925
926 #[test]
927 fn every_identity_round_trips_through_its_fixed_width_encoding() {
928 let identities = identities();
929 let encoded = identities.iter().map(|identity| identity.encode()).collect::<Vec<_>>();
930 for (identity, bytes) in identities.iter().zip(&encoded) {
931 let engine = identity.entries.engine;
932 assert_eq!(SnapshotIdentity::decode(engine, bytes), Some(*identity));
933 let (entry_bytes, control_bytes) = bytes.split_at(ENTRY_TIER_BYTES);
934 assert_eq!(
935 EntryTierIdentity::decode(engine, entry_bytes.try_into().expect("width")),
936 Some(identity.entries)
937 );
938 assert_eq!(
939 ControlTierIdentity::decode(control_bytes.try_into().expect("width")),
940 Some(identity.controls)
941 );
942 }
943 for (index, bytes) in encoded.iter().enumerate() {
945 assert!(!encoded[index + 1..].contains(bytes), "{:?}", identities[index]);
946 }
947 }
948
949 #[test]
950 fn bytes_no_encoder_writes_are_refused() {
951 let base = ScanConfig::default().snapshot_identity();
952 let engine = base.entries.engine;
953 let bounded = EntryTierIdentity {
954 scope: EntryScope { max_depth: Some(3), ..base.entries.scope },
955 ..base.entries
956 };
957 let (depth_tag_at, depth_at, flags_at) = (0, 1, BOUND_BYTES);
958 let mut forged_entries = Vec::new();
959 let mut unknown_flag = base.entries.encode();
960 unknown_flag[flags_at] |= 1 << 7;
961 forged_entries.push(unknown_flag);
962 let mut incompatible_population = base.entries.encode();
963 incompatible_population[flags_at] |= SCOPE_POPULATION_EXCLUDE | SCOPE_POPULATION_ONLY;
964 forged_entries.push(incompatible_population);
965 let mut missing_control_fingerprint = base.entries.encode();
966 missing_control_fingerprint[flags_at] |= SCOPE_POPULATION_EXCLUDE;
967 forged_entries.push(missing_control_fingerprint);
968 let mut unexplained_control_fingerprint = base.entries.encode();
969 unexplained_control_fingerprint[BOUND_BYTES + 1 + 8] = 1;
970 forged_entries.push(unexplained_control_fingerprint);
971 let mut unknown_depth_tag = bounded.encode();
972 assert_eq!(unknown_depth_tag[depth_tag_at], BOUNDED);
973 unknown_depth_tag[depth_tag_at] = 2;
974 forged_entries.push(unknown_depth_tag);
975 let mut unbounded_depth_with_a_value = base.entries.encode();
976 assert_eq!(unbounded_depth_with_a_value[depth_tag_at], UNBOUNDED);
977 unbounded_depth_with_a_value[depth_at] = 1;
978 forged_entries.push(unbounded_depth_with_a_value);
979 for bytes in forged_entries {
980 assert_eq!(EntryTierIdentity::decode(engine, &bytes), None, "{bytes:?}");
981 }
982
983 let observed = ControlTierIdentity::Observed { limits: limits(None, Some(1)) };
984 let controls = observed.encode();
985 let (budget_tag_at, budget_at, line_tag_at) = (1, 2, 1 + BOUND_BYTES);
986 assert_eq!(controls[budget_tag_at], UNBOUNDED);
987 assert_eq!(controls[line_tag_at], BOUNDED);
988 let mut forged = Vec::new();
989 let mut unknown_tag = controls;
990 unknown_tag[0] = 2;
991 forged.push(unknown_tag);
992 let mut unknown_limit_tag = controls;
993 unknown_limit_tag[line_tag_at] = 2;
994 forged.push(unknown_limit_tag);
995 let mut unbounded_with_a_value = controls;
996 unbounded_with_a_value[budget_at] = 1;
997 forged.push(unbounded_with_a_value);
998 let mut unobserved_with_limits = controls;
999 unobserved_with_limits[0] = CONTROLS_NOT_OBSERVED;
1000 forged.push(unobserved_with_limits);
1001 for bytes in forged {
1002 assert_eq!(ControlTierIdentity::decode(&bytes), None, "{bytes:?}");
1003 }
1004 }
1005
1006 #[test]
1009 fn a_content_tier_holds_its_records_type_rules_in_its_entry_tier() {
1010 use crate::content::AnalysisRequest;
1011
1012 let entries = ScanConfig::default().snapshot_identity().entries;
1013 let request = AnalysisRequest { profile: AnalysisSet::ALL, ..AnalysisRequest::default() };
1014 let records = ContentProvenance::for_request(request, entries.type_rules_fingerprint);
1015 let identity = ContentTierIdentity::for_request(entries, request.profile);
1016 assert_eq!(identity.record_provenance(), records);
1017 assert!(identity.admit_record(request.profile, &records).is_some());
1018
1019 let other_rules = ContentProvenance::for_request(request, !entries.type_rules_fingerprint);
1020 assert!(identity.admit_record(request.profile, &other_rules).is_none(), "other type rules");
1021 let lines = AnalysisSet::NONE.with_lines();
1022 assert!(identity.admit_record(lines, &records).is_none(), "another analyzer set's label");
1023 }
1024
1025 #[test]
1026 fn content_admission_refuses_every_identity_difference_and_applies_exact_identity() {
1027 let entries = ScanConfig::default().snapshot_identity().entries;
1028 let wanted = ContentTierIdentity::for_request(entries, AnalysisSet::ALL);
1029 assert_eq!(wanted.admit(&wanted).expect("exact admission").identity(), &wanted);
1030 let changes: &[fn(&mut ContentTierIdentity)] = &[
1031 |identity| identity.entries.engine ^= 1,
1032 |identity| identity.entries.scope.max_depth = Some(1),
1033 |identity| identity.entries.type_rules_fingerprint ^= 1,
1034 |identity| identity.entries.reducers_fingerprint ^= 1,
1035 |identity| identity.analysis = AnalysisSet::LINES_ONLY,
1036 |identity| identity.provenance.options_fingerprint.0 ^= 1,
1037 |identity| identity.provenance.analyzers[0].1.0 += 1,
1038 |identity| {
1039 identity.provenance.analyzers.pop();
1040 },
1041 ];
1042 for change in changes {
1043 let mut other = wanted.clone();
1044 change(&mut other);
1045 assert!(wanted.admit(&other).is_none(), "stored mismatch: {other:?}");
1046 assert!(other.admit(&wanted).is_none(), "requested mismatch: {other:?}");
1047 }
1048 }
1049
1050 #[test]
1051 fn the_engine_fingerprint_comes_from_the_store_not_the_encoding() {
1052 let identity = ScanConfig::default().snapshot_identity();
1053 let bytes = identity.encode();
1054 let other = SnapshotIdentity::decode(!identity.entries.engine, &bytes).expect("decode");
1055 assert_eq!(other.entries.engine, !identity.entries.engine);
1056 assert_eq!(serves_snapshot(other, identity), Serves::Refuse);
1057 }
1058}