1use crate::content::{
16 AnalysisSet, AnalyzerId, AnalyzerVersion, ContentProvenance, OptionsFingerprint,
17};
18use crate::control::ControlLimits;
19use crate::engine_contract::ScanScope;
20
21const IGNORE_RULES_VERSION: u64 = 2;
24
25#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
35pub struct EntryScope {
36 pub max_depth: Option<usize>,
38 pub follow_symlinks: bool,
40 pub one_filesystem: bool,
42 pub hidden_fingerprint: u64,
44 pub exclude_special: bool,
46}
47
48#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
56pub struct EntryTierIdentity {
57 pub engine: u64,
60 pub scope: EntryScope,
62 pub type_rules_fingerprint: u64,
64 pub reducers_fingerprint: u64,
66}
67
68impl EntryTierIdentity {
69 pub fn of_scope(scope: ScanScope) -> Self {
71 Self {
72 engine: crate::snapshot::engine_fingerprint(),
73 scope: scope.entry_scope(),
74 type_rules_fingerprint: scope.type_rules_fingerprint,
75 reducers_fingerprint: scope.reducers_fingerprint,
76 }
77 }
78}
79
80#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
82pub enum ControlTierIdentity {
83 NotObserved,
86 Observed {
88 limits: ControlLimits,
90 },
91}
92
93impl ControlTierIdentity {
94 pub const fn is_observed(self) -> bool {
96 matches!(self, Self::Observed { .. })
97 }
98
99 pub fn ignore_rules_fingerprint(self) -> u64 {
106 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
107 const FNV_PRIME: u64 = 0x100_0000_01b3;
108 const UNBOUNDED: u8 = 0;
109 const BOUNDED: u8 = 1;
110
111 let Self::Observed { limits } = self else {
112 return 0;
113 };
114 let mut fingerprint = FNV_OFFSET_BASIS;
115 let mut mix = |bytes: &[u8]| {
116 for byte in bytes {
117 fingerprint ^= u64::from(*byte);
118 fingerprint = fingerprint.wrapping_mul(FNV_PRIME);
119 }
120 };
121 mix(&IGNORE_RULES_VERSION.to_le_bytes());
122 for limit in [limits.budget, limits.line_limit] {
123 match limit {
124 None => mix(&[UNBOUNDED]),
125 Some(limit) => {
126 mix(&[BOUNDED]);
127 mix(&u64::try_from(limit).unwrap_or(u64::MAX).to_le_bytes());
128 }
129 }
130 }
131 fingerprint.max(1)
132 }
133}
134
135#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
137pub struct SnapshotIdentity {
138 pub entries: EntryTierIdentity,
140 pub controls: ControlTierIdentity,
142}
143
144impl SnapshotIdentity {
145 pub fn scan_scope(self) -> ScanScope {
147 let EntryScope {
148 max_depth,
149 follow_symlinks,
150 one_filesystem,
151 hidden_fingerprint,
152 exclude_special,
153 } = self.entries.scope;
154 ScanScope {
155 max_depth,
156 follow_symlinks,
157 one_filesystem,
158 hidden_fingerprint,
159 exclude_special,
160 ignore_rules_fingerprint: self.controls.ignore_rules_fingerprint(),
161 type_rules_fingerprint: self.entries.type_rules_fingerprint,
162 reducers_fingerprint: self.entries.reducers_fingerprint,
163 }
164 }
165}
166
167#[derive(Clone, PartialEq, Eq, Debug)]
178pub struct ContentTierIdentity {
179 pub entries: EntryTierIdentity,
181 pub analysis: AnalysisSet,
183 pub provenance: AnalyzerProvenance,
185}
186
187#[derive(Clone, PartialEq, Eq, Debug, Hash)]
192pub struct AnalyzerProvenance {
193 pub options_fingerprint: OptionsFingerprint,
195 pub analyzers: Vec<(AnalyzerId, AnalyzerVersion)>,
197}
198
199impl ContentTierIdentity {
200 pub(crate) fn record_provenance(&self) -> ContentProvenance {
202 ContentProvenance {
203 type_rules_fingerprint: self.entries.type_rules_fingerprint,
204 options_fingerprint: self.provenance.options_fingerprint,
205 analyzers: self.provenance.analyzers.clone(),
206 }
207 }
208
209 pub fn for_request(entries: EntryTierIdentity, analysis: AnalysisSet) -> Self {
211 let provenance = ContentProvenance::for_request(
212 crate::content::AnalysisRequest { profile: analysis, ..Default::default() },
213 entries.type_rules_fingerprint,
214 );
215 Self {
216 entries,
217 analysis,
218 provenance: AnalyzerProvenance {
219 options_fingerprint: provenance.options_fingerprint,
220 analyzers: provenance.analyzers,
221 },
222 }
223 }
224
225 #[must_use]
230 pub fn admit(&self, stored: &Self) -> Option<ContentAdmission<'_>> {
231 self.admit_parts(
232 stored.entries,
233 stored.analysis,
234 stored.provenance.options_fingerprint,
235 &stored.provenance.analyzers,
236 )
237 }
238
239 fn admit_parts(
240 &self,
241 entries: EntryTierIdentity,
242 analysis: AnalysisSet,
243 options: OptionsFingerprint,
244 analyzers: &[(AnalyzerId, AnalyzerVersion)],
245 ) -> Option<ContentAdmission<'_>> {
246 (entries == self.entries
247 && analysis == self.analysis
248 && options == self.provenance.options_fingerprint
249 && analyzers == self.provenance.analyzers)
250 .then_some(ContentAdmission { identity: self })
251 }
252
253 pub(crate) fn admit_record(
256 &self,
257 analysis: AnalysisSet,
258 provenance: &ContentProvenance,
259 ) -> Option<ContentAdmission<'_>> {
260 self.admit_parts(
261 EntryTierIdentity {
262 type_rules_fingerprint: provenance.type_rules_fingerprint,
263 ..self.entries
264 },
265 analysis,
266 provenance.options_fingerprint,
267 &provenance.analyzers,
268 )
269 }
270}
271
272#[must_use = "admission must be applied before consuming stored content"]
277#[derive(Clone, Copy, Debug)]
278pub struct ContentAdmission<'a> {
279 identity: &'a ContentTierIdentity,
280}
281
282impl<'a> ContentAdmission<'a> {
283 pub const fn identity(self) -> &'a ContentTierIdentity {
285 self.identity
286 }
287
288 pub(crate) fn record(self, record: crate::content::FileAnalysis) -> Option<AdmittedRecord<'a>> {
289 record
290 .matches_profile(self.identity.analysis)
291 .then_some(AdmittedRecord { identity: self.identity, record })
292 }
293
294 pub(crate) fn project(
295 self,
296 content: &crate::content::ContentIndex,
297 ) -> Option<ContentProjection<'_>> {
298 let _admission = self.identity.admit(content.identity()?)?;
299 Some(ContentProjection { content })
300 }
301}
302
303#[derive(Clone, Copy)]
305pub(crate) struct ContentProjection<'a> {
306 content: &'a crate::content::ContentIndex,
307}
308
309impl<'a> ContentProjection<'a> {
310 pub(crate) fn identity(self) -> &'a ContentTierIdentity {
311 self.content.identity().expect("admitted tier has an identity")
312 }
313 pub(crate) fn len(self) -> usize {
314 self.content.len()
315 }
316 pub(crate) fn state(self) -> Option<crate::content::ContentTierState> {
317 self.content.state()
318 }
319 pub(crate) fn file(self, path: &std::path::Path) -> Option<&'a crate::content::FileAnalysis> {
320 self.content.file(path)
321 }
322 pub(crate) fn records(
323 self,
324 ) -> impl Iterator<Item = (&'a std::path::Path, &'a crate::content::FileAnalysis)> {
325 self.content.records()
326 }
327}
328
329#[must_use]
331pub(crate) struct AdmittedRecord<'a> {
332 identity: &'a ContentTierIdentity,
333 record: crate::content::FileAnalysis,
334}
335
336impl AdmittedRecord<'_> {
337 pub(crate) fn value(&self) -> &crate::content::FileAnalysis {
338 &self.record
339 }
340
341 pub(crate) fn into_record(self) -> crate::content::FileAnalysis {
342 self.record
343 }
344
345 pub(crate) fn for_tier(
346 self,
347 wanted: &ContentTierIdentity,
348 ) -> Option<crate::content::FileAnalysis> {
349 wanted.admit(self.identity)?.record(self.record).map(AdmittedRecord::into_record)
350 }
351}
352
353#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
355pub enum Serves {
356 Exact,
359 ProjectControlsOff,
362 Refuse,
364}
365
366pub fn serves_snapshot(stored: SnapshotIdentity, wanted: SnapshotIdentity) -> Serves {
372 if stored == wanted {
373 Serves::Exact
374 } else if stored.entries == wanted.entries
375 && stored.controls.is_observed()
376 && wanted.controls == ControlTierIdentity::NotObserved
377 {
378 Serves::ProjectControlsOff
379 } else {
380 Serves::Refuse
381 }
382}
383
384pub(crate) fn entries_writable(index: &crate::Index) -> bool {
396 index.freshness() == crate::Freshness::Fresh
397 && index.state().coverage == crate::engine_contract::Coverage::Complete
398}
399
400pub(crate) fn content_record_writable(
410 index: &crate::Index,
411 path: &std::path::Path,
412 record: &crate::content::FileAnalysis,
413) -> bool {
414 if !record.is_reusable() {
415 return false;
416 }
417 if entries_writable(index) {
422 return true;
423 }
424 let crate::PathState::Present { kind: crate::EntryKind::File, attrs } = index.path_state(path)
425 else {
426 return false;
427 };
428 attrs.fingerprint() == record.fingerprint
429 && index.provenance(path).is_some_and(crate::Provenance::is_verified)
430}
431
432pub(crate) fn content_tier_writable(
440 index: &crate::Index,
441 stored_entries: impl FnOnce() -> Option<EntryTierIdentity>,
442) -> bool {
443 entries_writable(index)
444 || stored_entries().is_some_and(|stored| stored == index.snapshot_identity().entries)
445}
446
447pub(crate) const BOUND_BYTES: usize = 1 + 8;
458
459pub(crate) const ENTRY_TIER_BYTES: usize = BOUND_BYTES + 1 + 8 + 8 + 8;
463
464pub(crate) const CONTROL_TIER_BYTES: usize = 1 + 2 * BOUND_BYTES;
467
468pub(crate) const SNAPSHOT_IDENTITY_BYTES: usize = ENTRY_TIER_BYTES + CONTROL_TIER_BYTES;
470
471const SCOPE_FOLLOW_SYMLINKS: u8 = 1 << 0;
473const SCOPE_ONE_FILESYSTEM: u8 = 1 << 1;
475const SCOPE_EXCLUDE_SPECIAL: u8 = 1 << 2;
477const SCOPE_KNOWN_FLAGS: u8 = SCOPE_FOLLOW_SYMLINKS | SCOPE_ONE_FILESYSTEM | SCOPE_EXCLUDE_SPECIAL;
479
480const CONTROLS_NOT_OBSERVED: u8 = 0;
482const CONTROLS_OBSERVED: u8 = 1;
484
485const UNBOUNDED: u8 = 0;
487const BOUNDED: u8 = 1;
489
490const _: () = assert!(usize::BITS <= u64::BITS, "a bound fits its eight encoded bytes");
493
494struct FixedWriter<const N: usize> {
496 bytes: [u8; N],
497 at: usize,
498}
499
500impl<const N: usize> FixedWriter<N> {
501 const fn new() -> Self {
502 Self { bytes: [0; N], at: 0 }
503 }
504
505 fn put(&mut self, field: &[u8]) {
506 let end = self.at + field.len();
507 self.bytes[self.at..end].copy_from_slice(field);
508 self.at = end;
509 }
510
511 fn put_bound(&mut self, bound: Option<usize>) {
515 let (tag, value) = match bound {
516 None => (UNBOUNDED, 0),
517 Some(bound) => (BOUNDED, u64::try_from(bound).unwrap_or(u64::MAX)),
519 };
520 self.put(&[tag]);
521 self.put(&value.to_le_bytes());
522 }
523
524 fn finish(self) -> [u8; N] {
525 debug_assert_eq!(self.at, N, "every field of a fixed-width encoding is written");
526 self.bytes
527 }
528}
529
530struct NotEncoded;
532
533struct FixedReader<'a> {
535 rest: &'a [u8],
536}
537
538impl FixedReader<'_> {
539 fn take<const W: usize>(&mut self) -> [u8; W] {
540 let (field, rest) =
541 self.rest.split_first_chunk::<W>().expect("a fixed-width encoding holds every field");
542 self.rest = rest;
543 *field
544 }
545
546 fn u8(&mut self) -> u8 {
547 self.take::<1>()[0]
548 }
549
550 fn u64(&mut self) -> u64 {
551 u64::from_le_bytes(self.take())
552 }
553
554 fn bound(&mut self) -> Result<Option<usize>, NotEncoded> {
556 match (self.u8(), self.u64()) {
557 (UNBOUNDED, 0) => Ok(None),
558 (BOUNDED, value) => usize::try_from(value).map(Some).map_err(|_| NotEncoded),
559 _ => Err(NotEncoded),
560 }
561 }
562}
563
564impl EntryTierIdentity {
565 pub(crate) fn encode(self) -> [u8; ENTRY_TIER_BYTES] {
567 let scope = self.scope;
568 let mut flags = 0u8;
569 if scope.follow_symlinks {
570 flags |= SCOPE_FOLLOW_SYMLINKS;
571 }
572 if scope.one_filesystem {
573 flags |= SCOPE_ONE_FILESYSTEM;
574 }
575 if scope.exclude_special {
576 flags |= SCOPE_EXCLUDE_SPECIAL;
577 }
578 let mut out = FixedWriter::new();
579 out.put_bound(scope.max_depth);
580 out.put(&[flags]);
581 out.put(&scope.hidden_fingerprint.to_le_bytes());
582 out.put(&self.type_rules_fingerprint.to_le_bytes());
583 out.put(&self.reducers_fingerprint.to_le_bytes());
584 out.finish()
585 }
586
587 pub(crate) fn decode(engine: u64, bytes: &[u8; ENTRY_TIER_BYTES]) -> Option<Self> {
590 let mut fields = FixedReader { rest: bytes };
591 let max_depth = fields.bound().ok()?;
592 let flags = fields.u8();
593 if flags & !SCOPE_KNOWN_FLAGS != 0 {
594 return None;
595 }
596 let scope = EntryScope {
597 max_depth,
598 follow_symlinks: flags & SCOPE_FOLLOW_SYMLINKS != 0,
599 one_filesystem: flags & SCOPE_ONE_FILESYSTEM != 0,
600 hidden_fingerprint: fields.u64(),
601 exclude_special: flags & SCOPE_EXCLUDE_SPECIAL != 0,
602 };
603 Some(Self {
604 engine,
605 scope,
606 type_rules_fingerprint: fields.u64(),
607 reducers_fingerprint: fields.u64(),
608 })
609 }
610}
611
612impl ControlTierIdentity {
613 pub(crate) fn encode(self) -> [u8; CONTROL_TIER_BYTES] {
615 let mut out = FixedWriter::new();
616 match self {
617 Self::NotObserved => out.put(&[CONTROLS_NOT_OBSERVED; CONTROL_TIER_BYTES]),
618 Self::Observed { limits } => {
619 out.put(&[CONTROLS_OBSERVED]);
620 out.put_bound(limits.budget);
621 out.put_bound(limits.line_limit);
622 }
623 }
624 out.finish()
625 }
626
627 pub(crate) fn decode(bytes: &[u8; CONTROL_TIER_BYTES]) -> Option<Self> {
629 let mut fields = FixedReader { rest: bytes };
630 match fields.u8() {
631 CONTROLS_NOT_OBSERVED => {
632 bytes[1..].iter().all(|byte| *byte == 0).then_some(Self::NotObserved)
633 }
634 CONTROLS_OBSERVED => {
635 let budget = fields.bound().ok()?;
636 let line_limit = fields.bound().ok()?;
637 Some(Self::Observed { limits: ControlLimits { budget, line_limit } })
638 }
639 _ => None,
640 }
641 }
642}
643
644impl SnapshotIdentity {
645 pub(crate) fn encode(self) -> [u8; SNAPSHOT_IDENTITY_BYTES] {
647 let mut out = FixedWriter::new();
648 out.put(&self.entries.encode());
649 out.put(&self.controls.encode());
650 out.finish()
651 }
652
653 pub(crate) fn decode(engine: u64, bytes: &[u8; SNAPSHOT_IDENTITY_BYTES]) -> Option<Self> {
655 let mut fields = FixedReader { rest: bytes };
656 Some(Self {
657 entries: EntryTierIdentity::decode(engine, &fields.take())?,
658 controls: ControlTierIdentity::decode(&fields.take())?,
659 })
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use crate::ScanConfig;
667
668 fn limits(budget: Option<usize>, line_limit: Option<usize>) -> ControlLimits {
669 ControlLimits { budget, line_limit }
670 }
671
672 #[test]
673 fn snapshot_serving_is_equality_plus_observation_on_to_off() {
674 let base = ScanConfig::default().snapshot_identity();
675 assert_eq!(serves_snapshot(base, base), Serves::Exact);
676
677 let entries = base.entries;
678 let mut refused = vec![
679 SnapshotIdentity {
680 entries: EntryTierIdentity { engine: entries.engine ^ 1, ..entries },
681 ..base
682 },
683 SnapshotIdentity {
684 entries: EntryTierIdentity {
685 type_rules_fingerprint: entries.type_rules_fingerprint ^ 1,
686 ..entries
687 },
688 ..base
689 },
690 SnapshotIdentity {
691 entries: EntryTierIdentity {
692 reducers_fingerprint: entries.reducers_fingerprint ^ 1,
693 ..entries
694 },
695 ..base
696 },
697 SnapshotIdentity {
698 controls: ControlTierIdentity::Observed { limits: limits(None, None) },
699 ..base
700 },
701 ];
702 for config in [
703 ScanConfig { max_depth: Some(1), ..ScanConfig::default() },
704 ScanConfig { one_filesystem: true, ..ScanConfig::default() },
705 ScanConfig { exclude_special: true, ..ScanConfig::default() },
706 ScanConfig {
707 hidden: Some(std::sync::Arc::new(crate::HiddenPolicy::prune_hidden(
708 std::iter::empty::<std::ffi::OsString>(),
709 ))),
710 ..ScanConfig::default()
711 },
712 ] {
713 refused.push(config.snapshot_identity());
714 }
715 for wanted in refused {
716 assert_eq!(serves_snapshot(base, wanted), Serves::Refuse, "{wanted:?}");
717 assert_eq!(serves_snapshot(wanted, base), Serves::Refuse, "{wanted:?}");
718 }
719 let blind = SnapshotIdentity { controls: ControlTierIdentity::NotObserved, ..base };
720 assert_eq!(serves_snapshot(base, blind), Serves::ProjectControlsOff);
721 assert_eq!(serves_snapshot(blind, base), Serves::Refuse);
722 }
723
724 #[test]
725 fn control_settings_change_only_the_control_tier() {
726 let base = ScanConfig::default();
727 for config in [
728 ScanConfig { read_controls: false, ..base.clone() },
729 ScanConfig { control_limits: limits(None, Some(1)), ..base.clone() },
730 ScanConfig {
731 read_controls: false,
732 control_limits: limits(Some(1), None),
733 ..base.clone()
734 },
735 ] {
736 assert_eq!(config.snapshot_identity().entries, base.snapshot_identity().entries);
737 assert_ne!(config.snapshot_identity().controls, base.snapshot_identity().controls);
738 }
739 let blind = ScanConfig { read_controls: false, ..base.clone() };
741 let blind_other_limits = ScanConfig { control_limits: limits(None, None), ..blind.clone() };
742 assert_eq!(blind.snapshot_identity(), blind_other_limits.snapshot_identity());
743 assert_eq!(blind.control_identity(), ControlTierIdentity::NotObserved);
744 }
745
746 #[test]
747 fn the_ignore_rules_fingerprint_reserves_zero_for_an_unobserved_tier() {
748 assert_eq!(ControlTierIdentity::NotObserved.ignore_rules_fingerprint(), 0);
749 let defaults = ControlLimits::default();
750 let observed = [
751 defaults,
752 limits(None, defaults.line_limit),
753 limits(defaults.budget, None),
754 limits(None, None),
755 limits(defaults.line_limit, defaults.budget),
757 ]
758 .map(|limits| ControlTierIdentity::Observed { limits }.ignore_rules_fingerprint());
759 for (index, fingerprint) in observed.iter().enumerate() {
760 assert_ne!(*fingerprint, 0);
761 assert!(!observed[index + 1..].contains(fingerprint), "{observed:?}");
762 }
763 }
764
765 #[test]
766 fn the_scope_an_identity_composes_is_the_one_a_scan_records() {
767 for config in [
768 ScanConfig::default(),
769 ScanConfig { read_controls: false, ..ScanConfig::default() },
770 ScanConfig { control_limits: limits(None, None), ..ScanConfig::default() },
771 ScanConfig { max_depth: Some(3), exclude_special: true, ..ScanConfig::default() },
772 ] {
773 let identity = config.snapshot_identity();
774 let scope = identity.scan_scope();
775 assert_eq!(scope, config.scope());
776 assert_eq!(EntryTierIdentity::of_scope(scope), identity.entries);
777 assert_eq!(scope.observes_controls(), identity.controls.is_observed());
778
779 let index = crate::Index::new_with_config("/root", &config);
780 assert_eq!(index.snapshot_identity(), identity);
781 assert_eq!(index.control_identity(), config.control_identity());
782 }
783 }
784
785 #[test]
786 fn each_tier_is_writable_by_its_own_rule() {
787 let config = ScanConfig::default();
788 let entries = config.snapshot_identity().entries;
789 let other =
790 ScanConfig { max_depth: Some(2), ..ScanConfig::default() }.snapshot_identity().entries;
791
792 let mut complete = crate::Index::new_with_config("/root", &config);
793 complete.set_initial_freshness(true);
794 assert!(entries_writable(&complete));
795 for stored in [None, Some(entries), Some(other)] {
796 assert!(
797 content_tier_writable(&complete, || stored),
798 "a complete pass writes: {stored:?}"
799 );
800 }
801
802 let mut partial = crate::Index::new_with_config("/root", &config);
803 partial.set_initial_freshness(false);
804 assert!(!entries_writable(&partial), "an absent entry would change totals");
805 assert!(content_tier_writable(&partial, || Some(entries)));
806 for stored in [None, Some(other)] {
807 assert!(!content_tier_writable(&partial, || stored), "mismatched pair: {stored:?}");
808 }
809
810 let mut unverified = complete.clone();
811 unverified.mark_unverified();
812 assert!(!entries_writable(&unverified), "a cache-only index verified nothing");
813 }
814
815 fn identities() -> Vec<SnapshotIdentity> {
818 let base = ScanConfig::default().snapshot_identity();
819 let entries = base.entries;
820 let defaults = ControlLimits::default();
821 assert_eq!(entries.scope.max_depth, None);
822 assert!(defaults.budget.is_some() && defaults.line_limit.is_some());
823 let mut all = vec![base];
824 for scope in [
825 EntryScope { max_depth: Some(0), ..entries.scope },
826 EntryScope { max_depth: Some(usize::MAX), ..entries.scope },
828 EntryScope { follow_symlinks: true, ..entries.scope },
829 EntryScope { one_filesystem: true, ..entries.scope },
830 EntryScope { exclude_special: true, ..entries.scope },
831 EntryScope { hidden_fingerprint: u64::MAX, ..entries.scope },
832 ] {
833 all.push(SnapshotIdentity { entries: EntryTierIdentity { scope, ..entries }, ..base });
834 }
835 for entries in [
836 EntryTierIdentity { type_rules_fingerprint: 0, ..entries },
837 EntryTierIdentity { reducers_fingerprint: u64::MAX, ..entries },
838 ] {
839 all.push(SnapshotIdentity { entries, ..base });
840 }
841 for controls in [
842 ControlTierIdentity::NotObserved,
843 ControlTierIdentity::Observed { limits: limits(None, defaults.line_limit) },
844 ControlTierIdentity::Observed { limits: limits(Some(0), defaults.line_limit) },
845 ControlTierIdentity::Observed { limits: limits(defaults.budget, None) },
846 ControlTierIdentity::Observed { limits: limits(defaults.budget, Some(usize::MAX)) },
847 ] {
848 all.push(SnapshotIdentity { controls, ..base });
849 }
850 all
851 }
852
853 #[test]
854 fn every_identity_round_trips_through_its_fixed_width_encoding() {
855 let identities = identities();
856 let encoded = identities.iter().map(|identity| identity.encode()).collect::<Vec<_>>();
857 for (identity, bytes) in identities.iter().zip(&encoded) {
858 let engine = identity.entries.engine;
859 assert_eq!(SnapshotIdentity::decode(engine, bytes), Some(*identity));
860 let (entry_bytes, control_bytes) = bytes.split_at(ENTRY_TIER_BYTES);
861 assert_eq!(
862 EntryTierIdentity::decode(engine, entry_bytes.try_into().expect("width")),
863 Some(identity.entries)
864 );
865 assert_eq!(
866 ControlTierIdentity::decode(control_bytes.try_into().expect("width")),
867 Some(identity.controls)
868 );
869 }
870 for (index, bytes) in encoded.iter().enumerate() {
872 assert!(!encoded[index + 1..].contains(bytes), "{:?}", identities[index]);
873 }
874 }
875
876 #[test]
877 fn bytes_no_encoder_writes_are_refused() {
878 let base = ScanConfig::default().snapshot_identity();
879 let engine = base.entries.engine;
880 let bounded = EntryTierIdentity {
881 scope: EntryScope { max_depth: Some(3), ..base.entries.scope },
882 ..base.entries
883 };
884 let (depth_tag_at, depth_at, flags_at) = (0, 1, BOUND_BYTES);
885 let mut forged_entries = Vec::new();
886 let mut unknown_flag = base.entries.encode();
887 unknown_flag[flags_at] |= 1 << 7;
888 forged_entries.push(unknown_flag);
889 let mut unknown_depth_tag = bounded.encode();
890 assert_eq!(unknown_depth_tag[depth_tag_at], BOUNDED);
891 unknown_depth_tag[depth_tag_at] = 2;
892 forged_entries.push(unknown_depth_tag);
893 let mut unbounded_depth_with_a_value = base.entries.encode();
894 assert_eq!(unbounded_depth_with_a_value[depth_tag_at], UNBOUNDED);
895 unbounded_depth_with_a_value[depth_at] = 1;
896 forged_entries.push(unbounded_depth_with_a_value);
897 for bytes in forged_entries {
898 assert_eq!(EntryTierIdentity::decode(engine, &bytes), None, "{bytes:?}");
899 }
900
901 let observed = ControlTierIdentity::Observed { limits: limits(None, Some(1)) };
902 let controls = observed.encode();
903 let (budget_tag_at, budget_at, line_tag_at) = (1, 2, 1 + BOUND_BYTES);
904 assert_eq!(controls[budget_tag_at], UNBOUNDED);
905 assert_eq!(controls[line_tag_at], BOUNDED);
906 let mut forged = Vec::new();
907 let mut unknown_tag = controls;
908 unknown_tag[0] = 2;
909 forged.push(unknown_tag);
910 let mut unknown_limit_tag = controls;
911 unknown_limit_tag[line_tag_at] = 2;
912 forged.push(unknown_limit_tag);
913 let mut unbounded_with_a_value = controls;
914 unbounded_with_a_value[budget_at] = 1;
915 forged.push(unbounded_with_a_value);
916 let mut unobserved_with_limits = controls;
917 unobserved_with_limits[0] = CONTROLS_NOT_OBSERVED;
918 forged.push(unobserved_with_limits);
919 for bytes in forged {
920 assert_eq!(ControlTierIdentity::decode(&bytes), None, "{bytes:?}");
921 }
922 }
923
924 #[test]
927 fn a_content_tier_holds_its_records_type_rules_in_its_entry_tier() {
928 use crate::content::AnalysisRequest;
929
930 let entries = ScanConfig::default().snapshot_identity().entries;
931 let request = AnalysisRequest { profile: AnalysisSet::ALL, ..AnalysisRequest::default() };
932 let records = ContentProvenance::for_request(request, entries.type_rules_fingerprint);
933 let identity = ContentTierIdentity::for_request(entries, request.profile);
934 assert_eq!(identity.record_provenance(), records);
935 assert!(identity.admit_record(request.profile, &records).is_some());
936
937 let other_rules = ContentProvenance::for_request(request, !entries.type_rules_fingerprint);
938 assert!(identity.admit_record(request.profile, &other_rules).is_none(), "other type rules");
939 let lines = AnalysisSet::NONE.with_lines();
940 assert!(identity.admit_record(lines, &records).is_none(), "another analyzer set's label");
941 }
942
943 #[test]
944 fn content_admission_refuses_every_identity_difference_and_applies_exact_identity() {
945 let entries = ScanConfig::default().snapshot_identity().entries;
946 let wanted = ContentTierIdentity::for_request(entries, AnalysisSet::ALL);
947 assert_eq!(wanted.admit(&wanted).expect("exact admission").identity(), &wanted);
948 let changes: &[fn(&mut ContentTierIdentity)] = &[
949 |identity| identity.entries.engine ^= 1,
950 |identity| identity.entries.scope.max_depth = Some(1),
951 |identity| identity.entries.type_rules_fingerprint ^= 1,
952 |identity| identity.entries.reducers_fingerprint ^= 1,
953 |identity| identity.analysis = AnalysisSet::LINES_ONLY,
954 |identity| identity.provenance.options_fingerprint.0 ^= 1,
955 |identity| identity.provenance.analyzers[0].1.0 += 1,
956 |identity| {
957 identity.provenance.analyzers.pop();
958 },
959 ];
960 for change in changes {
961 let mut other = wanted.clone();
962 change(&mut other);
963 assert!(wanted.admit(&other).is_none(), "stored mismatch: {other:?}");
964 assert!(other.admit(&wanted).is_none(), "requested mismatch: {other:?}");
965 }
966 }
967
968 #[test]
969 fn the_engine_fingerprint_comes_from_the_store_not_the_encoding() {
970 let identity = ScanConfig::default().snapshot_identity();
971 let bytes = identity.encode();
972 let other = SnapshotIdentity::decode(!identity.entries.engine, &bytes).expect("decode");
973 assert_eq!(other.entries.engine, !identity.entries.engine);
974 assert_eq!(serves_snapshot(other, identity), Serves::Refuse);
975 }
976}