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(¬_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;