Skip to main content

fdu_core/
stored_state.rs

1//! Stored-state identity: which requests a stored tier may answer.
2//!
3//! A **tier** is a unit of identity: entries and their roll-ups, `.gitignore` control
4//! state, and content records. A **store** holds one or more tiers: a metadata snapshot
5//! holds the entry and control tiers, a content sidecar holds the content tier, and a
6//! retained [`Index`](crate::Index) holds all three. A store records the identity of every
7//! tier it holds, and a stored tier answers a request only when the request's identity
8//! for that tier is one it serves.
9//!
10//! Each identity is the engine fingerprint plus exactly the request parts that change the
11//! tier's values. Operational settings such as worker counts, batch sizes, and scan order
12//! never appear here, so they can never invalidate a store, and a request part that
13//! changes a tier's values always does.
14
15use crate::content::{
16    AnalysisSet, AnalyzerId, AnalyzerVersion, ContentProvenance, OptionsFingerprint,
17};
18use crate::control::ControlLimits;
19use crate::engine_contract::ScanScope;
20use crate::query::IgnoredEntries;
21
22/// Version of the fixed `.gitignore` control semantics, the first thing
23/// [`ControlTierIdentity::ignore_rules_fingerprint`] hashes.
24const IGNORE_RULES_VERSION: u64 = 2;
25
26/// Which entries a scan retains: its depth, symlink, filesystem-boundary, hidden-entry,
27/// and special-object settings.
28///
29/// This is [`ScanScope`] without its type-rules, reducer-set, and ignore-rules
30/// fingerprints: the filesystem-admission identity of a validated scan configuration. A
31/// store's entry tier records it, and an opened root binds it in
32/// [`EngineVersion::scope`](crate::EngineVersion::scope). Root binding and execution policy
33/// are deliberately absent, so two roots may share it without claiming to be the same live
34/// session.
35#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
36pub struct EntryScope {
37    /// Maximum retained relative depth, or unlimited when absent.
38    pub max_depth: Option<usize>,
39    /// Whether directory symlinks are followed.
40    pub follow_symlinks: bool,
41    /// Whether traversal stays on the root filesystem.
42    pub one_filesystem: bool,
43    /// Identity of leading-dot component admission and its exact-name allowlist.
44    pub hidden_fingerprint: u64,
45    /// Whether filesystem objects outside files, directories, and symlinks are excluded.
46    pub exclude_special: bool,
47    /// Ignored population that shaped retained entries and content candidates.
48    pub population: IgnoredEntries,
49    /// Governing control policy when population narrows retained facts; zero for Include.
50    pub control_fingerprint: u64,
51}
52
53/// Identity of an entry tier: the entries a store holds and the roll-ups derived from
54/// them.
55///
56/// Include populations have equal entry tiers with or without `.gitignore`
57/// observation, since classification alone does not change retained entries. Narrowed
58/// populations store the governing control policy in `scope`, because those rules
59/// determine which entries exist in this tier.
60#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
61pub struct EntryTierIdentity {
62    /// The engine fingerprint of the build that produced the tier
63    /// ([`crate::snapshot::engine_fingerprint`]).
64    pub engine: u64,
65    /// Which entries a scan retains.
66    pub scope: EntryScope,
67    /// Identity of the type-classification rules roll-ups were tallied under.
68    pub type_rules_fingerprint: u64,
69    /// Identity of the enabled reducer set.
70    pub reducers_fingerprint: u64,
71}
72
73impl EntryTierIdentity {
74    /// The entry tier identity this build gives an index of `scope`.
75    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/// Identity of a `.gitignore` control tier.
86#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
87pub enum ControlTierIdentity {
88    /// No control file was read and no entry was classified, so the tier cannot say
89    /// whether any entry is ignored.
90    NotObserved,
91    /// Control files were read and admitted under `limits`.
92    Observed {
93        /// The budget and line limit that decided which control files apply.
94        limits: ControlLimits,
95    },
96}
97
98impl ControlTierIdentity {
99    /// Whether the tier observed control state.
100    pub const fn is_observed(self) -> bool {
101        matches!(self, Self::Observed { .. })
102    }
103
104    /// The ignore-rules fingerprint a [`ScanScope`] with this control tier carries.
105    ///
106    /// Zero is reserved for [`Self::NotObserved`], which is what
107    /// [`ScanScope::observes_controls`] tests. An observed tier hashes the control
108    /// semantics version and each limit in turn with FNV-1a and is never zero, so a scope
109    /// taken under one budget or line limit never matches one taken under another.
110    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/// The identity of every tier a metadata snapshot holds.
141#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
142pub struct SnapshotIdentity {
143    /// The entry tier.
144    pub entries: EntryTierIdentity,
145    /// The `.gitignore` control tier.
146    pub controls: ControlTierIdentity,
147}
148
149impl SnapshotIdentity {
150    /// The scope an index holding these tiers records.
151    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/// Identity of a content tier: the per-file analysis records a sidecar holds.
176///
177/// The entry tier's identity rather than the whole snapshot's: Include observes
178/// controls only for selection, while narrowed entry tiers carry the governing
179/// control policy in their own identity. Then the analyzer set the records were
180/// produced for, and the analyzers' identities, versions, and options.
181///
182/// The type rules the records were classified under are the entry tier's, and stated only
183/// there: a record's [`ContentProvenance`] is this identity's entry-tier type rules and its
184/// [`AnalyzerProvenance`].
185#[derive(Clone, PartialEq, Eq, Debug)]
186pub struct ContentTierIdentity {
187    /// The entry tier the records were analyzed over, including their type rules.
188    pub entries: EntryTierIdentity,
189    /// The analyzer set the tier holds records for.
190    pub analysis: AnalysisSet,
191    /// The options and analyzer versions the records were produced under.
192    pub provenance: AnalyzerProvenance,
193}
194
195/// The analyzers a content tier's records were produced by, and the options they ran with.
196///
197/// A record's [`ContentProvenance`] without its type-rules fingerprint, which the content
198/// tier's [`EntryTierIdentity`] holds.
199#[derive(Clone, PartialEq, Eq, Debug, Hash)]
200pub struct AnalyzerProvenance {
201    /// Identity of the semantic analyzer options.
202    pub options_fingerprint: OptionsFingerprint,
203    /// Each analyzer the records ran, with its version, in the order the set enables them.
204    pub analyzers: Vec<(AnalyzerId, AnalyzerVersion)>,
205}
206
207impl ContentTierIdentity {
208    /// The provenance each record of this tier carries.
209    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    /// Identity produced by this build for the requested entry tier and analyzers.
218    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    /// Admit a stored content identity and return the projection that may consume it.
234    ///
235    /// Equality is the only lawful content projection today. Adding another relation
236    /// requires implementing its record and tier projection here, not widening a reader.
237    #[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    /// Admission for an observation already tied to this entry tier by its candidate.
262    /// This compares borrowed components, without allocating provenance per file.
263    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/// Proof that stored content can be projected to one requested identity.
281///
282/// The private constructor is the shared admission relation. Consumers apply this proof
283/// to records or a borrowed record set; the proof never widens the requested analyzer set.
284#[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    /// The identity the projected content answers.
292    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/// A record set projected through the content serving relation for one request.
312#[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/// A decoded record whose identity and analyzer slots have passed admission.
338#[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/// How a stored tier answers a request.
362#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
363pub enum Serves {
364    /// The stored identity equals the requested one, so the stored tier holds what a cold
365    /// run of the request would build.
366    Exact,
367    /// The stored entry tier equals the requested one, and an observed control tier can
368    /// be discarded to produce the controls-off index a cold run would build.
369    ProjectControlsOff,
370    /// The stored tier cannot answer the request, which is a miss.
371    Refuse,
372}
373
374/// Whether a snapshot of the `stored` identity answers a request for `wanted`.
375///
376/// Equality: a snapshot serves exactly the request whose identity for every tier equals
377/// its own. Any relation beyond equality arrives with a projection that yields what a cold
378/// run of `wanted` would, and is proven by its own test.
379pub 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
392// ---- write rules ----
393//
394// Each tier is written by what an absent item in it means. An absent entry changes every
395// roll-up above it, so the entry tier is written only when the pass verified all of it.
396// An absent content record is a miss that reads the file again, so records are written one
397// at a time, each when the pass verified it.
398
399/// Whether `index`'s entry tier, and the control tier stored with it, may be written.
400///
401/// Only a complete, fresh index: a snapshot missing an entry would be served as the tree's
402/// totals on the next run, and an older complete snapshot is better than that.
403pub(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
408/// Whether the content record `record` for the file at `path` may be written.
409///
410/// A record is written when it describes a file this pass verified: the index holds a
411/// regular file there whose fingerprint is the record's, the entry was scanned or
412/// revalidated by the pass rather than retained from a snapshot, and reading it did not
413/// fail. A file the pass verified was listed by its parent, so its subtree was verified
414/// down to it. A record under a subtree the pass could not verify describes a retained
415/// file nobody checked, so it is left out, and the next run that verifies the file reads
416/// it again.
417pub(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    // A complete, fresh pass verified every entry, and the content tier holds only records
426    // that match their live entry, because a metadata change invalidates a file's record
427    // and a commit checks the entry it lands on. So only a partial pass asks per file, and
428    // the common write pays no lookup per record.
429    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
440/// Whether `index`'s content tier may be written beside the store that holds
441/// `stored_entries`, the entry tier of the snapshot already stored for its root, if any.
442///
443/// After a complete pass, always: the snapshot is written with it. After a partial pass,
444/// only beside a snapshot of the same entry tier, which the sidecar pairs with, because a partial
445/// pass under another identity writes no snapshot, so replacing the sidecar would evict the
446/// records that pair with the snapshot that stays.
447pub(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
455// ---- fixed-width codecs ----
456//
457// Every store writes its tier identities in these encodings. Each is fixed-width and
458// canonical: one identity has exactly one encoding, and a decoder refuses any byte that no
459// encoder writes, so two stores hold equal identities exactly when their encoded bytes are
460// equal. The engine fingerprint is not encoded here: a store writes it once, in its
461// prologue beside the magic and format version, and every tier identity it holds shares
462// it.
463
464/// Encoded width of an optional bound: a tag, then eight value bytes.
465pub(crate) const BOUND_BYTES: usize = 1 + 8;
466
467/// Encoded width of an [`EntryTierIdentity`] after its engine fingerprint: the maximum
468/// depth as a bound, scope flags, and the hidden-entry, governing-control, type-rules,
469/// and reducer-set fingerprints.
470pub(crate) const ENTRY_TIER_BYTES: usize = BOUND_BYTES + 1 + 8 + 8 + 8 + 8;
471
472/// Encoded width of a [`ControlTierIdentity`]: the observation tag, then the budget and the
473/// line limit, each a bound.
474pub(crate) const CONTROL_TIER_BYTES: usize = 1 + 2 * BOUND_BYTES;
475
476/// Encoded width of a [`SnapshotIdentity`] after its engine fingerprint.
477pub(crate) const SNAPSHOT_IDENTITY_BYTES: usize = ENTRY_TIER_BYTES + CONTROL_TIER_BYTES;
478
479/// Scope flag for symlink-following traversal.
480const SCOPE_FOLLOW_SYMLINKS: u8 = 1 << 0;
481/// Scope flag for staying on the root filesystem.
482const SCOPE_ONE_FILESYSTEM: u8 = 1 << 1;
483/// Scope flag for excluding native special objects.
484const SCOPE_EXCLUDE_SPECIAL: u8 = 1 << 2;
485const SCOPE_POPULATION_EXCLUDE: u8 = 1 << 3;
486const SCOPE_POPULATION_ONLY: u8 = 1 << 4;
487/// Every scope flag this encoding defines.
488const SCOPE_KNOWN_FLAGS: u8 = SCOPE_FOLLOW_SYMLINKS
489    | SCOPE_ONE_FILESYSTEM
490    | SCOPE_EXCLUDE_SPECIAL
491    | SCOPE_POPULATION_EXCLUDE
492    | SCOPE_POPULATION_ONLY;
493
494/// Control tier tag for a tier that observed nothing, whose limit fields are all zero.
495const CONTROLS_NOT_OBSERVED: u8 = 0;
496/// Control tier tag for an observed tier, whose limit fields follow.
497const CONTROLS_OBSERVED: u8 = 1;
498
499/// Bound tag for no bound, whose eight value bytes are zero.
500const UNBOUNDED: u8 = 0;
501/// Bound tag for a bound, whose value is the eight bytes that follow.
502const BOUNDED: u8 = 1;
503
504// Every bound is a `usize`, which fits the eight bytes that encode it on every target, so a
505// bound is never refused or aliased at encode.
506const _: () = assert!(usize::BITS <= u64::BITS, "a bound fits its eight encoded bytes");
507
508/// Fills a fixed-width encoding field by field.
509struct 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    /// Write an optional bound as its tag and eight value bytes, zero when unbounded.
526    ///
527    /// A tag rather than a reserved value, so no bound can be mistaken for a sentinel.
528    fn put_bound(&mut self, bound: Option<usize>) {
529        let (tag, value) = match bound {
530            None => (UNBOUNDED, 0),
531            // Lossless: the width assertion above holds on every target this compiles for.
532            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
544/// A field holds bytes no encoder writes.
545struct NotEncoded;
546
547/// Reads a fixed-width encoding field by field.
548struct 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    /// Read [`FixedWriter::put_bound`], refusing a tag or value no encoder writes.
569    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    /// Encode every field but the engine fingerprint, which the store's prologue carries.
580    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    /// Decode [`Self::encode`] under the engine fingerprint of the store that holds it, or
608    /// `None` for a field no encoder writes.
609    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    /// Encode the observation and, when observed, both limits.
649    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    /// Decode [`Self::encode`], or `None` for a tag or value no encoder writes.
663    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    /// Encode the entry tier's fields, then the control tier.
681    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    /// Decode [`Self::encode`] under the engine fingerprint of the snapshot that holds it.
689    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        // Limits decide nothing when nothing is observed, so they leave no trace.
781        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            // The same values in each other's places are a different tier.
797            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    /// Identities that each differ from the default in one encoded field, including the
859    /// edges of each range, so a codec that dropped any one field would alias two of them.
860    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            // The largest bound is a bound, never the unbounded depth.
882            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        // Canonical: distinct identities never share an encoding.
926        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    /// A content tier states its type rules once, in its entry tier, and every record it
989    /// holds carries exactly those rules.
990    #[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}