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