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