Skip to main content

git_policy_cli/
policy_content.rs

1//! What:
2//!  What one lifecycle offers the content policies,
3//!  and the candidates it prepared
4//!       for them,
5//!  read once and shared by every content policy of the invocation.
6//! Why:
7//!  A forwarded command other than `git add` has no candidates,
8//!  so its content
9//!      policies find nothing.
10//!  `git add` and the direct commands have candidates,
11//!  which
12//!      are prepared the first time a content policy reads them:
13//!  an invocation whose
14//!      content policies are all off or escaped starts no Git process for them.
15//!  A failure
16//!      to prepare is remembered,
17//!  so it is reported once and never retried.
18//!
19//! In TS you'd write (pseudocode):
20//! ```ts
21//! // const version = await content.version(lifecycle, facts); for (const c of version.candidates) await content.bytes(c);
22//! ```
23
24/// Import the layer's failure type and its causes.
25use super::candidate_error::{CandidateError, CandidateFailure};
26/// Import the candidate's mode,
27///  which a correction keeps.
28use super::candidate_object::CandidateMode;
29/// Import what a lifecycle asks for and what preparing it yields.
30use super::candidate_prediction::{CandidateRequest, PreparedCandidates};
31/// Import the store the overlay reads through.
32use super::candidate_store::CandidateStore;
33/// Import the candidate and the immutable version type.
34use super::candidate_version::{Candidate, CandidateVersion};
35/// Import the change a converged fix installs,
36///  which also records a corrected file.
37use super::direct_fix_install::InstallChange;
38/// Import the outcome a check returns.
39use super::policy_engine::PolicyOutcome;
40/// Import the facts interface that prepares candidates.
41use super::repository_facts::RepositoryFacts;
42/// Import the scanner adapter.
43use super::scanner_adapter::CandidateScanner;
44/// Import the code of each candidate failure.
45use super::scanner_failure_code::candidate_failure_code;
46/// Import the scan pass,
47///  its byte reader and its failure.
48use super::scanner_run::{CandidateBytes, ScanRunError, scan_version};
49/// The scanner library's per-candidate result.
50use forbidden_strings::CandidateScan;
51/// What:
52///  `BTreeMap<K, V>` is a map kept sorted by key (sibling `HashMap` is unordered).
53/// Why:
54///   Corrected files are keyed by pathname bytes,
55///  and their sorted order is Git's
56///       byte order,
57///  the order a fix summary lists them in.
58///
59/// In TS you'd write (pseudocode):
60/// ```ts
61/// // Map<string, Uint8Array>, iterated in sorted key order.
62/// ```
63use std::collections::BTreeMap;
64/// `Path` is a borrowed filesystem path.
65use std::path::Path;
66/// `Rc<T>` is a shared,
67///  read-only handle;
68///  versions and bytes are shared,
69///  not copied.
70use std::rc::Rc;
71
72/// What:
73///  What the current lifecycle offers a content policy.
74///  An `enum` is a closed set
75///       of named alternatives;
76///  `Requested` carries what to prepare.
77///  `#[derive(...)]`
78///       generates cloning,
79///  debug printing and `==`.
80/// Why:
81///   "There are no files to check" and "there are files,
82///  prepared on demand" must
83///       never be confused:
84///  the first is clean,
85///  the second must be read.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// type LifecycleContent = { kind: 'none' } | { kind: 'requested'; request: CandidateRequest };
90/// ```
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub enum LifecycleContent {
93    /// The lifecycle has no candidate content,
94    ///  as for a forwarded command other than `git add`.
95    None,
96    /// The lifecycle has candidates;
97    ///  they are prepared when a content policy first reads them.
98    Requested(
99        /// What to prepare.
100        CandidateRequest,
101    ),
102}
103
104/// What:
105///  The candidates of one invocation once prepared,
106///  or the remembered failure.
107///       `Option<Result<..>>` is "not yet asked",
108///  "prepared" or "failed".
109/// Why:
110///   Every content policy of the invocation reads the same version through one store.
111///
112/// In TS you'd write (pseudocode):
113/// ```ts
114/// class ContentState { #prepared?: PreparedCandidates | CandidateError }
115/// ```
116pub struct ContentState {
117    /// The prepared candidates,
118    ///  or the failure of the one attempt to prepare them.
119    prepared: Option<Result<PreparedCandidates, CandidateError>>,
120    /// Each corrected file by pathname:
121    ///  its mode,
122    ///  original bytes and corrected bytes.
123    ///  Every other file keeps its object's bytes.
124    corrected: BTreeMap<Vec<u8>, InstallChange>,
125    /// The corrections policies proposed in the current pass,
126    ///  in proposal order.
127    proposals: Vec<Correction>,
128}
129
130/// What:
131///  One proposed full-content correction:
132///  the file,
133///  the bytes it held when the policy
134///       read it,
135///  and the bytes it should hold.
136///  `#[derive(...)]` generates cloning,
137///  debug
138///       printing and `==`.
139/// Why:
140///   A correction replaces a file's whole content,
141///  so applying it needs no patch
142///       format;
143///  the bytes it replaces are kept so the original of a file is known
144///       without reading it again.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// type Correction = { path: Uint8Array; mode: CandidateMode; before: Uint8Array; after: Uint8Array };
149/// ```
150#[derive(Clone, Debug, Eq, PartialEq)]
151pub struct Correction {
152    /// The pathname,
153    ///  relative to the repository's top level,
154    ///  as Git's raw bytes.
155    pub path: Vec<u8>,
156    /// The file's mode,
157    ///  which the correction keeps.
158    pub mode: CandidateMode,
159    /// The bytes the file held when the policy read it.
160    pub before: Rc<[u8]>,
161    /// The bytes the file should hold.
162    pub after: Rc<[u8]>,
163}
164
165/// What:
166///  The prepared store with the corrections laid over it,
167///  as a scan's byte reader.
168///       `'a` names how long both borrows last.
169/// Why:
170///   A later pass of a direct fix scans the corrected bytes,
171///  not the objects.
172///
173/// In TS you'd write (pseudocode):
174/// ```ts
175/// const reader: CandidateBytes = { bytes: (c) => corrected.get(c.path) ?? store.bytes(c) };
176/// ```
177struct OverlaidStore<'a> {
178    /// The store bound to the private index.
179    store: &'a mut CandidateStore,
180    /// The corrected files,
181    ///  by pathname.
182    corrected: &'a BTreeMap<Vec<u8>, InstallChange>,
183}
184
185/// What:
186///  `impl CandidateBytes for OverlaidStore<'_>` reads corrected bytes first.
187/// Why:
188///   The one byte rule every reader of a direct fix shares.
189///
190/// In TS you'd write (pseudocode):
191/// ```ts
192/// class OverlaidStore implements CandidateBytes {}
193/// ```
194impl CandidateBytes for OverlaidStore<'_> {
195    /// What:
196    ///  The corrected bytes of the candidate's file,
197    ///  or its object's bytes.
198    /// Why:
199    ///   See `overlaid_bytes`.
200    ///
201    /// In TS you'd write (pseudocode):
202    /// ```ts
203    /// bytes(candidate) { return overlaidBytes(this.store, this.corrected, candidate); }
204    /// ```
205    fn candidate_bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, CandidateError> {
206        return overlaid_bytes(self.store, self.corrected, candidate);
207    }
208}
209
210/// What:
211///  The corrected bytes of a candidate's file when a correction gave it some,
212///  else
213///       the bytes of its object through the store.
214/// Why:
215///   Every policy of a later pass must read what the earlier passes made of the file.
216///
217/// In TS you'd write (pseudocode):
218/// ```ts
219/// const overlaidBytes = (store, corrected, candidate) => corrected.get(candidate.path) ?? store.bytes(candidate);
220/// ```
221fn overlaid_bytes(
222    store: &mut CandidateStore,
223    corrected: &BTreeMap<Vec<u8>, InstallChange>,
224    candidate: &Candidate,
225) -> Result<Rc<[u8]>, CandidateError> {
226    // `if let Some(found) = ...get(..)` runs only when the file was corrected.
227    if let Some(found) = corrected.get(&candidate.path) {
228        return Ok(Rc::clone(&found.replacement));
229    }
230    return store.bytes(candidate);
231}
232
233/// What:
234///  The outcome of a content policy whose candidates could not be prepared or read.
235///       `&CandidateError` borrows the failure;
236///  its code follows its cause.
237/// Why:
238///   Every cause but a sequencing defect is unreadable content;
239///  the message is the
240///       layer's own,
241///  which never contains a candidate's pathname.
242///
243/// In TS you'd write (pseudocode):
244/// ```ts
245/// const candidateFailure = (error: CandidateError): PolicyOutcome => ({ kind: 'failed', code: candidateFailureCode(error.failure), message: error.message });
246/// ```
247pub fn candidate_failure(error: &CandidateError) -> PolicyOutcome {
248    return PolicyOutcome::Failed {
249        code: candidate_failure_code(error.failure),
250        // `.clone()` copies the message the remembered failure keeps.
251        message: error.message.clone(),
252        // `None`: the layer's failures name no candidate.
253        path: None,
254    };
255}
256
257/// What:
258///  `impl ContentState { ... }` attaches the constructor and the two reads.
259/// Why:
260///   Preparation and reading stay behind one owner,
261///  so no policy can read bytes of a
262///       version it did not get from here.
263///
264/// In TS you'd write (pseudocode):
265/// ```ts
266/// class ContentState { version(lifecycle, facts) {} bytes(candidate) {} }
267/// ```
268impl ContentState {
269    /// What:
270    ///  A state that has prepared nothing.
271    /// Why:
272    ///   Preparation waits until a content policy reads.
273    ///
274    /// In TS you'd write (pseudocode):
275    /// ```ts
276    /// constructor() {}
277    /// ```
278    pub fn new() -> ContentState {
279        // `None` is the "absent" variant: nothing has been asked; nothing is corrected yet.
280        return ContentState {
281            prepared: None,
282            corrected: BTreeMap::new(),
283            proposals: Vec::new(),
284        };
285    }
286
287    /// What:
288    ///  The candidate version,
289    ///  prepared on first use;
290    ///  `Ok(None)` when the lifecycle
291    ///       has no candidates.
292    ///  `<F: RepositoryFacts>` accepts any facts provider;
293    ///       `Err(error)` is the candidate layer's failure,
294    ///  remembered for later calls.
295    /// Why:
296    ///   A direct command prepares its scope before any policy runs,
297    ///  as the installed
298    ///       wrapper does,
299    ///  and reports a failure as a lifecycle failure of its own.
300    ///
301    /// In TS you'd write (pseudocode):
302    /// ```ts
303    /// async prepare(lifecycle: LifecycleContent, facts: RepositoryFacts): Promise<CandidateVersion | undefined>; // throws CandidateError
304    /// ```
305    pub fn prepare<F: RepositoryFacts>(
306        &mut self,
307        lifecycle: &LifecycleContent,
308        facts: &mut F,
309    ) -> Result<Option<Rc<CandidateVersion>>, CandidateError> {
310        let LifecycleContent::Requested(request) = lifecycle else {
311            return Ok(None);
312        };
313        if self.prepared.is_none() {
314            // `Some(...)` stores the one attempt's result, success or failure.
315            self.prepared = Some(facts.candidates(request));
316        }
317        // `match` on the remembered attempt; `&self.prepared` borrows it read-only.
318        match &self.prepared {
319            Some(Ok(prepared)) => return Ok(Some(Rc::clone(&prepared.version))),
320            // `.clone()` copies the remembered failure for this caller.
321            Some(Err(error)) => return Err(error.clone()),
322            None => return Err(not_prepared_error()),
323        }
324    }
325
326    /// What:
327    ///  The candidate version for a content policy:
328    ///  as `prepare`,
329    ///  with a failure
330    ///       turned into the outcome the policy returns as it is.
331    /// Why:
332    ///   The first content policy that reads pays for the preparation;
333    ///  later ones
334    ///       share it,
335    ///  and a failure is reported the same way to each.
336    ///
337    /// In TS you'd write (pseudocode):
338    /// ```ts
339    /// async version(lifecycle: LifecycleContent, facts: RepositoryFacts): Promise<CandidateVersion | undefined>; // throws PolicyOutcome
340    /// ```
341    pub fn version<F: RepositoryFacts>(
342        &mut self,
343        lifecycle: &LifecycleContent,
344        facts: &mut F,
345    ) -> Result<Option<Rc<CandidateVersion>>, PolicyOutcome> {
346        match self.prepare(lifecycle, facts) {
347            Ok(found) => return Ok(found),
348            Err(error) => return Err(candidate_failure(&error)),
349        }
350    }
351
352    /// What:
353    ///  The bytes of a candidate of the prepared version,
354    ///  with corrections applied.
355    /// Why:
356    ///   Bytes come from the store bound to the private index,
357    ///  never the live worktree,
358    ///       unless a correction of this invocation replaced them.
359    ///
360    /// In TS you'd write (pseudocode):
361    /// ```ts
362    /// async bytes(candidate: Candidate): Promise<Uint8Array>;
363    /// ```
364    pub fn bytes(&mut self, candidate: &Candidate) -> Result<Rc<[u8]>, PolicyOutcome> {
365        // `&mut self.prepared` lends the remembered attempt for reading through its store.
366        match &mut self.prepared {
367            Some(Ok(prepared)) => {
368                match overlaid_bytes(&mut prepared.store, &self.corrected, candidate) {
369                    Ok(read) => return Ok(read),
370                    Err(error) => return Err(candidate_failure(&error)),
371                }
372            }
373            Some(Err(_)) | None => return Err(candidate_failure(&not_prepared_error())),
374        }
375    }
376
377    /// What:
378    ///  The real index the prepared candidates were copied from,
379    ///  once prepared.
380    /// Why:
381    ///   A direct fix proves that installing its corrections left that index unchanged.
382    ///
383    /// In TS you'd write (pseudocode):
384    /// ```ts
385    /// get realIndexPath(): string | undefined;
386    /// ```
387    pub fn real_index(&self) -> Option<&Path> {
388        // `match &self.prepared` borrows the remembered attempt read-only.
389        match &self.prepared {
390            Some(Ok(prepared)) => return Some(prepared.real_index.as_path()),
391            Some(Err(_)) | None => return None,
392        }
393    }
394}
395
396/// What:
397///  `impl ContentState { ... }` continued:
398///  corrections of a fixing lifecycle.
399/// Why:
400///   A policy proposes,
401///  the fix loop applies after the pass,
402///  and later passes read
403///       the corrected bytes;
404///  nothing reaches the worktree until the fix installs them.
405///
406/// In TS you'd write (pseudocode):
407/// ```ts
408/// class ContentState { propose(c) {} takeProposals() {} apply(cs) {} }
409/// ```
410impl ContentState {
411    /// What:
412    ///  Record one correction a policy proposes in the current pass.
413    /// Why:
414    ///   Corrections wait until the pass ends,
415    ///  so every policy of the pass reads the
416    ///       same bytes.
417    ///
418    /// In TS you'd write (pseudocode):
419    /// ```ts
420    /// propose(correction: Correction): void;
421    /// ```
422    pub fn propose(&mut self, correction: Correction) {
423        self.proposals.push(correction);
424    }
425
426    /// What:
427    ///  Hand over the current pass's proposals and forget them.
428    ///       `std::mem::take` moves the list out and leaves an empty one behind.
429    /// Why:
430    ///   Each pass starts with no proposals,
431    ///  and a proposal is applied at most once.
432    ///
433    /// In TS you'd write (pseudocode):
434    /// ```ts
435    /// takeProposals(): Correction[];
436    /// ```
437    pub fn take_proposals(&mut self) -> Vec<Correction> {
438        return std::mem::take(&mut self.proposals);
439    }
440
441    /// What:
442    ///  Apply corrections in order.
443    ///  A file whose correction restores its original
444    ///       bytes is no longer corrected.
445    /// Why:
446    ///   The corrected map then equals another state's map exactly when both states
447    ///       hold the same bytes in every file,
448    ///  which the fix loop's cycle check needs.
449    ///  A
450    ///       file's original is the bytes its first correction replaced;
451    ///  a file restored
452    ///       and corrected again replaces those same bytes.
453    ///
454    /// In TS you'd write (pseudocode):
455    /// ```ts
456    /// apply(corrections: Correction[]): void;
457    /// ```
458    pub fn apply(&mut self, corrections: Vec<Correction>) {
459        for correction in corrections {
460            let original: Rc<[u8]> = match self.corrected.get(&correction.path) {
461                Some(file) => Rc::clone(&file.original),
462                None => correction.before,
463            };
464            if *original == *correction.after {
465                self.corrected.remove(&correction.path);
466            } else {
467                self.corrected.insert(
468                    correction.path.clone(),
469                    InstallChange {
470                        path: correction.path,
471                        mode: correction.mode,
472                        original,
473                        replacement: correction.after,
474                    },
475                );
476            }
477        }
478    }
479
480    /// What:
481    ///  A copy of every corrected file,
482    ///  by pathname.
483    ///  Copies share the bytes.
484    /// Why:
485    ///   The fix loop keeps one per changed state to recognize a repeated state.
486    ///
487    /// In TS you'd write (pseudocode):
488    /// ```ts
489    /// correctedState(): Map<string, InstallChange>;
490    /// ```
491    pub fn corrected_state(&self) -> BTreeMap<Vec<u8>, InstallChange> {
492        return self.corrected.clone();
493    }
494
495    /// What:
496    ///  Every corrected file,
497    ///  in Git's byte order of pathnames.
498    /// Why:
499    ///   These are the changes a converged fix installs,
500    ///  and the order its summary
501    ///       lists them in.
502    ///
503    /// In TS you'd write (pseudocode):
504    /// ```ts
505    /// installChanges(): InstallChange[];
506    /// ```
507    pub fn install_changes(&self) -> Vec<InstallChange> {
508        // `.values().cloned().collect()` copies each change in key order into a list.
509        return self.corrected.values().cloned().collect();
510    }
511}
512
513/// What:
514///  `impl ContentState { ... }` continued:
515///  the scan of every scannable candidate.
516/// Why:
517///   The scan pass reads bytes through the store the candidates were prepared in,
518///       with this invocation's corrections laid over it.
519///
520/// In TS you'd write (pseudocode):
521/// ```ts
522/// class ContentState { scan(scanner, rulesPath) {} }
523/// ```
524impl ContentState {
525    /// What:
526    ///  Scan the prepared version through `scanner`.
527    ///  `Option<&[u8]>` is the rules
528    ///       file's own pathname,
529    ///  which is not scanned,
530    ///  or nothing.
531    /// Why:
532    ///   `scan_version` is the candidate layer's one scan pass;
533    ///  this lends it the store
534    ///       and the version together.
535    ///
536    /// In TS you'd write (pseudocode):
537    /// ```ts
538    /// async scan(scanner: CandidateScanner, rulesPath?: Buffer): Promise<CandidateScan[]>;
539    /// ```
540    pub fn scan(
541        &mut self,
542        scanner: &CandidateScanner,
543        rules_path: Option<&[u8]>,
544    ) -> Result<Vec<CandidateScan>, ScanRunError> {
545        match &mut self.prepared {
546            // The store is lent for writing and the version for reading: two separate fields.
547            Some(Ok(prepared)) => {
548                let mut reader: OverlaidStore<'_> = OverlaidStore {
549                    store: &mut prepared.store,
550                    corrected: &self.corrected,
551                };
552                return scan_version(scanner, &mut reader, &prepared.version, rules_path);
553            }
554            Some(Err(_)) | None => return Err(ScanRunError::Candidate(not_prepared_error())),
555        }
556    }
557}
558
559/// What: `impl ContentState { ... }` continued: the whole candidate state, for a policy
560///       that reads files beyond the candidates.
561/// Why:  The dependent-version policy lists every tracked file of the private index and
562///       reads files that are not candidates, through the same store and the same
563///       corrections every other content policy reads through.
564///
565/// In TS you'd write (pseudocode):
566/// ```ts
567/// class ContentState { storeWithCorrections() {} }
568/// ```
569impl ContentState {
570    /// What: The store the candidates were prepared in, with this invocation's corrected
571    ///       files; nothing before preparation or after a failed one. The two borrows are
572    ///       of separate fields, so both can be held at once.
573    /// Why:  A later pass of a direct fix must read what the earlier passes made of every
574    ///       file, candidate or not.
575    ///
576    /// In TS you'd write (pseudocode):
577    /// ```ts
578    /// storeWithCorrections(): [CandidateStore, Map<string, InstallChange>] | undefined;
579    /// ```
580    pub fn store_with_corrections(
581        &mut self,
582    ) -> Option<(&mut CandidateStore, &BTreeMap<Vec<u8>, InstallChange>)> {
583        match &mut self.prepared {
584            Some(Ok(prepared)) => return Some((&mut prepared.store, &self.corrected)),
585            Some(Err(_)) | None => return None,
586        }
587    }
588}
589
590/// What: The failure of a read made before any version was prepared.
591/// Why:  Only a defect in a calling policy can cause it. It is reported as a stale
592///       candidate, whose code is `policy-incomplete`: the policy's own sequencing
593///       failed, and the read must still not look clean.
594///
595/// In TS you'd write (pseudocode):
596/// ```ts
597/// const notPreparedError = () => new CandidateError('stale-candidate', '...');
598/// ```
599fn not_prepared_error() -> CandidateError {
600    return CandidateError::new(
601        CandidateFailure::StaleCandidate,
602        "cli-git read candidate content before the candidates were prepared; this is a defect in cli-git.",
603    );
604}
605
606/// What:
607///  `impl Default for ContentState` lets `ContentState::default()` build the empty state.
608/// Why:
609///   Clippy asks a type with a `new()` without arguments to provide `Default` too.
610///
611/// In TS you'd write (pseudocode):
612/// ```ts
613/// static default(): ContentState { return new ContentState(); }
614/// ```
615impl Default for ContentState {
616    /// What:
617    ///  The state that has prepared nothing.
618    ///  Why:
619    ///  same as `new`.
620    ///
621    /// In TS you'd write (pseudocode):
622    /// ```ts
623    /// static default(): ContentState
624    /// ```
625    fn default() -> ContentState {
626        return ContentState::new();
627    }
628}
629
630/// Preparation,
631///  sharing and failure controls stay out of the release executable.
632#[cfg(test)]
633#[path = "policy_content_tests.rs"]
634mod tests;