Skip to main content

projectatlas_cli/
optional_parser_lifecycle.rs

1//! Explicit local lifecycle for the separately shipped optional parser pack.
2
3use crate::parser_supervisor::{
4    OptionalParserSupervisor, ParserSupervisorError, admit_optional_parser_artifact,
5};
6use projectatlas_core::optional_parser_pack::{
7    OPTIONAL_PARSER_PACK_ID, OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES,
8    OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES, OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES,
9    OPTIONAL_PARSER_PACK_MAX_FILE_BYTES, OPTIONAL_PARSER_PACK_MAX_FILE_ENTRIES,
10    OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION, OptionalParserCapability,
11    OptionalParserPackArtifactManifest, OptionalParserPackManifest,
12    OptionalParserPackManifestError, PackPlatform, PackRelativePath,
13};
14use projectatlas_core::optional_parser_protocol::{ParserArtifactIdentity, ParserContentDigest};
15use serde::{Deserialize, Serialize};
16use sha2::{Digest as _, Sha256};
17use std::collections::BTreeMap;
18use std::env;
19use std::fs::{self, File, OpenOptions};
20use std::io::{self, BufReader, Read, Write};
21use std::path::{Path, PathBuf};
22#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
23use std::process::{Command, Stdio};
24use std::sync::OnceLock;
25#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
26use std::thread;
27#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
28use std::time::{Duration, Instant};
29#[cfg(test)]
30use tar::EntryType;
31use tempfile::{NamedTempFile, TempDir};
32use thiserror::Error;
33
34/// Canonical top-level directory inside every completed parser-pack archive.
35const ARCHIVE_ROOT: &str = "projectatlas-broad-parser";
36/// Logical capability manifest at the artifact root.
37const ACCEPTED_MANIFEST_FILE_NAME: &str = "accepted-capabilities.json";
38/// Platform artifact manifest at the artifact root.
39const ARTIFACT_MANIFEST_FILE_NAME: &str = "artifact-manifest.json";
40/// Project-local selection metadata, deliberately inside the excluded atlas state directory.
41pub const OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH: &str =
42    ".projectatlas/optional-parser-pack.json";
43/// Current strict project-selection schema.
44const PROJECT_SELECTION_SCHEMA_VERSION: u32 = 1;
45/// Maximum project-selection bytes accepted from disk.
46const PROJECT_SELECTION_MAX_BYTES: u64 = 16 * 1024;
47/// Maximum decompressed tar framing beyond manifest-bounded file payloads.
48const TAR_FRAMING_ALLOWANCE_BYTES: u64 = 1024 * 1024;
49/// Canonical regular payload mode in a completed archive.
50const PAYLOAD_MODE: u32 = 0o644;
51/// Canonical parser-worker mode in a completed archive.
52const WORKER_MODE: u32 = 0o755;
53/// Maximum lifecycle-owned directory entries inspected by one metadata operation.
54const LIFECYCLE_METADATA_ENTRY_LIMIT: usize = 1_024;
55/// Stable sibling lease retained across logical-pack removal and process crashes.
56const OPTIONAL_PARSER_PACK_LEASE_FILE_NAME: &str = ".projectatlas-broad-parser.lifecycle.lock";
57/// Stable project-local lease serializing selection read-modify-write transitions.
58/// Operations needing both leases always acquire the pack lease first.
59const OPTIONAL_PARSER_SELECTION_LEASE_FILE_NAME: &str = "optional-parser-pack.selection.lock";
60/// Exact artifact-bound Windows containment broker file.
61#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
62const WINDOWS_CONTAINMENT_BROKER_FILE_NAME: &str = "projectatlas-parser-containment.exe";
63/// Closed broker operation that removes only its own artifact-scoped profile and ACEs.
64#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
65const WINDOWS_PROFILE_CLEANUP_ARGUMENT: &str = "cleanup-artifact-profile";
66/// Fixed successful cleanup result emitted by the trusted broker.
67#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
68const WINDOWS_PROFILE_CLEANUP_RESULT: &str = "[parser-containment] artifact profile cleanup passed";
69/// Hard deadline for one trusted profile cleanup command.
70#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
71const WINDOWS_PROFILE_CLEANUP_TIMEOUT: Duration = Duration::from_secs(30);
72/// Hard post-operation ceiling for termination, reap, and pipe-reader joins.
73#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
74const WINDOWS_PROFILE_CLEANUP_REAP_TIMEOUT: Duration = Duration::from_secs(5);
75/// Bounded stdout and stderr retained from one cleanup command.
76#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
77const WINDOWS_PROFILE_CLEANUP_OUTPUT_BYTES: u64 = 64 * 1024;
78/// Prefix for a unique slot tombstone whose profile cleanup is still pending.
79#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
80const WINDOWS_REMOVING_TOMBSTONE_PREFIX: &str = ".pa-r-";
81/// Prefix for a unique tombstone whose profile is cleaned and only deletion remains.
82#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
83const WINDOWS_CLEANED_TOMBSTONE_PREFIX: &str = ".pa-c-";
84/// Hex characters retained as a fail-closed tombstone routing hint.
85#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
86const WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS: usize = 32;
87/// Largest working-directory path `CreateProcessW` can accept after adding `\0`.
88#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
89const WINDOWS_PROCESS_CURRENT_DIRECTORY_MAX_UTF16_UNITS: usize = 258;
90
91/// Failure while inspecting or changing the local optional-parser lifecycle.
92#[derive(Debug, Error)]
93pub enum OptionalParserPackLifecycleError {
94    /// This host has no accepted parser containment adapter.
95    #[error("optional parser containment is unsupported on {os}/{architecture}")]
96    UnsupportedContainment {
97        /// Host operating-system identity.
98        os: &'static str,
99        /// Host architecture identity.
100        architecture: &'static str,
101    },
102    /// A required user-owned storage location could not be derived.
103    #[error("could not determine the user-owned optional parser-pack storage root")]
104    StorageRootUnavailable,
105    /// Another process currently holds an incompatible parser-pack lifecycle lease.
106    #[error(
107        "optional parser-pack lifecycle is busy at {path:?}; retry after the active operation finishes"
108    )]
109    Busy {
110        /// Exact user-owned lease file that could not be locked.
111        path: PathBuf,
112    },
113    /// One filesystem operation failed.
114    #[error("{operation} failed for {path:?}: {source}")]
115    Io {
116        /// Stable operation description.
117        operation: &'static str,
118        /// Exact lifecycle-owned path involved.
119        path: PathBuf,
120        /// Source filesystem failure.
121        #[source]
122        source: io::Error,
123    },
124    /// An archive or lifecycle record violated a closed contract.
125    #[error("optional parser-pack lifecycle data is invalid: {reason}")]
126    InvalidData {
127        /// Bounded rejection reason.
128        reason: String,
129    },
130    /// A logical or artifact manifest failed validation.
131    #[error("{0}")]
132    Manifest(#[from] OptionalParserPackManifestError),
133    /// A verified artifact failed supervisor admission.
134    #[error("{0}")]
135    Supervisor(#[from] ParserSupervisorError),
136    /// Strict JSON decoding or encoding failed.
137    #[error("optional parser-pack JSON is invalid: {0}")]
138    Json(#[from] serde_json::Error),
139    /// One or more artifact-scoped cleanup operations failed.
140    #[error("optional parser-pack cleanup was incomplete: {message}")]
141    CleanupIncomplete {
142        /// Bounded artifact-scoped cleanup failure details.
143        message: String,
144    },
145    /// A lifecycle operation failed and mandatory child cleanup also failed.
146    #[error("optional parser-pack lifecycle operation failed and cleanup also failed")]
147    OperationAndCleanup {
148        /// Original typed lifecycle failure.
149        operation: Box<Self>,
150        /// Typed mandatory cleanup failure.
151        cleanup: Box<Self>,
152    },
153}
154
155impl OptionalParserPackLifecycleError {
156    /// Return whether this failure is the typed unsupported-containment boundary.
157    #[must_use]
158    pub const fn is_unsupported_containment(&self) -> bool {
159        matches!(self, Self::UnsupportedContainment { .. })
160    }
161}
162
163/// Preserve both a lifecycle operation failure and its mandatory cleanup failure.
164fn finish_with_cleanup<T>(
165    operation: Result<T, OptionalParserPackLifecycleError>,
166    cleanup: Result<(), OptionalParserPackLifecycleError>,
167) -> Result<T, OptionalParserPackLifecycleError> {
168    match (operation, cleanup) {
169        (Ok(value), Ok(())) => Ok(value),
170        (Err(operation), Ok(())) => Err(operation),
171        (Ok(_), Err(cleanup)) => Err(cleanup),
172        (Err(operation), Err(cleanup)) => {
173            Err(OptionalParserPackLifecycleError::OperationAndCleanup {
174                operation: Box::new(operation),
175                cleanup: Box::new(cleanup),
176            })
177        }
178    }
179}
180
181/// Explicit parser-pack lifecycle operation represented in structured output.
182#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
183#[serde(rename_all = "snake_case")]
184pub enum OptionalParserPackOperation {
185    /// Validate a completed archive without installing it.
186    Verify,
187    /// Install a completed archive into an immutable user-owned slot.
188    Install,
189    /// Select an already installed immutable slot for the current project.
190    Enable,
191    /// Install and atomically select a replacement while retaining rollback identity.
192    Update,
193    /// Remove the current project's selection without deleting installed slots.
194    Disable,
195    /// Disable this project and delete this logical pack's user-owned slots.
196    Remove,
197    /// Inspect content-free local lifecycle state.
198    Status,
199}
200
201/// Stable content-free lifecycle state.
202#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
203#[serde(rename_all = "snake_case")]
204pub enum OptionalParserPackState {
205    /// No accepted containment adapter exists on this host.
206    UnsupportedContainment,
207    /// No installed slot or project selection exists.
208    Absent,
209    /// At least one slot is installed but this project is disabled.
210    InstalledDisabled,
211    /// This project selects a present immutable slot.
212    Enabled,
213    /// This project selects a present slot and retains a present rollback slot.
214    RollbackReady,
215    /// Local selection or slot metadata is malformed, missing, or structurally unsafe.
216    Stale,
217}
218
219/// Content-free identity and presence of one immutable parser-pack slot.
220#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
221pub struct OptionalParserPackSlotReport {
222    /// `ProjectAtlas` release line that owns the slot namespace.
223    pub projectatlas_version: String,
224    /// BLAKE3 identity of the exact artifact-manifest bytes.
225    pub artifact: String,
226    /// Whether the exact immutable slot directory is present.
227    pub present: bool,
228}
229
230/// Structured result shared by lifecycle commands and later settings integration.
231#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
232pub struct OptionalParserPackLifecycleReport {
233    /// Operation that produced this report.
234    pub operation: OptionalParserPackOperation,
235    /// Current content-free lifecycle state.
236    pub state: OptionalParserPackState,
237    /// Stable identity of the one logical optional parser pack.
238    pub pack_id: &'static str,
239    /// Whether the current host has an accepted containment adapter.
240    pub supported: bool,
241    /// Closed host capability, including built-in-only fallback on unsupported tuples.
242    pub capability: OptionalParserCapability,
243    /// Accepted target triple for this host, when supported.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub platform: Option<&'static str>,
246    /// Number of installed immutable slots observed within the bounded status scan.
247    pub installed_slots: usize,
248    /// Whether lifecycle metadata traversal reached its directory-entry bound.
249    pub installed_slots_truncated: bool,
250    /// Current project selection, including missing-slot state.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub selected: Option<OptionalParserPackSlotReport>,
253    /// Previous project selection retained by the last successful update.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub rollback: Option<OptionalParserPackSlotReport>,
256    /// Artifact targeted by verify, install, enable, or update.
257    #[serde(skip_serializing_if = "Option::is_none")]
258    pub artifact: Option<OptionalParserPackSlotReport>,
259    /// Whether this command changed durable lifecycle state.
260    pub changed: bool,
261}
262
263/// Injectable artifact cleanup retained only by unit-test guards.
264#[cfg(test)]
265type ProfileCleanupOverride = dyn FnMut(&Path, &ParserArtifactIdentity) -> Result<(), OptionalParserPackLifecycleError>
266    + Send
267    + Sync;
268
269/// Owns cleanup of an artifact profile created from a temporary pack root.
270///
271/// Callers must either hold the parser-pack lifecycle's exclusive lease or run
272/// on an isolated host where no installed copy of the same artifact can be in
273/// use. Normal control flow must call [`Self::cleanup`]; `Drop` is only a
274/// best-effort fallback for unwinding or a forgotten terminal action.
275#[must_use = "temporary parser artifact profiles require cleanup or installed-slot transfer"]
276pub struct TemporaryParserArtifactProfile {
277    /// Extracted pack root containing the exact cleanup broker and manifest.
278    pack_root: PathBuf,
279    /// Identity of the exact artifact manifest that owns the profile.
280    artifact: ParserArtifactIdentity,
281    /// Whether this temporary guard still owns its one cleanup attempt.
282    cleanup_pending: bool,
283    /// Per-instance cleanup override that keeps unit tests away from host profiles.
284    #[cfg(test)]
285    cleanup_override: Option<Box<ProfileCleanupOverride>>,
286}
287
288impl TemporaryParserArtifactProfile {
289    /// Arm cleanup ownership from an already verified parser artifact authority.
290    ///
291    /// The caller must hold the lifecycle's exclusive pack lease or run on an
292    /// isolated verifier host before allowing packaged code to execute.
293    pub fn for_verified_supervisor(supervisor: &OptionalParserSupervisor) -> Self {
294        Self {
295            pack_root: supervisor.pack_root().to_path_buf(),
296            artifact: supervisor.artifact_identity().clone(),
297            cleanup_pending: true,
298            #[cfg(test)]
299            cleanup_override: None,
300        }
301    }
302
303    /// Build a cleanup owner for focused lifecycle state tests.
304    #[cfg(test)]
305    fn new(pack_root: impl Into<PathBuf>, artifact: ParserArtifactIdentity) -> Self {
306        Self {
307            pack_root: pack_root.into(),
308            artifact,
309            cleanup_pending: true,
310            cleanup_override: None,
311        }
312    }
313
314    /// Consume the one explicit fallible artifact-profile cleanup attempt.
315    ///
316    /// A failure reaches the caller without another destructor attempt.
317    ///
318    /// # Errors
319    ///
320    /// Returns a typed lifecycle error when the exact broker cannot validate or
321    /// remove its artifact-scoped profile and access-control entries.
322    pub fn cleanup(mut self) -> Result<(), OptionalParserPackLifecycleError> {
323        self.cleanup_pending_profile()
324    }
325
326    /// Transfer cleanup ownership to the newly published immutable slot.
327    fn transfer_to_installed_slot(&mut self) {
328        self.cleanup_pending = false;
329    }
330
331    /// Run cleanup at most once while the temporary pack root still exists.
332    fn cleanup_pending_profile(&mut self) -> Result<(), OptionalParserPackLifecycleError> {
333        if !std::mem::take(&mut self.cleanup_pending) {
334            return Ok(());
335        }
336        #[cfg(test)]
337        if let Some(cleanup) = self.cleanup_override.as_mut() {
338            return cleanup(&self.pack_root, &self.artifact);
339        }
340        cleanup_platform_profile(&self.pack_root, &self.artifact)
341    }
342}
343
344impl Drop for TemporaryParserArtifactProfile {
345    fn drop(&mut self) {
346        drop(self.cleanup_pending_profile());
347    }
348}
349
350/// Concrete owner of project selection and user-owned immutable pack slots.
351#[derive(Clone, Debug)]
352pub struct OptionalParserPackLifecycle {
353    /// Selected project whose local enablement is owned by this lifecycle.
354    project_root: PathBuf,
355    /// User-owned root containing versioned immutable slots.
356    storage_root: OnceLock<Option<PathBuf>>,
357    /// Closed host capability retained for every lifecycle report.
358    capability: OptionalParserCapability,
359    /// Test-only failure seam proving admission precedes lifecycle mutation.
360    #[cfg(test)]
361    admission_failure: Option<fn(&Path) -> ParserSupervisorError>,
362    /// Deterministically fail selection publication after update staging in focused tests.
363    #[cfg(test)]
364    selection_publication_failure: bool,
365}
366
367/// Validated content-free identity of one selected immutable parser-pack slot.
368#[derive(Clone, Debug, Eq, Hash, PartialEq)]
369pub struct OptionalParserPackSelectionKey {
370    /// Stable opaque key for derivation fingerprints and cache ownership.
371    value: String,
372    /// `ProjectAtlas` release namespace that owns the slot.
373    projectatlas_version: String,
374    /// Authenticated identity of the selected artifact manifest.
375    artifact: ParserArtifactIdentity,
376}
377
378impl OptionalParserPackSelectionKey {
379    /// Borrow the stable opaque selection key.
380    #[must_use]
381    pub fn as_str(&self) -> &str {
382        &self.value
383    }
384
385    /// Borrow the authenticated selected-artifact identity.
386    #[must_use]
387    pub const fn artifact(&self) -> &ParserArtifactIdentity {
388        &self.artifact
389    }
390}
391
392/// Content-free project selection used by derivation and source-admission policy.
393#[derive(Clone, Debug, Eq, PartialEq)]
394pub enum OptionalParserPackProjectSelection {
395    /// No optional parser pack is selected for this project.
396    Inactive,
397    /// One validated artifact identity is selected for this project.
398    Selected(OptionalParserPackSelectionKey),
399}
400
401impl OptionalParserPackProjectSelection {
402    /// Borrow the selected slot key, or return `None` for inactive default-core operation.
403    #[must_use]
404    pub const fn selection_key(&self) -> Option<&OptionalParserPackSelectionKey> {
405        match self {
406            Self::Inactive => None,
407            Self::Selected(selection) => Some(selection),
408        }
409    }
410
411    /// Borrow the selected artifact identity, or return `None` when inactive.
412    #[must_use]
413    pub const fn artifact(&self) -> Option<&ParserArtifactIdentity> {
414        match self {
415            Self::Inactive => None,
416            Self::Selected(selection) => Some(selection.artifact()),
417        }
418    }
419}
420
421/// Already-open immutable slot after manifest, digest, and permission verification.
422struct OpenedOptionalParserPackSlot {
423    /// Durable selected-slot identity.
424    selection_key: OptionalParserPackSelectionKey,
425    /// Already-open process authority; no child has been launched.
426    supervisor: OptionalParserSupervisor,
427}
428
429/// Cooperative mode for the one stable cross-process parser-pack lease.
430#[derive(Clone, Copy, Debug, Eq, PartialEq)]
431enum OptionalParserPackLeaseMode {
432    /// Multiple verified readers and executions may coexist.
433    Shared,
434    /// One immutable-slot mutation excludes every reader and other writer.
435    Exclusive,
436}
437
438/// RAII-held cross-process authority over immutable optional-parser slots.
439struct OptionalParserPackLease {
440    /// Open locked file; process exit closes it even when destructors do not run.
441    file: File,
442}
443
444impl Drop for OptionalParserPackLease {
445    fn drop(&mut self) {
446        drop(self.file.unlock());
447    }
448}
449
450/// Verified current-project handoff for runtime staging.
451///
452/// The handoff owns the already-open, still-unlaunched supervisor so runtime
453/// integration cannot accidentally reopen and rehash the exact selected slot.
454pub struct VerifiedOptionalParserPackSelection {
455    /// Durable selected-slot identity.
456    selection_key: OptionalParserPackSelectionKey,
457    /// Already-open verified process authority; no child has been launched.
458    supervisor: OptionalParserSupervisor,
459    /// Shared cross-process authority retained for the complete worker lifetime.
460    _execution_lease: OptionalParserPackLease,
461}
462
463impl VerifiedOptionalParserPackSelection {
464    /// Borrow the durable content-free selected-slot key.
465    #[must_use]
466    pub const fn selection_key(&self) -> &OptionalParserPackSelectionKey {
467        &self.selection_key
468    }
469
470    /// Borrow the verified artifact-manifest identity.
471    #[must_use]
472    pub const fn artifact(&self) -> &ParserArtifactIdentity {
473        self.selection_key.artifact()
474    }
475
476    /// Return whether the selected artifact admits one canonical language identity.
477    #[must_use]
478    pub fn accepts_language(&self, language_id: &str) -> bool {
479        self.supervisor.accepts_language(language_id)
480    }
481
482    /// Mutably borrow the already-open supervisor without releasing execution authority.
483    pub fn supervisor_mut(&mut self) -> &mut OptionalParserSupervisor {
484        &mut self.supervisor
485    }
486}
487
488impl OptionalParserPackLifecycle {
489    /// Construct lifecycle ownership without opening archives or touching the filesystem.
490    ///
491    /// `storage_root` is a hidden test/managed-install override. Normal callers pass `None`;
492    /// the platform's user-owned application data directory is resolved only if a later
493    /// operation actually needs parser-pack storage.
494    ///
495    /// # Errors
496    ///
497    /// Construction currently preserves the fallible public signature for compatibility but
498    /// performs no storage lookup. A storage-dependent operation reports
499    /// [`OptionalParserPackLifecycleError::StorageRootUnavailable`] when the deferred platform
500    /// root cannot be derived.
501    pub fn new(
502        project_root: impl Into<PathBuf>,
503        storage_root: Option<PathBuf>,
504    ) -> Result<Self, OptionalParserPackLifecycleError> {
505        let deferred_storage_root = OnceLock::new();
506        if let Some(storage_root) = storage_root
507            && deferred_storage_root.set(Some(storage_root)).is_err()
508        {
509            return Err(OptionalParserPackLifecycleError::StorageRootUnavailable);
510        }
511        let capability = OptionalParserCapability::current();
512        Ok(Self {
513            project_root: project_root.into(),
514            storage_root: deferred_storage_root,
515            capability,
516            #[cfg(test)]
517            admission_failure: None,
518            #[cfg(test)]
519            selection_publication_failure: false,
520        })
521    }
522
523    /// Verify and fixture-probe one local completed archive without installing it.
524    ///
525    /// # Errors
526    ///
527    /// Returns unsupported containment before opening the archive, or an archive,
528    /// manifest, digest, platform, path, bound, or supervisor-admission failure.
529    pub fn verify(
530        &self,
531        archive: &Path,
532    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
533        let platform = self.require_supported()?;
534        #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
535        let _lease = self.acquire_pack_lease(OptionalParserPackLeaseMode::Exclusive)?;
536        #[cfg(test)]
537        self.fail_admission_if_injected(archive)?;
538        let verified = Self::verify_archive(archive, platform, None)?;
539        let artifact = verified.slot_report(false);
540        verified.cleanup_profile()?;
541        Ok(self.report(OptionalParserPackOperation::Verify, false, Some(artifact)))
542    }
543
544    /// Install one local completed archive into an immutable versioned slot.
545    ///
546    /// Installation never enables the slot for this or another project.
547    ///
548    /// # Errors
549    ///
550    /// Returns unsupported containment before opening the archive or mutating storage,
551    /// or a validation, admission, publication, or filesystem failure.
552    pub fn install(
553        &self,
554        archive: &Path,
555    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
556        let platform = self.require_supported()?;
557        let _lease = self.acquire_pack_lease(OptionalParserPackLeaseMode::Exclusive)?;
558        let (slot, changed) = self.install_archive(archive, platform)?;
559        Ok(self.report(
560            OptionalParserPackOperation::Install,
561            changed,
562            Some(slot.report(true)),
563        ))
564    }
565
566    /// Enable an explicitly named already installed slot for the current project.
567    ///
568    /// Passing the artifact reported in `status.rollback` is the canonical explicit rollback:
569    /// the retained slot becomes selected and the displaced selection becomes rollback-ready.
570    ///
571    /// # Errors
572    ///
573    /// Returns unsupported containment before state inspection or mutation, or a slot,
574    /// selection, manifest, digest, admission, or atomic-publication failure.
575    pub fn enable(
576        &self,
577        artifact: &str,
578    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
579        let _platform = self.require_supported()?;
580        let _pack_lease = self.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?;
581        let _selection_lease = self.acquire_selection_mutation_lease()?;
582        let slot = PackSlotIdentity::current(artifact)?;
583        self.admit_installed_slot(&slot)?;
584        let previous = self.read_selection()?;
585        let changed = previous.as_ref().map(|value| &value.selected) != Some(&slot);
586        if changed {
587            let rollback = previous.map(|value| value.selected);
588            self.write_selection(&ProjectSelection::new(slot.clone(), rollback))?;
589        }
590        Ok(self.report(
591            OptionalParserPackOperation::Enable,
592            changed,
593            Some(slot.report(true)),
594        ))
595    }
596
597    /// Install and atomically select a replacement, retaining the previous exact slot.
598    ///
599    /// Selection publication is the update commit point. A failed install or verification
600    /// publishes no candidate. If selection publication fails after a new immutable candidate
601    /// was installed, the prior selection bytes, selected slot, and rollback slot stay exact;
602    /// the verified candidate remains installed so an identical retry can reuse it deterministically.
603    ///
604    /// # Errors
605    ///
606    /// Returns unsupported containment before archive or state access, rejects an absent or
607    /// invalid prior selection, and propagates archive, admission, or atomic-publication failures.
608    pub fn update(
609        &self,
610        archive: &Path,
611    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
612        let platform = self.require_supported()?;
613        let _pack_lease = self.acquire_pack_lease(OptionalParserPackLeaseMode::Exclusive)?;
614        let _selection_lease = self.acquire_selection_mutation_lease()?;
615        let previous = self.read_selection()?.ok_or_else(|| {
616            invalid_data("update requires an enabled current-project parser-pack selection")
617        })?;
618        self.open_verified_installed_slot(&previous.selected)?;
619        let (slot, installed) = self.install_archive(archive, platform)?;
620        let selection_changed = self.publish_installed_update(&previous, &slot)?;
621        Ok(self.report(
622            OptionalParserPackOperation::Update,
623            installed || selection_changed,
624            Some(slot.report(true)),
625        ))
626    }
627
628    /// Atomically commit one already-installed update candidate to project selection.
629    fn publish_installed_update(
630        &self,
631        previous: &ProjectSelection,
632        slot: &PackSlotIdentity,
633    ) -> Result<bool, OptionalParserPackLifecycleError> {
634        if slot == &previous.selected {
635            return Ok(false);
636        }
637        self.write_selection(&ProjectSelection::new(
638            slot.clone(),
639            Some(previous.selected.clone()),
640        ))?;
641        Ok(true)
642    }
643
644    /// Disable the optional parser pack for the current project.
645    ///
646    /// This cleanup remains supported and idempotent when containment is unsupported or the
647    /// selection file is stale, malformed, or from another lifecycle schema.
648    ///
649    /// # Errors
650    ///
651    /// Returns only a filesystem cleanup or bounded status-inspection failure.
652    pub fn disable(
653        &self,
654    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
655        if !self.selection_mutation_needed()? {
656            return Ok(self.report(OptionalParserPackOperation::Disable, false, None));
657        }
658        let _selection_lease = self.acquire_selection_mutation_lease()?;
659        let changed = self.remove_selection_if_present()?;
660        Ok(self.report(OptionalParserPackOperation::Disable, changed, None))
661    }
662
663    /// Disable this project and remove this logical pack's user-owned immutable slots.
664    ///
665    /// Other projects are never scanned or mutated; their now-missing selections are reported
666    /// as stale when they next inspect status. Cleanup is safe and idempotent on unsupported hosts.
667    ///
668    /// # Errors
669    ///
670    /// Returns only an exact lifecycle-owned filesystem cleanup or bounded status failure.
671    pub fn remove(
672        &self,
673    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
674        let acquire_pack_lease = if self.capability.pack_platform().is_none() {
675            let storage_root = self.storage_root()?;
676            match direct_directory_state(storage_root)? {
677                DirectDirectoryState::Missing => false,
678                DirectDirectoryState::Real => !matches!(
679                    direct_directory_state(&storage_root.join(OPTIONAL_PARSER_PACK_ID))?,
680                    DirectDirectoryState::Missing
681                ),
682                DirectDirectoryState::Unsafe => true,
683            }
684        } else {
685            true
686        };
687        let _pack_lease = acquire_pack_lease
688            .then(|| self.acquire_pack_lease(OptionalParserPackLeaseMode::Exclusive))
689            .transpose()?;
690        let _selection_lease = self
691            .selection_mutation_needed()?
692            .then(|| self.acquire_selection_mutation_lease())
693            .transpose()?;
694        let pack_root = self.pack_root()?;
695        let slots = installed_slot_paths(&pack_root)?;
696        let selection_changed = self.remove_selection_if_present()?;
697        let storage_changed = self.remove_installed_pack(&pack_root, slots)?;
698        Ok(self.report(
699            OptionalParserPackOperation::Remove,
700            selection_changed || storage_changed,
701            None,
702        ))
703    }
704
705    /// Inspect bounded content-free lifecycle metadata without hashing or loading pack assets.
706    ///
707    /// # Errors
708    ///
709    /// Returns a filesystem error only when exact lifecycle-owned metadata cannot be inspected.
710    pub fn status(
711        &self,
712    ) -> Result<OptionalParserPackLifecycleReport, OptionalParserPackLifecycleError> {
713        Ok(self.report(OptionalParserPackOperation::Status, false, None))
714    }
715
716    /// Derive the current project's content-free optional-parser selection.
717    ///
718    /// An absent selection takes the default-core fast path without inspecting
719    /// parser-pack storage. A present selection is rejected on unsupported hosts
720    /// before its contents are read, then decoded through the strict lifecycle
721    /// schema without opening any source file.
722    ///
723    /// # Errors
724    ///
725    /// Returns an unsafe project-state-path failure, typed unsupported containment
726    /// for any present selection on an unsupported host, or a strict selection
727    /// schema or identity failure.
728    pub fn derive_project_selection(
729        &self,
730    ) -> Result<OptionalParserPackProjectSelection, OptionalParserPackLifecycleError> {
731        if !self.selection_entry_present()? {
732            return Ok(OptionalParserPackProjectSelection::Inactive);
733        }
734        self.require_supported()?;
735        let selection = self
736            .read_selection()?
737            .ok_or_else(|| invalid_data("project parser-pack selection disappeared"))?;
738        Ok(OptionalParserPackProjectSelection::Selected(
739            selection.selected.selection_key()?,
740        ))
741    }
742
743    /// Resolve the current project's enabled slot for normal runtime staging.
744    ///
745    /// An absent selection returns `Ok(None)` on every host without inspecting pack storage.
746    /// A present selection refuses unsupported hosts before reading its contents. Supported
747    /// selections are returned only after immutable-slot, manifest, digest, and supervisor
748    /// admission verification.
749    ///
750    /// # Errors
751    ///
752    /// Returns typed unsupported containment for a present selection on an unsupported host,
753    /// or a strict selection, slot, manifest, digest, immutability, or admission failure.
754    pub fn resolve_selected_pack(
755        &self,
756    ) -> Result<Option<VerifiedOptionalParserPackSelection>, OptionalParserPackLifecycleError> {
757        let selection = self.derive_project_selection()?;
758        let OptionalParserPackProjectSelection::Selected(selection_key) = selection else {
759            return Ok(None);
760        };
761        let execution_lease = self.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?;
762        let slot = PackSlotIdentity::from_selection_key(&selection_key);
763        let verified = self.open_verified_installed_slot(&slot)?;
764        if verified.selection_key != selection_key {
765            return Err(invalid_data(
766                "verified optional parser-pack slot differs from project selection",
767            ));
768        }
769        Ok(Some(VerifiedOptionalParserPackSelection {
770            selection_key: verified.selection_key,
771            supervisor: verified.supervisor,
772            _execution_lease: execution_lease,
773        }))
774    }
775
776    /// Refuse unsupported hosts before callers access archives, state, or storage.
777    fn require_supported(&self) -> Result<PackPlatform, OptionalParserPackLifecycleError> {
778        match self.capability {
779            OptionalParserCapability::Pack { platform } => Ok(platform),
780            OptionalParserCapability::BuiltInOnly => {
781                Err(OptionalParserPackLifecycleError::UnsupportedContainment {
782                    os: env::consts::OS,
783                    architecture: env::consts::ARCH,
784                })
785            }
786        }
787    }
788
789    /// Install after the unsupported-host guard has already passed.
790    fn install_archive(
791        &self,
792        archive: &Path,
793        platform: PackPlatform,
794    ) -> Result<(PackSlotIdentity, bool), OptionalParserPackLifecycleError> {
795        #[cfg(test)]
796        self.fail_admission_if_injected(archive)?;
797        self.ensure_storage_roots()?;
798        let versions_root = self.versions_root()?;
799        let mut verified = Self::verify_archive(archive, platform, Some(&versions_root))?;
800        let slot = verified.slot_identity();
801        let operation = (|| {
802            let version_root = versions_root.join(&slot.projectatlas_version);
803            ensure_direct_directory(&versions_root, &version_root)?;
804            #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
805            if windows_slot_cleanup_in_progress(&version_root, &slot.artifact)? {
806                return Err(invalid_data(
807                    "the selected parser-pack artifact still has a cleanup tombstone",
808                ));
809            }
810            let destination = self.slot_path(&slot)?;
811            if self.slot_path_is_real(&slot)? {
812                self.open_verified_installed_slot(&slot)?;
813                return Ok((slot.clone(), false));
814            }
815            if fs::symlink_metadata(&destination).is_ok() {
816                return Err(invalid_data(
817                    "immutable slot path is occupied by a non-directory entry",
818                ));
819            }
820            #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
821            let _publication_staging = {
822                let staging = stage_parser_pack_for_atomic_publication(
823                    &mut verified.pack_root,
824                    &version_root,
825                )?;
826                verified
827                    .temporary_profile
828                    .pack_root
829                    .clone_from(&verified.pack_root);
830                staging
831            };
832            if let Err(operation) = seal_immutable_tree(&verified.pack_root) {
833                return finish_with_cleanup(
834                    Err(operation),
835                    make_tree_writable(&verified.pack_root),
836                );
837            }
838            match fs::rename(&verified.pack_root, &destination) {
839                Ok(()) => {
840                    verified.transfer_profile_to_installed_slot();
841                    Ok((slot.clone(), true))
842                }
843                Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {
844                    make_tree_writable(&verified.pack_root)?;
845                    self.open_verified_installed_slot(&slot)?;
846                    Ok((slot.clone(), false))
847                }
848                Err(source) => finish_with_cleanup(
849                    Err(io_error(
850                        "publish immutable parser-pack slot",
851                        destination,
852                        source,
853                    )),
854                    make_tree_writable(&verified.pack_root),
855                ),
856            }
857        })();
858        let cleanup = verified.cleanup_profile();
859        finish_with_cleanup(operation, cleanup)
860    }
861
862    /// Validate a local completed archive into a temporary directory.
863    fn verify_archive(
864        archive: &Path,
865        platform: PackPlatform,
866        staging_parent: Option<&Path>,
867    ) -> Result<VerifiedArchive, OptionalParserPackLifecycleError> {
868        let before = sha256_file(archive, OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES)?;
869        let extracted = extract_archive(archive, staging_parent)?;
870        let accepted_bytes = read_bounded_file(
871            &extracted.pack_root.join(ACCEPTED_MANIFEST_FILE_NAME),
872            u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES)
873                .map_err(|source| invalid_data(source.to_string()))?,
874        )?;
875        let logical = OptionalParserPackManifest::from_json(&accepted_bytes)?;
876        let artifact_bytes = read_bounded_file(
877            &extracted.pack_root.join(ARTIFACT_MANIFEST_FILE_NAME),
878            u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES)
879                .map_err(|source| invalid_data(source.to_string()))?,
880        )?;
881        let artifact: OptionalParserPackArtifactManifest = serde_json::from_slice(&artifact_bytes)?;
882        artifact.validate(&logical)?;
883        if artifact.platform != platform {
884            return Err(invalid_data(format!(
885                "archive target {} does not match current host target {}",
886                artifact.platform.as_str(),
887                platform.as_str()
888            )));
889        }
890        require_archive_name(archive, &artifact.projectatlas_version, platform)?;
891        validate_observed_inventory(&extracted.observed, &artifact)?;
892        let projectatlas_version = artifact.projectatlas_version;
893        let supervisor = OptionalParserSupervisor::open(&extracted.pack_root)?;
894        let artifact_identity = supervisor.artifact_identity().clone();
895        let temporary_profile =
896            TemporaryParserArtifactProfile::for_verified_supervisor(&supervisor);
897        if let Err(error) = admit_optional_parser_artifact(supervisor, &logical) {
898            return finish_with_cleanup(Err(error.into()), temporary_profile.cleanup());
899        }
900        let after = match sha256_file(archive, OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES) {
901            Ok(after) => after,
902            Err(operation) => {
903                return finish_with_cleanup(Err(operation), temporary_profile.cleanup());
904            }
905        };
906        if before != after {
907            return finish_with_cleanup(
908                Err(invalid_data(
909                    "completed archive changed during verification",
910                )),
911                temporary_profile.cleanup(),
912            );
913        }
914        Ok(VerifiedArchive {
915            temporary_profile,
916            _directory: extracted.directory,
917            pack_root: extracted.pack_root,
918            artifact: artifact_identity,
919            projectatlas_version,
920        })
921    }
922
923    /// Execute the complete current-host admission probe before selecting an installed slot.
924    fn admit_installed_slot(
925        &self,
926        slot: &PackSlotIdentity,
927    ) -> Result<(), OptionalParserPackLifecycleError> {
928        let root = self.slot_path(slot)?;
929        #[cfg(test)]
930        if let Some(failure) = self.admission_failure {
931            return Err(failure(&root).into());
932        }
933        let logical_bytes = read_bounded_file(
934            &root.join(ACCEPTED_MANIFEST_FILE_NAME),
935            u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES)
936                .map_err(|source| invalid_data(source.to_string()))?,
937        )?;
938        let logical = OptionalParserPackManifest::from_json(&logical_bytes)?;
939        let verified = self.open_verified_installed_slot(slot)?;
940        admit_optional_parser_artifact(verified.supervisor, &logical)?;
941        Ok(())
942    }
943
944    /// Verify exact installed identity, manifest bindings, and immutable permissions.
945    fn open_verified_installed_slot(
946        &self,
947        slot: &PackSlotIdentity,
948    ) -> Result<OpenedOptionalParserPackSlot, OptionalParserPackLifecycleError> {
949        let root = self.slot_path(slot)?;
950        if !self.slot_path_is_real(slot)? {
951            return Err(invalid_data(
952                "selected optional parser-pack slot is not installed",
953            ));
954        }
955        verify_immutable_tree(&root)?;
956        let selection_key = slot.selection_key()?;
957        let supervisor = OptionalParserSupervisor::open(root)?;
958        if supervisor.artifact_identity() != selection_key.artifact() {
959            return Err(invalid_data(
960                "installed slot identity differs from its artifact manifest",
961            ));
962        }
963        Ok(OpenedOptionalParserPackSlot {
964            selection_key,
965            supervisor,
966        })
967    }
968
969    /// Attempt exact artifact-scoped cleanup for every installed slot before removing storage.
970    fn remove_installed_pack(
971        &self,
972        pack_root: &Path,
973        slots: Vec<InstalledSlotPath>,
974    ) -> Result<bool, OptionalParserPackLifecycleError> {
975        let mut changed = false;
976        let mut failures = Vec::new();
977        for slot in slots {
978            let result = self.remove_installed_slot(&slot);
979            match result {
980                Ok(slot_changed) => changed |= slot_changed,
981                Err(error) => {
982                    if failures.len() < 16 {
983                        failures.push(format!("{}: {error}", slot.bounded_label()));
984                    }
985                }
986            }
987        }
988        if !failures.is_empty() {
989            return Err(OptionalParserPackLifecycleError::CleanupIncomplete {
990                message: failures.join("; "),
991            });
992        }
993        Ok(remove_tree_if_present(pack_root)? || changed)
994    }
995
996    /// Complete one exact slot's platform cleanup and bounded tree deletion.
997    fn remove_installed_slot(
998        &self,
999        slot: &InstalledSlotPath,
1000    ) -> Result<bool, OptionalParserPackLifecycleError> {
1001        if self.capability.pack_platform() == Some(PackPlatform::WindowsX86_64) {
1002            #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1003            {
1004                return Self::remove_windows_slot(slot);
1005            }
1006            #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
1007            {
1008                return Err(OptionalParserPackLifecycleError::UnsupportedContainment {
1009                    os: env::consts::OS,
1010                    architecture: env::consts::ARCH,
1011                });
1012            }
1013        }
1014        remove_tree_if_present(&slot.entry_root)
1015    }
1016
1017    /// Atomically isolate, profile-clean, and delete one Windows artifact slot.
1018    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1019    fn remove_windows_slot(
1020        slot: &InstalledSlotPath,
1021    ) -> Result<bool, OptionalParserPackLifecycleError> {
1022        if slot.state == InstalledSlotCleanupState::ProfileCleaned {
1023            return remove_tree_if_present(&slot.entry_root);
1024        }
1025        let pending = if slot.state == InstalledSlotCleanupState::Installed {
1026            transition_slot_to_removing_tombstone(slot)?
1027        } else {
1028            slot.clone()
1029        };
1030        let pack_root = pending
1031            .pack_root
1032            .as_deref()
1033            .ok_or_else(|| invalid_data("pending parser-pack tombstone has no pack root"))?;
1034        let identity = PackSlotIdentity {
1035            projectatlas_version: pending.projectatlas_version.clone(),
1036            artifact: pending.artifact.clone(),
1037        };
1038        identity.validate()?;
1039        verify_immutable_tree(pack_root)?;
1040        let supervisor = OptionalParserSupervisor::open(pack_root)?;
1041        if supervisor.artifact_identity().digest().as_str() != identity.artifact {
1042            return Err(invalid_data(
1043                "cleanup tombstone identity differs from its artifact manifest",
1044            ));
1045        }
1046        cleanup_platform_profile(supervisor.pack_root(), supervisor.artifact_identity())?;
1047        drop(supervisor);
1048        let cleaned = transition_tombstone_to_profile_cleaned(&pending)?;
1049        remove_tree_if_present(&cleaned.entry_root)?;
1050        Ok(true)
1051    }
1052
1053    /// Render current state after one operation without opening or hashing pack payloads.
1054    fn report(
1055        &self,
1056        operation: OptionalParserPackOperation,
1057        changed: bool,
1058        artifact: Option<OptionalParserPackSlotReport>,
1059    ) -> OptionalParserPackLifecycleReport {
1060        let selection = self.read_selection_for_status();
1061        let (installed_slots, installed_slots_truncated, cleanup_pending, mut unsafe_storage) =
1062            match self
1063                .pack_root()
1064                .and_then(|root| count_installed_slots(&root))
1065            {
1066                Ok(value) => (value.0, value.1, value.2, false),
1067                Err(_) => (0, false, false, true),
1068            };
1069        let selection_stale = selection.is_err();
1070        let selection = selection.ok().flatten();
1071        let selected = selection.as_ref().map(|value| {
1072            let (present, unsafe_path) = self.slot_presence(&value.selected);
1073            unsafe_storage |= unsafe_path;
1074            value.selected.report(present)
1075        });
1076        let rollback = selection.as_ref().and_then(|value| {
1077            value.rollback.as_ref().map(|slot| {
1078                let (present, unsafe_path) = self.slot_presence(slot);
1079                unsafe_storage |= unsafe_path;
1080                slot.report(present)
1081            })
1082        });
1083        let selected_missing = selected.as_ref().is_some_and(|slot| !slot.present);
1084        let rollback_present = rollback.as_ref().is_some_and(|slot| slot.present);
1085        let state = if selection_stale
1086            || unsafe_storage
1087            || selected_missing
1088            || installed_slots_truncated
1089            || cleanup_pending
1090        {
1091            OptionalParserPackState::Stale
1092        } else if selected.is_some() && rollback_present {
1093            OptionalParserPackState::RollbackReady
1094        } else if selected.is_some() {
1095            OptionalParserPackState::Enabled
1096        } else if installed_slots > 0 {
1097            OptionalParserPackState::InstalledDisabled
1098        } else if self.capability.pack_platform().is_none() {
1099            OptionalParserPackState::UnsupportedContainment
1100        } else {
1101            OptionalParserPackState::Absent
1102        };
1103        OptionalParserPackLifecycleReport {
1104            operation,
1105            state,
1106            pack_id: OPTIONAL_PARSER_PACK_ID,
1107            supported: self.capability.pack_platform().is_some(),
1108            capability: self.capability,
1109            platform: self.capability.pack_platform().map(PackPlatform::as_str),
1110            installed_slots,
1111            installed_slots_truncated,
1112            selected,
1113            rollback,
1114            artifact,
1115            changed,
1116        }
1117    }
1118
1119    /// Return whether any exact selection entry exists without reading its contents.
1120    fn selection_entry_present(&self) -> Result<bool, OptionalParserPackLifecycleError> {
1121        match direct_directory_state(&self.selection_parent())? {
1122            DirectDirectoryState::Missing => return Ok(false),
1123            DirectDirectoryState::Real => {}
1124            DirectDirectoryState::Unsafe => {
1125                return Err(invalid_data(
1126                    "project .projectatlas selection parent is not a real directory",
1127                ));
1128            }
1129        }
1130        let path = self.selection_path();
1131        match fs::symlink_metadata(&path) {
1132            Ok(_) => Ok(true),
1133            Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(false),
1134            Err(source) => Err(io_error(
1135                "inspect project parser-pack selection",
1136                path,
1137                source,
1138            )),
1139        }
1140    }
1141
1142    /// Read strict selection for mutating supported operations.
1143    fn read_selection(&self) -> Result<Option<ProjectSelection>, OptionalParserPackLifecycleError> {
1144        match direct_directory_state(&self.selection_parent())? {
1145            DirectDirectoryState::Missing => return Ok(None),
1146            DirectDirectoryState::Real => {}
1147            DirectDirectoryState::Unsafe => {
1148                return Err(invalid_data(
1149                    "project .projectatlas selection parent is not a real directory",
1150                ));
1151            }
1152        }
1153        let path = self.selection_path();
1154        match fs::symlink_metadata(&path) {
1155            Ok(metadata) if metadata.file_type().is_file() => {
1156                let bytes = read_bounded_file(&path, PROJECT_SELECTION_MAX_BYTES)?;
1157                let selection: ProjectSelection = serde_json::from_slice(&bytes)?;
1158                selection.validate()?;
1159                Ok(Some(selection))
1160            }
1161            Ok(_) => Err(invalid_data(
1162                "project parser-pack selection is not a regular file",
1163            )),
1164            Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(None),
1165            Err(source) => Err(io_error(
1166                "inspect project parser-pack selection",
1167                path,
1168                source,
1169            )),
1170        }
1171    }
1172
1173    /// Read selection for status, preserving malformed metadata as typed stale state.
1174    fn read_selection_for_status(
1175        &self,
1176    ) -> Result<Option<ProjectSelection>, OptionalParserPackLifecycleError> {
1177        self.read_selection()
1178    }
1179
1180    /// Atomically replace current-project selection after every candidate is verified.
1181    fn write_selection(
1182        &self,
1183        selection: &ProjectSelection,
1184    ) -> Result<(), OptionalParserPackLifecycleError> {
1185        selection.validate()?;
1186        let path = self.selection_path();
1187        ensure_anchor_directory(&self.project_root)?;
1188        let parent = self.selection_parent();
1189        ensure_direct_directory(&self.project_root, &parent)?;
1190        let bytes = serde_json::to_vec_pretty(selection)?;
1191        if u64::try_from(bytes.len()).unwrap_or(u64::MAX) > PROJECT_SELECTION_MAX_BYTES {
1192            return Err(invalid_data(
1193                "project parser-pack selection exceeds its byte bound",
1194            ));
1195        }
1196        let mut temporary = NamedTempFile::new_in(&parent)
1197            .map_err(|source| io_error("create temporary project selection", &parent, source))?;
1198        temporary.write_all(&bytes).map_err(|source| {
1199            io_error("write temporary project selection", path.clone(), source)
1200        })?;
1201        temporary
1202            .as_file()
1203            .sync_all()
1204            .map_err(|source| io_error("sync temporary project selection", path.clone(), source))?;
1205        #[cfg(test)]
1206        if self.selection_publication_failure {
1207            return Err(invalid_data(
1208                "injected project selection publication failure",
1209            ));
1210        }
1211        temporary.persist(&path).map_err(|error| {
1212            io_error("publish project parser-pack selection", path, error.error)
1213        })?;
1214        Ok(())
1215    }
1216
1217    /// Exact project-local selection path.
1218    fn selection_path(&self) -> PathBuf {
1219        self.project_root
1220            .join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH)
1221    }
1222
1223    /// Exact direct product-owned parent of project selection metadata.
1224    fn selection_parent(&self) -> PathBuf {
1225        self.project_root.join(".projectatlas")
1226    }
1227
1228    /// Return whether selection cleanup has an exact direct entry to serialize.
1229    fn selection_mutation_needed(&self) -> Result<bool, OptionalParserPackLifecycleError> {
1230        match direct_directory_state(&self.selection_parent())? {
1231            DirectDirectoryState::Missing | DirectDirectoryState::Unsafe => Ok(false),
1232            DirectDirectoryState::Real => self.selection_entry_present(),
1233        }
1234    }
1235
1236    /// Serialize one project-local selection read-modify-write transition.
1237    fn acquire_selection_mutation_lease(
1238        &self,
1239    ) -> Result<OptionalParserPackLease, OptionalParserPackLifecycleError> {
1240        ensure_anchor_directory(&self.project_root)?;
1241        let parent = self.selection_parent();
1242        ensure_direct_directory(&self.project_root, &parent)?;
1243        let path = parent.join(OPTIONAL_PARSER_SELECTION_LEASE_FILE_NAME);
1244        let file = open_or_create_direct_lease_file(&path)?;
1245        match file.try_lock() {
1246            Ok(()) => {
1247                require_direct_lease_path(&path)?;
1248                Ok(OptionalParserPackLease { file })
1249            }
1250            Err(fs::TryLockError::WouldBlock) => {
1251                Err(OptionalParserPackLifecycleError::Busy { path })
1252            }
1253            Err(fs::TryLockError::Error(source)) => {
1254                Err(io_error("lock project parser-pack selection", path, source))
1255            }
1256        }
1257    }
1258
1259    /// Remove only a selection inside a real direct project-owned parent.
1260    fn remove_selection_if_present(&self) -> Result<bool, OptionalParserPackLifecycleError> {
1261        match direct_directory_state(&self.selection_parent())? {
1262            DirectDirectoryState::Missing | DirectDirectoryState::Unsafe => Ok(false),
1263            DirectDirectoryState::Real => remove_file_if_present(&self.selection_path()),
1264        }
1265    }
1266
1267    /// Exact user-owned logical pack root.
1268    fn storage_root(&self) -> Result<&Path, OptionalParserPackLifecycleError> {
1269        self.storage_root
1270            .get_or_init(|| default_storage_root().ok())
1271            .as_deref()
1272            .ok_or(OptionalParserPackLifecycleError::StorageRootUnavailable)
1273    }
1274
1275    /// Exact user-owned logical pack root, resolved only for storage operations.
1276    fn pack_root(&self) -> Result<PathBuf, OptionalParserPackLifecycleError> {
1277        Ok(self.storage_root()?.join(OPTIONAL_PARSER_PACK_ID))
1278    }
1279
1280    /// Version namespace containing immutable artifact slots.
1281    fn versions_root(&self) -> Result<PathBuf, OptionalParserPackLifecycleError> {
1282        Ok(self.pack_root()?.join("versions"))
1283    }
1284
1285    /// Exact path for one validated immutable slot identity.
1286    fn slot_path(
1287        &self,
1288        slot: &PackSlotIdentity,
1289    ) -> Result<PathBuf, OptionalParserPackLifecycleError> {
1290        Ok(self
1291            .versions_root()?
1292            .join(&slot.projectatlas_version)
1293            .join(&slot.artifact))
1294    }
1295
1296    /// Require every product-owned slot ancestor and the slot leaf to be a real directory.
1297    fn slot_path_is_real(
1298        &self,
1299        slot: &PackSlotIdentity,
1300    ) -> Result<bool, OptionalParserPackLifecycleError> {
1301        let pack_root = self.pack_root()?;
1302        let versions_root = self.versions_root()?;
1303        for component in [
1304            pack_root,
1305            versions_root.clone(),
1306            versions_root.join(&slot.projectatlas_version),
1307            self.slot_path(slot)?,
1308        ] {
1309            match direct_directory_state(&component)? {
1310                DirectDirectoryState::Real => {}
1311                DirectDirectoryState::Missing => return Ok(false),
1312                DirectDirectoryState::Unsafe => {
1313                    return Err(invalid_data(
1314                        "optional parser-pack slot path contains an unsafe owned component",
1315                    ));
1316                }
1317            }
1318        }
1319        Ok(true)
1320    }
1321
1322    /// Return content-free presence plus whether an unsafe owned component was observed.
1323    fn slot_presence(&self, slot: &PackSlotIdentity) -> (bool, bool) {
1324        match self.slot_path_is_real(slot) {
1325            Ok(present) => (present, false),
1326            Err(_) => (false, true),
1327        }
1328    }
1329
1330    /// Create and verify each product-owned storage component below the caller-owned anchor.
1331    fn ensure_storage_roots(&self) -> Result<(), OptionalParserPackLifecycleError> {
1332        let storage_root = self.storage_root()?;
1333        ensure_anchor_directory(storage_root)?;
1334        let pack_root = self.pack_root()?;
1335        ensure_direct_directory(storage_root, &pack_root)?;
1336        ensure_direct_directory(&pack_root, &self.versions_root()?)
1337    }
1338
1339    /// Acquire one nonblocking shared execution or exclusive storage-mutation lease.
1340    fn acquire_pack_lease(
1341        &self,
1342        mode: OptionalParserPackLeaseMode,
1343    ) -> Result<OptionalParserPackLease, OptionalParserPackLifecycleError> {
1344        let storage_root = self.storage_root()?;
1345        ensure_anchor_directory(storage_root)?;
1346        let path = storage_root.join(OPTIONAL_PARSER_PACK_LEASE_FILE_NAME);
1347        let file = open_or_create_direct_lease_file(&path)?;
1348        let result = match mode {
1349            OptionalParserPackLeaseMode::Shared => file.try_lock_shared(),
1350            OptionalParserPackLeaseMode::Exclusive => file.try_lock(),
1351        };
1352        match result {
1353            Ok(()) => {
1354                require_direct_lease_path(&path)?;
1355                Ok(OptionalParserPackLease { file })
1356            }
1357            Err(fs::TryLockError::WouldBlock) => {
1358                Err(OptionalParserPackLifecycleError::Busy { path })
1359            }
1360            Err(fs::TryLockError::Error(source)) => Err(io_error(
1361                "lock optional parser-pack lifecycle",
1362                path,
1363                source,
1364            )),
1365        }
1366    }
1367
1368    #[cfg(test)]
1369    fn for_test(
1370        project_root: PathBuf,
1371        storage_root: PathBuf,
1372        platform: Option<PackPlatform>,
1373    ) -> Self {
1374        let capability = platform.map_or(OptionalParserCapability::BuiltInOnly, |platform| {
1375            OptionalParserCapability::Pack { platform }
1376        });
1377        Self {
1378            project_root,
1379            storage_root: OnceLock::from(Some(storage_root)),
1380            capability,
1381            admission_failure: None,
1382            selection_publication_failure: false,
1383        }
1384    }
1385
1386    /// Inject one deterministic admission failure without exposing a production seam.
1387    #[cfg(test)]
1388    fn with_admission_failure(mut self, failure: fn(&Path) -> ParserSupervisorError) -> Self {
1389        self.admission_failure = Some(failure);
1390        self
1391    }
1392
1393    /// Inject one deterministic selection-publication failure without a production seam.
1394    #[cfg(test)]
1395    fn with_selection_publication_failure(mut self) -> Self {
1396        self.selection_publication_failure = true;
1397        self
1398    }
1399
1400    /// Return one injected admission failure before any lifecycle mutation.
1401    #[cfg(test)]
1402    fn fail_admission_if_injected(
1403        &self,
1404        artifact: &Path,
1405    ) -> Result<(), OptionalParserPackLifecycleError> {
1406        match self.admission_failure {
1407            Some(failure) => Err(failure(artifact).into()),
1408            None => Ok(()),
1409        }
1410    }
1411}
1412
1413/// Strict current-project selection record.
1414#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1415#[serde(deny_unknown_fields)]
1416struct ProjectSelection {
1417    /// Strict lifecycle selection schema.
1418    schema_version: u32,
1419    /// Stable identity of the one supported logical pack.
1420    pack_id: String,
1421    /// Currently enabled immutable slot.
1422    selected: PackSlotIdentity,
1423    /// Previous slot retained by a successful update.
1424    #[serde(skip_serializing_if = "Option::is_none")]
1425    rollback: Option<PackSlotIdentity>,
1426}
1427
1428impl ProjectSelection {
1429    /// Construct one current-schema project selection.
1430    fn new(selected: PackSlotIdentity, rollback: Option<PackSlotIdentity>) -> Self {
1431        Self {
1432            schema_version: PROJECT_SELECTION_SCHEMA_VERSION,
1433            pack_id: OPTIONAL_PARSER_PACK_ID.to_owned(),
1434            selected,
1435            rollback,
1436        }
1437    }
1438
1439    /// Validate strict schema, logical identity, and distinct canonical slots.
1440    fn validate(&self) -> Result<(), OptionalParserPackLifecycleError> {
1441        if self.schema_version != PROJECT_SELECTION_SCHEMA_VERSION {
1442            return Err(invalid_data(
1443                "project parser-pack selection schema is unsupported",
1444            ));
1445        }
1446        if self.pack_id != OPTIONAL_PARSER_PACK_ID {
1447            return Err(invalid_data(
1448                "project parser-pack selection has another pack identity",
1449            ));
1450        }
1451        self.selected.validate()?;
1452        if let Some(rollback) = &self.rollback {
1453            rollback.validate()?;
1454            if rollback == &self.selected {
1455                return Err(invalid_data(
1456                    "project parser-pack rollback duplicates selected slot",
1457                ));
1458            }
1459        }
1460        Ok(())
1461    }
1462}
1463
1464/// Validated immutable slot identity retained in project metadata.
1465#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
1466#[serde(deny_unknown_fields)]
1467struct PackSlotIdentity {
1468    /// `ProjectAtlas` release namespace that owns the slot.
1469    projectatlas_version: String,
1470    /// BLAKE3 identity of exact artifact-manifest bytes.
1471    artifact: String,
1472}
1473
1474impl PackSlotIdentity {
1475    /// Validate one artifact identity in the current release namespace.
1476    fn current(artifact: &str) -> Result<Self, OptionalParserPackLifecycleError> {
1477        let slot = Self {
1478            projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_owned(),
1479            artifact: artifact.to_owned(),
1480        };
1481        slot.validate()?;
1482        Ok(slot)
1483    }
1484
1485    /// Validate release compatibility and canonical artifact digest.
1486    fn validate(&self) -> Result<(), OptionalParserPackLifecycleError> {
1487        if self.projectatlas_version != OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION {
1488            return Err(invalid_data(
1489                "parser-pack slot belongs to another ProjectAtlas release line",
1490            ));
1491        }
1492        ParserContentDigest::new(self.artifact.clone())
1493            .map_err(|source| invalid_data(source.to_string()))?;
1494        Ok(())
1495    }
1496
1497    /// Project one validated opaque selection key for derivation ownership.
1498    fn selection_key(
1499        &self,
1500    ) -> Result<OptionalParserPackSelectionKey, OptionalParserPackLifecycleError> {
1501        self.validate()?;
1502        let artifact = ParserArtifactIdentity::new(
1503            ParserContentDigest::new(self.artifact.clone())
1504                .map_err(|source| invalid_data(source.to_string()))?,
1505        );
1506        Ok(OptionalParserPackSelectionKey {
1507            value: format!(
1508                "{}:{}:{}",
1509                OPTIONAL_PARSER_PACK_ID, self.projectatlas_version, self.artifact
1510            ),
1511            projectatlas_version: self.projectatlas_version.clone(),
1512            artifact,
1513        })
1514    }
1515
1516    /// Reconstruct an internal slot identity from an already validated selection key.
1517    fn from_selection_key(selection: &OptionalParserPackSelectionKey) -> Self {
1518        Self {
1519            projectatlas_version: selection.projectatlas_version.clone(),
1520            artifact: selection.artifact.digest().as_str().to_owned(),
1521        }
1522    }
1523
1524    /// Project one content-free slot report.
1525    fn report(&self, present: bool) -> OptionalParserPackSlotReport {
1526        OptionalParserPackSlotReport {
1527            projectatlas_version: self.projectatlas_version.clone(),
1528            artifact: self.artifact.clone(),
1529            present,
1530        }
1531    }
1532}
1533
1534/// Verified extracted artifact kept alive until it is discarded or atomically published.
1535struct VerifiedArchive {
1536    /// Armed cleanup owner, declared before the temporary directory so fallback
1537    /// cleanup runs while the exact broker and manifest still exist.
1538    temporary_profile: TemporaryParserArtifactProfile,
1539    /// Temporary directory whose drop cleans an unpublished extraction.
1540    _directory: TempDir,
1541    /// Canonical extracted artifact root.
1542    pack_root: PathBuf,
1543    /// Identity of exact validated artifact-manifest bytes.
1544    artifact: ParserArtifactIdentity,
1545    /// Manifest-bound `ProjectAtlas` release line.
1546    projectatlas_version: String,
1547}
1548
1549impl VerifiedArchive {
1550    /// Construct the immutable slot identity for this verified archive.
1551    fn slot_identity(&self) -> PackSlotIdentity {
1552        PackSlotIdentity {
1553            projectatlas_version: self.projectatlas_version.clone(),
1554            artifact: self.artifact.digest().as_str().to_owned(),
1555        }
1556    }
1557
1558    /// Project one content-free slot report.
1559    fn slot_report(&self, present: bool) -> OptionalParserPackSlotReport {
1560        self.slot_identity().report(present)
1561    }
1562
1563    /// Complete temporary profile cleanup before the extraction is discarded.
1564    fn cleanup_profile(self) -> Result<(), OptionalParserPackLifecycleError> {
1565        let Self {
1566            temporary_profile,
1567            _directory: directory,
1568            ..
1569        } = self;
1570        let result = temporary_profile.cleanup();
1571        drop(directory);
1572        result
1573    }
1574
1575    /// Make the immutable installed slot the sole remaining profile owner.
1576    fn transfer_profile_to_installed_slot(&mut self) {
1577        self.temporary_profile.transfer_to_installed_slot();
1578    }
1579}
1580
1581/// One exact extracted file observation.
1582struct ObservedFile {
1583    /// Exact extracted file bytes.
1584    bytes: u64,
1585    /// SHA-256 of exact extracted file bytes.
1586    sha256: String,
1587}
1588
1589/// Temporary archive extraction and exact observed inventory.
1590struct ExtractedArchive {
1591    /// Temporary directory owning the extraction until publish or drop.
1592    directory: TempDir,
1593    /// Canonical root inside the temporary extraction.
1594    pack_root: PathBuf,
1595    /// Exact extracted files keyed by canonical artifact-relative path.
1596    observed: BTreeMap<String, ObservedFile>,
1597}
1598
1599/// Closed cleanup state for one lifecycle-owned slot or tombstone entry.
1600#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1601#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1602enum InstalledSlotCleanupState {
1603    /// Canonical deterministic installed slot awaiting an atomic removal transition.
1604    Installed,
1605    /// Unique tombstone whose artifact profile still requires idempotent cleanup.
1606    ProfilePending,
1607    /// Unique tombstone whose profile is already cleaned and only deletion remains.
1608    ProfileCleaned,
1609}
1610
1611/// One direct slot or unique tombstone retained for bounded cleanup.
1612#[derive(Clone, Debug)]
1613struct InstalledSlotPath {
1614    /// Direct version directory name, retained even when stale.
1615    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1616    projectatlas_version: String,
1617    /// Canonical artifact identity parsed from the slot or tombstone name.
1618    artifact: String,
1619    /// Exact lifecycle-owned direct slot or tombstone container.
1620    entry_root: PathBuf,
1621    /// Exact pack root while artifact/profile verification remains necessary.
1622    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1623    pack_root: Option<PathBuf>,
1624    /// Current closed cleanup transition state.
1625    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
1626    state: InstalledSlotCleanupState,
1627}
1628
1629impl InstalledSlotPath {
1630    /// Return a bounded content-free label for aggregate cleanup diagnostics.
1631    fn bounded_label(&self) -> String {
1632        self.artifact.chars().take(64).collect()
1633    }
1634}
1635
1636/// Reader that rejects decompression beyond one hard byte ceiling.
1637struct BoundedReader<R> {
1638    /// Wrapped decompressor.
1639    inner: R,
1640    /// Maximum bytes the reader may return.
1641    maximum: u64,
1642    /// Bytes returned so far.
1643    consumed: u64,
1644}
1645
1646impl<R> BoundedReader<R> {
1647    /// Wrap one decompressor with a hard returned-byte ceiling.
1648    const fn new(inner: R, maximum: u64) -> Self {
1649        Self {
1650            inner,
1651            maximum,
1652            consumed: 0,
1653        }
1654    }
1655}
1656
1657impl<R: Read> Read for BoundedReader<R> {
1658    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
1659        if buffer.is_empty() {
1660            return Ok(0);
1661        }
1662        if self.consumed >= self.maximum {
1663            let mut probe = [0u8; 1];
1664            return match self.inner.read(&mut probe)? {
1665                0 => Ok(0),
1666                _ => Err(io::Error::new(
1667                    io::ErrorKind::InvalidData,
1668                    "expanded archive exceeded its hard byte ceiling",
1669                )),
1670            };
1671        }
1672        let remaining = self.maximum.saturating_sub(self.consumed);
1673        let allowed = usize::try_from(remaining.min(buffer.len() as u64))
1674            .map_err(|source| io::Error::new(io::ErrorKind::InvalidData, source))?;
1675        let read = self.inner.read(&mut buffer[..allowed])?;
1676        self.consumed = self
1677            .consumed
1678            .checked_add(u64::try_from(read).map_err(io::Error::other)?)
1679            .ok_or_else(|| {
1680                io::Error::new(io::ErrorKind::InvalidData, "archive byte count overflowed")
1681            })?;
1682        Ok(read)
1683    }
1684}
1685
1686/// Stream and validate one canonical completed archive.
1687fn extract_archive(
1688    path: &Path,
1689    staging_parent: Option<&Path>,
1690) -> Result<ExtractedArchive, OptionalParserPackLifecycleError> {
1691    let metadata = fs::symlink_metadata(path)
1692        .map_err(|source| io_error("inspect optional parser-pack archive", path, source))?;
1693    if !metadata.file_type().is_file()
1694        || metadata.len() == 0
1695        || metadata.len() > OPTIONAL_PARSER_PACK_MAX_ARCHIVE_BYTES
1696    {
1697        return Err(invalid_data(
1698            "archive is not a bounded non-empty regular file",
1699        ));
1700    }
1701    let directory = match staging_parent {
1702        Some(parent) => TempDir::new_in(parent)
1703            .map_err(|source| io_error("create parser-pack staging directory", parent, source))?,
1704        None => TempDir::new().map_err(|source| {
1705            io_error("create parser-pack verification directory", path, source)
1706        })?,
1707    };
1708    let pack_root = directory.path().join(ARCHIVE_ROOT);
1709    fs::create_dir(&pack_root)
1710        .map_err(|source| io_error("create extracted parser-pack root", &pack_root, source))?;
1711    let input = File::open(path)
1712        .map_err(|source| io_error("open optional parser-pack archive", path, source))?;
1713    let decoder = zstd::Decoder::new(BufReader::new(input))
1714        .map_err(|source| io_error("decode optional parser-pack archive", path, source))?;
1715    let maximum_tar_bytes = OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES
1716        .checked_add(TAR_FRAMING_ALLOWANCE_BYTES)
1717        .ok_or_else(|| invalid_data("tar expansion bound overflowed"))?;
1718    let bounded = BoundedReader::new(decoder, maximum_tar_bytes);
1719    let mut archive = tar::Archive::new(bounded);
1720    let mut observed = BTreeMap::new();
1721    let mut previous_path: Option<String> = None;
1722    let mut expanded_bytes = 0u64;
1723    let entries = archive
1724        .entries()
1725        .map_err(|source| io_error("read optional parser-pack archive entries", path, source))?;
1726    for entry in entries {
1727        let mut entry = entry
1728            .map_err(|source| io_error("read optional parser-pack archive entry", path, source))?;
1729        if observed.len() >= OPTIONAL_PARSER_PACK_MAX_FILE_ENTRIES.saturating_add(1) {
1730            return Err(invalid_data("archive exceeded its file-entry ceiling"));
1731        }
1732        if !entry.header().entry_type().is_file() {
1733            return Err(invalid_data("archive contains a non-regular entry"));
1734        }
1735        let raw_path = entry.path_bytes();
1736        let archive_path = std::str::from_utf8(raw_path.as_ref())
1737            .map_err(|source| invalid_data(source.to_string()))?;
1738        let prefix = format!("{ARCHIVE_ROOT}/");
1739        let relative = archive_path
1740            .strip_prefix(&prefix)
1741            .ok_or_else(|| invalid_data("archive entry is outside the canonical pack root"))?;
1742        let relative = PackRelativePath::new(relative)?;
1743        if previous_path
1744            .as_ref()
1745            .is_some_and(|previous| previous.as_str() >= relative.as_str())
1746        {
1747            return Err(invalid_data(
1748                "archive entries are not strictly path-sorted and unique",
1749            ));
1750        }
1751        previous_path = Some(relative.as_str().to_owned());
1752        let bytes = entry
1753            .header()
1754            .size()
1755            .map_err(|source| io_error("read parser-pack entry size", path, source))?;
1756        if bytes == 0 || bytes > OPTIONAL_PARSER_PACK_MAX_FILE_BYTES {
1757            return Err(invalid_data(
1758                "archive entry is empty or exceeds its file bound",
1759            ));
1760        }
1761        let expected_mode = if matches!(
1762            relative.as_str(),
1763            "projectatlas-parser-worker" | "projectatlas-parser-worker.exe"
1764        ) {
1765            WORKER_MODE
1766        } else {
1767            PAYLOAD_MODE
1768        };
1769        let header = entry.header();
1770        if header
1771            .uid()
1772            .map_err(|source| invalid_data(source.to_string()))?
1773            != 0
1774            || header
1775                .gid()
1776                .map_err(|source| invalid_data(source.to_string()))?
1777                != 0
1778            || header
1779                .mtime()
1780                .map_err(|source| invalid_data(source.to_string()))?
1781                != 0
1782            || header
1783                .mode()
1784                .map_err(|source| invalid_data(source.to_string()))?
1785                != expected_mode
1786        {
1787            return Err(invalid_data("archive entry metadata is not canonical"));
1788        }
1789        expanded_bytes = expanded_bytes
1790            .checked_add(bytes)
1791            .ok_or_else(|| invalid_data("expanded payload byte count overflowed"))?;
1792        if expanded_bytes > OPTIONAL_PARSER_PACK_MAX_EXPANDED_BYTES {
1793            return Err(invalid_data(
1794                "archive exceeded its expanded payload ceiling",
1795            ));
1796        }
1797        let destination = pack_root.join(Path::new(relative.as_str()));
1798        if let Some(parent) = destination.parent() {
1799            fs::create_dir_all(parent).map_err(|source| {
1800                io_error("create parser-pack payload directory", parent, source)
1801            })?;
1802        }
1803        let mut output = OpenOptions::new()
1804            .write(true)
1805            .create_new(true)
1806            .open(&destination)
1807            .map_err(|source| {
1808                io_error("create extracted parser-pack file", &destination, source)
1809            })?;
1810        let mut hasher = Sha256::new();
1811        let copied = copy_and_hash(&mut entry, &mut output, &mut hasher)?;
1812        if copied != bytes {
1813            return Err(invalid_data(
1814                "archive entry size differs from its tar header",
1815            ));
1816        }
1817        output
1818            .sync_all()
1819            .map_err(|source| io_error("sync extracted parser-pack file", &destination, source))?;
1820        #[cfg(unix)]
1821        set_extracted_mode(&destination, expected_mode)?;
1822        let key = relative.as_str().to_owned();
1823        if observed
1824            .insert(
1825                key,
1826                ObservedFile {
1827                    bytes,
1828                    sha256: lowercase_hex(hasher.finalize().as_ref()),
1829                },
1830            )
1831            .is_some()
1832        {
1833            return Err(invalid_data("archive contains a duplicate payload path"));
1834        }
1835    }
1836    let mut bounded = archive.into_inner();
1837    require_zero_tar_padding(&mut bounded)?;
1838    Ok(ExtractedArchive {
1839        directory,
1840        pack_root,
1841        observed,
1842    })
1843}
1844
1845/// Require exact manifest-listed files plus the self-excluded artifact manifest.
1846fn validate_observed_inventory(
1847    observed: &BTreeMap<String, ObservedFile>,
1848    artifact: &OptionalParserPackArtifactManifest,
1849) -> Result<(), OptionalParserPackLifecycleError> {
1850    let expected_count = artifact
1851        .files
1852        .len()
1853        .checked_add(1)
1854        .ok_or_else(|| invalid_data("expected artifact file count overflowed"))?;
1855    if observed.len() != expected_count {
1856        return Err(invalid_data(format!(
1857            "artifact contains {} files; expected {expected_count}",
1858            observed.len()
1859        )));
1860    }
1861    for file in &artifact.files {
1862        let actual = observed
1863            .get(file.path.as_str())
1864            .ok_or_else(|| invalid_data(format!("artifact is missing {:?}", file.path.as_str())))?;
1865        if actual.bytes != file.bytes || actual.sha256 != file.sha256.as_str() {
1866            return Err(invalid_data(format!(
1867                "payload {:?} differs from its artifact manifest",
1868                file.path.as_str()
1869            )));
1870        }
1871    }
1872    if !observed.contains_key(ARTIFACT_MANIFEST_FILE_NAME) {
1873        return Err(invalid_data(
1874            "artifact manifest is missing from archive inventory",
1875        ));
1876    }
1877    Ok(())
1878}
1879
1880/// Require the published release archive basename for the current platform.
1881fn require_archive_name(
1882    path: &Path,
1883    projectatlas_version: &str,
1884    platform: PackPlatform,
1885) -> Result<(), OptionalParserPackLifecycleError> {
1886    let expected = format!(
1887        "projectatlas-v{projectatlas_version}-broad-parser-{}.tar.zst",
1888        platform.as_str()
1889    );
1890    if path.file_name().and_then(std::ffi::OsStr::to_str) != Some(expected.as_str()) {
1891        return Err(invalid_data(format!(
1892            "archive basename must be {expected:?} for {}",
1893            platform.as_str()
1894        )));
1895    }
1896    Ok(())
1897}
1898
1899/// Hash one bounded non-empty regular file for TOCTOU comparison.
1900fn sha256_file(
1901    path: &Path,
1902    maximum: u64,
1903) -> Result<(String, u64), OptionalParserPackLifecycleError> {
1904    let metadata = fs::symlink_metadata(path)
1905        .map_err(|source| io_error("inspect bounded lifecycle file", path, source))?;
1906    if !metadata.file_type().is_file() || metadata.len() == 0 || metadata.len() > maximum {
1907        return Err(invalid_data(
1908            "lifecycle file is not a bounded non-empty regular file",
1909        ));
1910    }
1911    let mut input = BufReader::new(
1912        File::open(path).map_err(|source| io_error("open bounded lifecycle file", path, source))?,
1913    );
1914    let mut hasher = Sha256::new();
1915    let mut buffer = vec![0u8; 64 * 1024].into_boxed_slice();
1916    let mut total = 0u64;
1917    loop {
1918        let read = input
1919            .read(&mut buffer)
1920            .map_err(|source| io_error("hash bounded lifecycle file", path, source))?;
1921        if read == 0 {
1922            break;
1923        }
1924        hasher.update(&buffer[..read]);
1925        total = total
1926            .checked_add(u64::try_from(read).map_err(|source| invalid_data(source.to_string()))?)
1927            .ok_or_else(|| invalid_data("lifecycle file byte count overflowed"))?;
1928        if total > maximum {
1929            return Err(invalid_data("lifecycle file exceeded its byte ceiling"));
1930        }
1931    }
1932    if total != metadata.len() {
1933        return Err(invalid_data(
1934            "lifecycle file changed while it was being hashed",
1935        ));
1936    }
1937    Ok((lowercase_hex(hasher.finalize().as_ref()), total))
1938}
1939
1940/// Read one exact bounded non-empty regular file.
1941fn read_bounded_file(
1942    path: &Path,
1943    maximum: u64,
1944) -> Result<Vec<u8>, OptionalParserPackLifecycleError> {
1945    let metadata = fs::symlink_metadata(path)
1946        .map_err(|source| io_error("inspect bounded lifecycle file", path, source))?;
1947    if !metadata.file_type().is_file() || metadata.len() == 0 || metadata.len() > maximum {
1948        return Err(invalid_data(
1949            "lifecycle file is not a bounded non-empty regular file",
1950        ));
1951    }
1952    let capacity =
1953        usize::try_from(metadata.len()).map_err(|source| invalid_data(source.to_string()))?;
1954    let mut bytes = Vec::with_capacity(capacity);
1955    File::open(path)
1956        .map_err(|source| io_error("open bounded lifecycle file", path, source))?
1957        .take(maximum.saturating_add(1))
1958        .read_to_end(&mut bytes)
1959        .map_err(|source| io_error("read bounded lifecycle file", path, source))?;
1960    if bytes.len() != capacity {
1961        return Err(invalid_data(
1962            "lifecycle file changed while it was being read",
1963        ));
1964    }
1965    Ok(bytes)
1966}
1967
1968/// Copy one archive entry while hashing exact bytes.
1969fn copy_and_hash(
1970    input: &mut impl Read,
1971    output: &mut File,
1972    hasher: &mut Sha256,
1973) -> Result<u64, OptionalParserPackLifecycleError> {
1974    let mut buffer = vec![0u8; 64 * 1024].into_boxed_slice();
1975    let mut total = 0u64;
1976    loop {
1977        let read = input.read(&mut buffer).map_err(|source| {
1978            io_error(
1979                "read parser-pack archive entry",
1980                Path::new(ARCHIVE_ROOT),
1981                source,
1982            )
1983        })?;
1984        if read == 0 {
1985            break;
1986        }
1987        output.write_all(&buffer[..read]).map_err(|source| {
1988            io_error(
1989                "write extracted parser-pack file",
1990                Path::new(ARCHIVE_ROOT),
1991                source,
1992            )
1993        })?;
1994        hasher.update(&buffer[..read]);
1995        total = total
1996            .checked_add(u64::try_from(read).map_err(|source| invalid_data(source.to_string()))?)
1997            .ok_or_else(|| invalid_data("archive entry byte count overflowed"))?;
1998        if total > OPTIONAL_PARSER_PACK_MAX_FILE_BYTES {
1999            return Err(invalid_data("archive entry exceeded its file byte ceiling"));
2000        }
2001    }
2002    Ok(total)
2003}
2004
2005/// Require canonical zero padding after the tar terminator.
2006fn require_zero_tar_padding(input: &mut impl Read) -> Result<(), OptionalParserPackLifecycleError> {
2007    let mut buffer = vec![0u8; 64 * 1024].into_boxed_slice();
2008    loop {
2009        let read = input.read(&mut buffer).map_err(|source| {
2010            io_error(
2011                "read parser-pack tar padding",
2012                Path::new(ARCHIVE_ROOT),
2013                source,
2014            )
2015        })?;
2016        if read == 0 {
2017            return Ok(());
2018        }
2019        if buffer[..read].iter().any(|byte| *byte != 0) {
2020            return Err(invalid_data(
2021                "archive contains non-zero data after its tar terminator",
2022            ));
2023        }
2024    }
2025}
2026
2027/// Enumerate exact canonical installed slots under the lifecycle-owned namespace.
2028fn installed_slot_paths(
2029    pack_root: &Path,
2030) -> Result<Vec<InstalledSlotPath>, OptionalParserPackLifecycleError> {
2031    match fs::symlink_metadata(pack_root) {
2032        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
2033        Ok(metadata) if metadata.file_type().is_dir() => {}
2034        Ok(_) => return Ok(Vec::new()),
2035        Err(source) => {
2036            return Err(io_error(
2037                "inspect parser-pack storage root",
2038                pack_root,
2039                source,
2040            ));
2041        }
2042    }
2043    let versions = pack_root.join("versions");
2044    match fs::symlink_metadata(&versions) {
2045        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
2046        Ok(metadata) if metadata.file_type().is_dir() => {}
2047        Ok(_) => return Ok(Vec::new()),
2048        Err(source) => {
2049            return Err(io_error(
2050                "inspect parser-pack versions root",
2051                versions,
2052                source,
2053            ));
2054        }
2055    }
2056    let mut slots = Vec::new();
2057    let mut observed_entries = 0usize;
2058    for version in fs::read_dir(&versions)
2059        .map_err(|source| io_error("list parser-pack versions", &versions, source))?
2060    {
2061        if observed_entries == LIFECYCLE_METADATA_ENTRY_LIMIT {
2062            return Err(invalid_data(
2063                "parser-pack metadata entries exceed the cleanup bound",
2064            ));
2065        }
2066        observed_entries = observed_entries.saturating_add(1);
2067        let version = version
2068            .map_err(|source| io_error("read parser-pack version entry", &versions, source))?;
2069        if !version
2070            .file_type()
2071            .map_err(|source| {
2072                io_error("inspect parser-pack version entry", version.path(), source)
2073            })?
2074            .is_dir()
2075        {
2076            continue;
2077        }
2078        let projectatlas_version_name = version
2079            .file_name()
2080            .into_string()
2081            .map_err(|_name| invalid_data("parser-pack version directory is not UTF-8"))?;
2082        for slot in fs::read_dir(version.path())
2083            .map_err(|source| io_error("list parser-pack slots", version.path(), source))?
2084        {
2085            if observed_entries == LIFECYCLE_METADATA_ENTRY_LIMIT {
2086                return Err(invalid_data(
2087                    "parser-pack metadata entries exceed the cleanup bound",
2088                ));
2089            }
2090            observed_entries = observed_entries.saturating_add(1);
2091            let slot = slot.map_err(|source| {
2092                io_error("read parser-pack slot entry", version.path(), source)
2093            })?;
2094            if !slot
2095                .file_type()
2096                .map_err(|source| io_error("inspect parser-pack slot entry", slot.path(), source))?
2097                .is_dir()
2098            {
2099                continue;
2100            }
2101            let entry_name = slot
2102                .file_name()
2103                .into_string()
2104                .map_err(|_name| invalid_data("parser-pack artifact directory is not UTF-8"))?;
2105            #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2106            if let Some((state, artifact_prefix)) = parse_windows_tombstone_name(&entry_name) {
2107                let entry_root = slot.path();
2108                let artifact = windows_tombstone_artifact(&entry_root, &artifact_prefix)?;
2109                slots.push(InstalledSlotPath {
2110                    projectatlas_version: projectatlas_version_name.clone(),
2111                    artifact,
2112                    pack_root: (state == InstalledSlotCleanupState::ProfilePending)
2113                        .then(|| entry_root.clone()),
2114                    entry_root,
2115                    state,
2116                });
2117                continue;
2118            }
2119            let entry_root = slot.path();
2120            slots.push(InstalledSlotPath {
2121                #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2122                projectatlas_version: projectatlas_version_name.clone(),
2123                artifact: entry_name,
2124                #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2125                pack_root: Some(entry_root.clone()),
2126                entry_root,
2127                #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2128                state: InstalledSlotCleanupState::Installed,
2129            });
2130        }
2131        #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
2132        drop(projectatlas_version_name);
2133    }
2134    Ok(slots)
2135}
2136
2137/// Parse only unique lifecycle-owned Windows tombstone directory names.
2138#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2139fn parse_windows_tombstone_name(name: &str) -> Option<(InstalledSlotCleanupState, String)> {
2140    let (state, remainder) =
2141        if let Some(remainder) = name.strip_prefix(WINDOWS_REMOVING_TOMBSTONE_PREFIX) {
2142            (InstalledSlotCleanupState::ProfilePending, remainder)
2143        } else {
2144            let remainder = name.strip_prefix(WINDOWS_CLEANED_TOMBSTONE_PREFIX)?;
2145            (InstalledSlotCleanupState::ProfileCleaned, remainder)
2146        };
2147    if remainder.len() <= WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS {
2148        return None;
2149    }
2150    let (artifact_prefix, suffix) = remainder.split_at(WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS);
2151    if !artifact_prefix
2152        .bytes()
2153        .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
2154        || !suffix.starts_with('-')
2155        || suffix.len() < 2
2156        || suffix.len() > 33
2157        || !suffix[1..].bytes().all(|byte| byte.is_ascii_alphanumeric())
2158    {
2159        return None;
2160    }
2161    Some((state, artifact_prefix.to_owned()))
2162}
2163
2164/// Recover and validate one tombstone's exact artifact identity.
2165#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2166fn windows_tombstone_artifact(
2167    root: &Path,
2168    expected_prefix: &str,
2169) -> Result<String, OptionalParserPackLifecycleError> {
2170    let manifest_bytes = read_bounded_file(
2171        &root.join(ARTIFACT_MANIFEST_FILE_NAME),
2172        u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES)
2173            .map_err(|source| invalid_data(source.to_string()))?,
2174    )?;
2175    let artifact = ParserArtifactIdentity::for_bytes(&manifest_bytes)
2176        .digest()
2177        .as_str()
2178        .to_owned();
2179    if !artifact.starts_with(expected_prefix) {
2180        return Err(invalid_data(
2181            "parser-pack cleanup tombstone differs from its artifact manifest",
2182        ));
2183    }
2184    Ok(artifact)
2185}
2186
2187/// Return whether one artifact has any bounded profile-removal tombstone.
2188#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2189fn windows_slot_cleanup_in_progress(
2190    version_root: &Path,
2191    artifact: &str,
2192) -> Result<bool, OptionalParserPackLifecycleError> {
2193    ParserContentDigest::new(artifact.to_owned())
2194        .map_err(|source| invalid_data(source.to_string()))?;
2195    let entries = fs::read_dir(version_root)
2196        .map_err(|source| io_error("list parser-pack cleanup tombstones", version_root, source))?;
2197    for (index, entry) in entries.enumerate() {
2198        if index == LIFECYCLE_METADATA_ENTRY_LIMIT {
2199            return Err(invalid_data(
2200                "parser-pack cleanup tombstones exceed the lifecycle bound",
2201            ));
2202        }
2203        let entry = entry.map_err(|source| {
2204            io_error("read parser-pack cleanup tombstone", version_root, source)
2205        })?;
2206        if !entry
2207            .file_type()
2208            .map_err(|source| {
2209                io_error(
2210                    "inspect parser-pack cleanup tombstone",
2211                    entry.path(),
2212                    source,
2213                )
2214            })?
2215            .is_dir()
2216        {
2217            continue;
2218        }
2219        let Ok(name) = entry.file_name().into_string() else {
2220            continue;
2221        };
2222        if let Some((_state, artifact_prefix)) = parse_windows_tombstone_name(&name)
2223            && artifact.starts_with(&artifact_prefix)
2224            && windows_tombstone_artifact(&entry.path(), &artifact_prefix)? == artifact
2225        {
2226            return Ok(true);
2227        }
2228    }
2229    Ok(false)
2230}
2231
2232/// Atomically move a deterministic slot into one unique profile-pending tombstone.
2233#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2234fn transition_slot_to_removing_tombstone(
2235    slot: &InstalledSlotPath,
2236) -> Result<InstalledSlotPath, OptionalParserPackLifecycleError> {
2237    if slot.state != InstalledSlotCleanupState::Installed {
2238        return Err(invalid_data(
2239            "only an installed slot can enter the removing tombstone state",
2240        ));
2241    }
2242    ParserContentDigest::new(slot.artifact.clone())
2243        .map_err(|source| invalid_data(source.to_string()))?;
2244    let parent = slot
2245        .entry_root
2246        .parent()
2247        .ok_or_else(|| invalid_data("parser-pack slot has no version parent"))?;
2248    let tombstone_prefix = format!(
2249        "{WINDOWS_REMOVING_TOMBSTONE_PREFIX}{}-",
2250        &slot.artifact[..WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS]
2251    );
2252    let reservation = tempfile::Builder::new()
2253        .prefix(&tombstone_prefix)
2254        .tempfile_in(parent)
2255        .map_err(|source| io_error("reserve unique parser-pack tombstone", parent, source))?;
2256    let tombstone = reservation.path().to_path_buf();
2257    if let Err(error) = require_windows_cleanup_paths(&slot.entry_root, &tombstone) {
2258        reservation.close().map_err(|source| {
2259            io_error(
2260                "release unsupported parser-pack tombstone reservation",
2261                &tombstone,
2262                source,
2263            )
2264        })?;
2265        return Err(error);
2266    }
2267    reservation.close().map_err(|source| {
2268        io_error(
2269            "release parser-pack tombstone reservation",
2270            &tombstone,
2271            source,
2272        )
2273    })?;
2274    fs::rename(&slot.entry_root, &tombstone).map_err(|source| {
2275        io_error(
2276            "move parser-pack slot into removing tombstone",
2277            &slot.entry_root,
2278            source,
2279        )
2280    })?;
2281    Ok(InstalledSlotPath {
2282        projectatlas_version: slot.projectatlas_version.clone(),
2283        artifact: slot.artifact.clone(),
2284        entry_root: tombstone.clone(),
2285        pack_root: Some(tombstone),
2286        state: InstalledSlotCleanupState::ProfilePending,
2287    })
2288}
2289
2290/// Atomically record successful profile cleanup in the unique tombstone path itself.
2291#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2292fn transition_tombstone_to_profile_cleaned(
2293    slot: &InstalledSlotPath,
2294) -> Result<InstalledSlotPath, OptionalParserPackLifecycleError> {
2295    if slot.state != InstalledSlotCleanupState::ProfilePending {
2296        return Err(invalid_data(
2297            "only a profile-pending tombstone can become profile-cleaned",
2298        ));
2299    }
2300    let name = slot
2301        .entry_root
2302        .file_name()
2303        .and_then(|name| name.to_str())
2304        .ok_or_else(|| invalid_data("parser-pack cleanup tombstone name is not UTF-8"))?;
2305    let remainder = name
2306        .strip_prefix(WINDOWS_REMOVING_TOMBSTONE_PREFIX)
2307        .ok_or_else(|| invalid_data("parser-pack removing tombstone prefix is invalid"))?;
2308    let parent = slot
2309        .entry_root
2310        .parent()
2311        .ok_or_else(|| invalid_data("parser-pack cleanup tombstone has no parent"))?;
2312    let cleaned_root = parent.join(format!("{WINDOWS_CLEANED_TOMBSTONE_PREFIX}{remainder}"));
2313    require_windows_cleanup_paths(&slot.entry_root, &cleaned_root)?;
2314    if fs::symlink_metadata(&cleaned_root).is_ok() {
2315        return Err(invalid_data(
2316            "profile-cleaned parser-pack tombstone path is already occupied",
2317        ));
2318    }
2319    fs::rename(&slot.entry_root, &cleaned_root).map_err(|source| {
2320        io_error(
2321            "record parser-pack profile cleanup in tombstone state",
2322            &slot.entry_root,
2323            source,
2324        )
2325    })?;
2326    Ok(InstalledSlotPath {
2327        projectatlas_version: slot.projectatlas_version.clone(),
2328        artifact: slot.artifact.clone(),
2329        entry_root: cleaned_root,
2330        pack_root: None,
2331        state: InstalledSlotCleanupState::ProfileCleaned,
2332    })
2333}
2334
2335/// Convert one canonical absolute Windows path to extended-length syntax.
2336#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2337fn windows_verbatim_path(path: &Path) -> PathBuf {
2338    use std::ffi::OsString;
2339    use std::os::windows::ffi::{OsStrExt as _, OsStringExt as _};
2340
2341    const DIRECTORY_SEPARATOR: u16 = b'\\' as u16;
2342    const VERBATIM_PREFIX: &[u16] = &[
2343        DIRECTORY_SEPARATOR,
2344        DIRECTORY_SEPARATOR,
2345        b'?' as u16,
2346        DIRECTORY_SEPARATOR,
2347    ];
2348    const VERBATIM_UNC_PREFIX: &[u16] = &[
2349        DIRECTORY_SEPARATOR,
2350        DIRECTORY_SEPARATOR,
2351        b'?' as u16,
2352        DIRECTORY_SEPARATOR,
2353        b'U' as u16,
2354        b'N' as u16,
2355        b'C' as u16,
2356        DIRECTORY_SEPARATOR,
2357    ];
2358
2359    let path = path.as_os_str().encode_wide().collect::<Vec<_>>();
2360    if path.starts_with(VERBATIM_PREFIX) {
2361        return PathBuf::from(OsString::from_wide(&path));
2362    }
2363    let (prefix, suffix) = if path.starts_with(&[DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR]) {
2364        (VERBATIM_UNC_PREFIX, &path[2..])
2365    } else {
2366        (VERBATIM_PREFIX, path.as_slice())
2367    };
2368    let mut extended = Vec::with_capacity(prefix.len().saturating_add(suffix.len()));
2369    extended.extend_from_slice(prefix);
2370    extended.extend_from_slice(suffix);
2371    PathBuf::from(OsString::from_wide(&extended))
2372}
2373
2374/// Require the non-verbatim path form accepted by the .NET Framework cleanup broker.
2375#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2376fn require_windows_framework_path(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
2377    use std::os::windows::ffi::OsStrExt as _;
2378
2379    let path = path.as_os_str().encode_wide().collect::<Vec<_>>();
2380    if path.starts_with(&[
2381        u16::from(b'\\'),
2382        u16::from(b'\\'),
2383        u16::from(b'?'),
2384        u16::from(b'\\'),
2385    ]) || path.len() > WINDOWS_PROCESS_CURRENT_DIRECTORY_MAX_UTF16_UNITS
2386    {
2387        return Err(invalid_data(
2388            "Windows parser-pack cleanup path exceeds the .NET Framework path contract",
2389        ));
2390    }
2391    Ok(())
2392}
2393
2394/// Validate every retained path before the legacy broker traverses the pack.
2395#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2396fn require_windows_cleanup_paths(
2397    source_root: &Path,
2398    cleanup_root: &Path,
2399) -> Result<(), OptionalParserPackLifecycleError> {
2400    let mut pending = vec![(source_root.to_path_buf(), cleanup_root.to_path_buf())];
2401    let mut observed = 0_usize;
2402    while let Some((source, cleanup)) = pending.pop() {
2403        if observed == LIFECYCLE_METADATA_ENTRY_LIMIT {
2404            return Err(invalid_data(
2405                "parser-pack cleanup tree exceeds the lifecycle bound",
2406            ));
2407        }
2408        observed = observed.saturating_add(1);
2409        require_windows_framework_path(&cleanup)?;
2410        let metadata = fs::symlink_metadata(&source)
2411            .map_err(|error| io_error("inspect parser-pack cleanup path", &source, error))?;
2412        if metadata.is_dir() && !metadata.file_type().is_symlink() {
2413            for entry in fs::read_dir(&source)
2414                .map_err(|error| io_error("list parser-pack cleanup path", &source, error))?
2415            {
2416                let entry = entry
2417                    .map_err(|error| io_error("read parser-pack cleanup path", &source, error))?;
2418                pending.push((entry.path(), cleanup.join(entry.file_name())));
2419            }
2420        }
2421    }
2422    Ok(())
2423}
2424
2425/// Run the exact verified Windows broker cleanup command for its own artifact directory.
2426#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2427fn cleanup_platform_profile(
2428    root: &Path,
2429    artifact: &ParserArtifactIdentity,
2430) -> Result<(), OptionalParserPackLifecycleError> {
2431    use std::os::windows::process::CommandExt as _;
2432
2433    const CREATE_NO_WINDOW: u32 = 0x0800_0000;
2434    let manifest_path = root.join(ARTIFACT_MANIFEST_FILE_NAME);
2435    let manifest_bytes = read_bounded_file(
2436        &manifest_path,
2437        u64::try_from(OPTIONAL_PARSER_PACK_MANIFEST_MAX_BYTES)
2438            .map_err(|source| invalid_data(source.to_string()))?,
2439    )?;
2440    let observed = ParserArtifactIdentity::for_bytes(&manifest_bytes);
2441    if &observed != artifact {
2442        return Err(invalid_data(
2443            "artifact manifest changed before profile cleanup",
2444        ));
2445    }
2446    let sha256 = Sha256::digest(&manifest_bytes);
2447    let profile_name = format!("projectatlas.parser.{}", lowercase_hex(&sha256[..20]));
2448    require_windows_cleanup_paths(root, root)?;
2449    let broker = windows_verbatim_path(&root.join(WINDOWS_CONTAINMENT_BROKER_FILE_NAME));
2450    let windows_directory = validated_windows_directory()?;
2451    let mut command = Command::new(&broker);
2452    command
2453        .arg(WINDOWS_PROFILE_CLEANUP_ARGUMENT)
2454        .current_dir(root)
2455        .env_clear()
2456        .env("SystemRoot", &windows_directory)
2457        .env("WINDIR", &windows_directory)
2458        .stdin(Stdio::null())
2459        .stdout(Stdio::piped())
2460        .stderr(Stdio::piped())
2461        .creation_flags(CREATE_NO_WINDOW);
2462    let mut child = command
2463        .spawn()
2464        .map_err(|source| io_error("start artifact profile cleanup broker", &broker, source))?;
2465    supervise_cleanup_broker(
2466        &mut child,
2467        &broker,
2468        &profile_name,
2469        WINDOWS_PROFILE_CLEANUP_TIMEOUT,
2470        WINDOWS_PROFILE_CLEANUP_REAP_TIMEOUT,
2471    )
2472}
2473
2474/// Non-Windows hosts have no durable artifact profile to remove.
2475#[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
2476// Keep one fallible cross-platform cleanup contract for the RAII owner; only
2477// the Windows implementation can fail because only Windows creates a profile.
2478#[allow(clippy::unnecessary_wraps)]
2479fn cleanup_platform_profile(
2480    _root: &Path,
2481    _artifact: &ParserArtifactIdentity,
2482) -> Result<(), OptionalParserPackLifecycleError> {
2483    Ok(())
2484}
2485
2486/// Own one cleanup child through bounded operation, termination, reap, and pipe drain.
2487#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2488fn supervise_cleanup_broker(
2489    child: &mut std::process::Child,
2490    broker: &Path,
2491    profile_name: &str,
2492    operation_timeout: Duration,
2493    cleanup_timeout: Duration,
2494) -> Result<(), OptionalParserPackLifecycleError> {
2495    let mut operation_failure = None;
2496    let stdout_reader = if let Some(stdout) = child.stdout.take() {
2497        Some(thread::spawn(move || read_bounded_cleanup_output(stdout)))
2498    } else {
2499        operation_failure = Some(invalid_data("profile cleanup broker stdout was not piped"));
2500        None
2501    };
2502    let stderr_reader = if let Some(stderr) = child.stderr.take() {
2503        Some(thread::spawn(move || read_bounded_cleanup_output(stderr)))
2504    } else {
2505        operation_failure
2506            .get_or_insert_with(|| invalid_data("profile cleanup broker stderr was not piped"));
2507        None
2508    };
2509    let operation_deadline = Instant::now()
2510        .checked_add(operation_timeout)
2511        .unwrap_or_else(Instant::now);
2512    let mut status = None;
2513    while operation_failure.is_none() && status.is_none() {
2514        match child.try_wait() {
2515            Ok(observed) => status = observed,
2516            Err(source) => {
2517                operation_failure = Some(io_error(
2518                    "wait for artifact profile cleanup broker",
2519                    broker,
2520                    source,
2521                ));
2522            }
2523        }
2524        if status.is_none() && Instant::now() >= operation_deadline {
2525            operation_failure = Some(OptionalParserPackLifecycleError::CleanupIncomplete {
2526                message: format!("profile {profile_name} cleanup exceeded its deadline"),
2527            });
2528        }
2529        if operation_failure.is_none() && status.is_none() {
2530            thread::sleep(Duration::from_millis(10));
2531        }
2532    }
2533
2534    let cleanup_deadline = Instant::now()
2535        .checked_add(cleanup_timeout)
2536        .unwrap_or_else(Instant::now);
2537    let mut cleanup_failures = Vec::new();
2538    if status.is_none()
2539        && let Err(kill_source) = child.kill()
2540    {
2541        match child.try_wait() {
2542            Ok(Some(observed)) => status = Some(observed),
2543            Ok(None) => cleanup_failures.push(format!(
2544                "terminate cleanup broker failed: {kill_source}"
2545            )),
2546            Err(wait_source) => cleanup_failures.push(format!(
2547                "terminate cleanup broker failed: {kill_source}; observe child failed: {wait_source}"
2548            )),
2549        }
2550    }
2551    while status.is_none() && Instant::now() < cleanup_deadline {
2552        match child.try_wait() {
2553            Ok(observed) => status = observed,
2554            Err(source) => {
2555                cleanup_failures.push(format!("reap cleanup broker failed: {source}"));
2556                break;
2557            }
2558        }
2559        if status.is_none() {
2560            thread::sleep(Duration::from_millis(10));
2561        }
2562    }
2563    if status.is_none() {
2564        cleanup_failures
2565            .push("cleanup broker was not reaped within its cleanup deadline".to_owned());
2566    }
2567
2568    while (!cleanup_reader_finished(stdout_reader.as_ref())
2569        || !cleanup_reader_finished(stderr_reader.as_ref()))
2570        && Instant::now() < cleanup_deadline
2571    {
2572        thread::sleep(Duration::from_millis(10));
2573    }
2574    let stdout = join_cleanup_reader(stdout_reader, "stdout", broker, &mut cleanup_failures);
2575    let stderr = join_cleanup_reader(stderr_reader, "stderr", broker, &mut cleanup_failures);
2576
2577    if operation_failure.is_none() {
2578        let operation_result = match (status, stdout, stderr) {
2579            (Some(status), Some(Ok(stdout)), Some(Ok(stderr))) => match String::from_utf8(stdout) {
2580                Ok(stdout)
2581                    if status.success()
2582                        && stdout.trim_end() == WINDOWS_PROFILE_CLEANUP_RESULT
2583                        && stderr.is_empty() =>
2584                {
2585                    Ok(())
2586                }
2587                Ok(_) => Err(invalid_data(format!(
2588                    "profile {profile_name} cleanup broker returned an invalid bounded result"
2589                ))),
2590                Err(source) => Err(invalid_data(source.to_string())),
2591            },
2592            (_, Some(Err(source)), _) => Err(io_error(
2593                "read artifact profile cleanup stdout",
2594                broker,
2595                source,
2596            )),
2597            (_, _, Some(Err(source))) => Err(io_error(
2598                "read artifact profile cleanup stderr",
2599                broker,
2600                source,
2601            )),
2602            _ => Err(invalid_data(
2603                "profile cleanup broker result was incomplete after reap",
2604            )),
2605        };
2606        if let Err(error) = operation_result {
2607            operation_failure = Some(error);
2608        }
2609    }
2610
2611    let cleanup_failure = (!cleanup_failures.is_empty()).then(|| {
2612        OptionalParserPackLifecycleError::CleanupIncomplete {
2613            message: cleanup_failures.join("; ").chars().take(4_096).collect(),
2614        }
2615    });
2616    match (operation_failure, cleanup_failure) {
2617        (None, None) => Ok(()),
2618        (Some(operation), None) => Err(operation),
2619        (None, Some(cleanup)) => Err(cleanup),
2620        (Some(operation), Some(cleanup)) => {
2621            Err(OptionalParserPackLifecycleError::OperationAndCleanup {
2622                operation: Box::new(operation),
2623                cleanup: Box::new(cleanup),
2624            })
2625        }
2626    }
2627}
2628
2629/// Return whether one optional cleanup output reader has terminated.
2630#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2631fn cleanup_reader_finished(
2632    reader: Option<&thread::JoinHandle<Result<Vec<u8>, io::Error>>>,
2633) -> bool {
2634    reader.is_none_or(thread::JoinHandle::is_finished)
2635}
2636
2637/// Join one completed cleanup reader without hiding a mandatory drain failure.
2638#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2639fn join_cleanup_reader(
2640    reader: Option<thread::JoinHandle<Result<Vec<u8>, io::Error>>>,
2641    stream: &'static str,
2642    broker: &Path,
2643    cleanup_failures: &mut Vec<String>,
2644) -> Option<Result<Vec<u8>, io::Error>> {
2645    let reader = reader?;
2646    if !reader.is_finished() {
2647        cleanup_failures.push(format!(
2648            "cleanup broker {stream} reader did not drain within its cleanup deadline"
2649        ));
2650        return None;
2651    }
2652    match reader.join() {
2653        Ok(result) => Some(result),
2654        Err(_panic) => {
2655            cleanup_failures.push(format!(
2656                "cleanup broker {stream} reader panicked for {}",
2657                broker.display()
2658            ));
2659            None
2660        }
2661    }
2662}
2663
2664/// Resolve and validate the one Windows directory needed to initialize the managed broker.
2665#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2666fn validated_windows_directory() -> Result<PathBuf, OptionalParserPackLifecycleError> {
2667    let configured = env::var_os("SystemRoot")
2668        .or_else(|| env::var_os("WINDIR"))
2669        .ok_or_else(|| invalid_data("Windows directory environment is unavailable"))?;
2670    let path = PathBuf::from(configured);
2671    if !path.is_absolute() {
2672        return Err(invalid_data("Windows directory is not absolute"));
2673    }
2674    let metadata = fs::symlink_metadata(&path)
2675        .map_err(|source| io_error("inspect Windows directory", &path, source))?;
2676    if !metadata.file_type().is_dir() {
2677        return Err(invalid_data("Windows directory is not a real directory"));
2678    }
2679    fs::canonicalize(&path)
2680        .map_err(|source| io_error("canonicalize Windows directory", path, source))
2681}
2682
2683/// Read one trusted cleanup stream through a hard byte ceiling.
2684#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2685fn read_bounded_cleanup_output(input: impl Read) -> Result<Vec<u8>, io::Error> {
2686    let mut output = Vec::new();
2687    input
2688        .take(WINDOWS_PROFILE_CLEANUP_OUTPUT_BYTES.saturating_add(1))
2689        .read_to_end(&mut output)?;
2690    if u64::try_from(output.len()).unwrap_or(u64::MAX) > WINDOWS_PROFILE_CLEANUP_OUTPUT_BYTES {
2691        return Err(io::Error::new(
2692            io::ErrorKind::InvalidData,
2693            "profile cleanup output exceeded its byte ceiling",
2694        ));
2695    }
2696    Ok(output)
2697}
2698
2699/// Count immutable slot directory rows without opening any pack file.
2700fn count_installed_slots(
2701    pack_root: &Path,
2702) -> Result<(usize, bool, bool), OptionalParserPackLifecycleError> {
2703    let versions = pack_root.join("versions");
2704    match fs::symlink_metadata(pack_root) {
2705        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok((0, false, false)),
2706        Ok(metadata) if !metadata.file_type().is_dir() => {
2707            return Err(invalid_data("parser-pack storage root is not a directory"));
2708        }
2709        Ok(_) => {}
2710        Err(source) => {
2711            return Err(io_error(
2712                "inspect parser-pack storage root",
2713                pack_root,
2714                source,
2715            ));
2716        }
2717    }
2718    match fs::symlink_metadata(&versions) {
2719        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok((0, false, false)),
2720        Ok(metadata) if !metadata.file_type().is_dir() => {
2721            return Err(invalid_data("parser-pack versions root is not a directory"));
2722        }
2723        Ok(_) => {}
2724        Err(source) => {
2725            return Err(io_error(
2726                "inspect parser-pack versions root",
2727                versions,
2728                source,
2729            ));
2730        }
2731    }
2732    let mut count = 0usize;
2733    let mut observed_entries = 0usize;
2734    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2735    let mut cleanup_pending = false;
2736    #[cfg(not(all(target_os = "windows", target_arch = "x86_64")))]
2737    let cleanup_pending = false;
2738    let version_entries = fs::read_dir(&versions)
2739        .map_err(|source| io_error("list parser-pack versions", &versions, source))?;
2740    for version in version_entries {
2741        if observed_entries == LIFECYCLE_METADATA_ENTRY_LIMIT {
2742            return Ok((count, true, cleanup_pending));
2743        }
2744        observed_entries = observed_entries.saturating_add(1);
2745        let version = version
2746            .map_err(|source| io_error("read parser-pack version entry", &versions, source))?;
2747        if !version
2748            .file_type()
2749            .map_err(|source| {
2750                io_error("inspect parser-pack version entry", version.path(), source)
2751            })?
2752            .is_dir()
2753        {
2754            continue;
2755        }
2756        let slots = fs::read_dir(version.path())
2757            .map_err(|source| io_error("list parser-pack slots", version.path(), source))?;
2758        for slot in slots {
2759            if observed_entries == LIFECYCLE_METADATA_ENTRY_LIMIT {
2760                return Ok((count, true, cleanup_pending));
2761            }
2762            observed_entries = observed_entries.saturating_add(1);
2763            let slot = slot.map_err(|source| {
2764                io_error("read parser-pack slot entry", version.path(), source)
2765            })?;
2766            if !slot
2767                .file_type()
2768                .map_err(|source| io_error("inspect parser-pack slot entry", slot.path(), source))?
2769                .is_dir()
2770            {
2771                continue;
2772            }
2773            #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
2774            if slot
2775                .file_name()
2776                .to_str()
2777                .and_then(parse_windows_tombstone_name)
2778                .is_some()
2779            {
2780                cleanup_pending = true;
2781                continue;
2782            }
2783            count = count.saturating_add(1);
2784        }
2785    }
2786    Ok((count, false, cleanup_pending))
2787}
2788
2789/// Open or create the stable direct lease file without accepting an indirect leaf.
2790fn open_or_create_direct_lease_file(path: &Path) -> Result<File, OptionalParserPackLifecycleError> {
2791    loop {
2792        match fs::symlink_metadata(path) {
2793            Ok(metadata) => {
2794                require_direct_lease_file(path, &metadata)?;
2795                let file = OpenOptions::new()
2796                    .read(true)
2797                    .write(true)
2798                    .open(path)
2799                    .map_err(|source| {
2800                        io_error("open optional parser-pack lifecycle lease", path, source)
2801                    })?;
2802                require_direct_lease_path(path)?;
2803                return Ok(file);
2804            }
2805            Err(source) if source.kind() == io::ErrorKind::NotFound => {
2806                match OpenOptions::new()
2807                    .read(true)
2808                    .write(true)
2809                    .create_new(true)
2810                    .open(path)
2811                {
2812                    Ok(file) => {
2813                        require_direct_lease_path(path)?;
2814                        return Ok(file);
2815                    }
2816                    Err(source) if source.kind() == io::ErrorKind::AlreadyExists => {}
2817                    Err(source) => {
2818                        return Err(io_error(
2819                            "create optional parser-pack lifecycle lease",
2820                            path,
2821                            source,
2822                        ));
2823                    }
2824                }
2825            }
2826            Err(source) => {
2827                return Err(io_error(
2828                    "inspect optional parser-pack lifecycle lease",
2829                    path,
2830                    source,
2831                ));
2832            }
2833        }
2834    }
2835}
2836
2837/// Revalidate that the stable named lease remains a direct regular file.
2838fn require_direct_lease_path(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
2839    let metadata = fs::symlink_metadata(path).map_err(|source| {
2840        io_error(
2841            "revalidate optional parser-pack lifecycle lease",
2842            path,
2843            source,
2844        )
2845    })?;
2846    require_direct_lease_file(path, &metadata)?;
2847    Ok(())
2848}
2849
2850/// Reject symlink, Windows reparse-point, directory, and special-file lease leaves.
2851fn require_direct_lease_file(
2852    path: &Path,
2853    metadata: &fs::Metadata,
2854) -> Result<(), OptionalParserPackLifecycleError> {
2855    #[cfg(windows)]
2856    let indirect = {
2857        use std::os::windows::fs::MetadataExt as _;
2858
2859        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
2860        metadata.file_type().is_symlink()
2861            || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
2862    };
2863    #[cfg(not(windows))]
2864    let indirect = metadata.file_type().is_symlink();
2865    if indirect || !metadata.file_type().is_file() {
2866        return Err(invalid_data(format!(
2867            "optional parser-pack lifecycle lease is not a direct regular file: {}",
2868            path.display()
2869        )));
2870    }
2871    Ok(())
2872}
2873
2874/// Create the caller-selected storage anchor; its own ancestor policy belongs to the caller.
2875fn ensure_anchor_directory(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
2876    match fs::symlink_metadata(path) {
2877        Ok(metadata) if metadata.file_type().is_dir() => Ok(()),
2878        Ok(_) => Err(invalid_data(
2879            "parser-pack storage anchor is not a directory",
2880        )),
2881        Err(source) if source.kind() == io::ErrorKind::NotFound => fs::create_dir_all(path)
2882            .map_err(|source| io_error("create parser-pack storage anchor", path, source)),
2883        Err(source) => Err(io_error("inspect parser-pack storage anchor", path, source)),
2884    }
2885}
2886
2887/// Create one direct product-owned directory without following a symlink at that component.
2888fn ensure_direct_directory(
2889    parent: &Path,
2890    path: &Path,
2891) -> Result<(), OptionalParserPackLifecycleError> {
2892    if path.parent() != Some(parent) {
2893        return Err(invalid_data(
2894            "lifecycle directory is not a direct owned child",
2895        ));
2896    }
2897    match fs::symlink_metadata(path) {
2898        Ok(metadata) if metadata.file_type().is_dir() => Ok(()),
2899        Ok(_) => Err(invalid_data(
2900            "product-owned lifecycle component is not a real directory",
2901        )),
2902        Err(source) if source.kind() == io::ErrorKind::NotFound => fs::create_dir(path)
2903            .map_err(|source| io_error("create product-owned lifecycle directory", path, source)),
2904        Err(source) => Err(io_error(
2905            "inspect product-owned lifecycle directory",
2906            path,
2907            source,
2908        )),
2909    }
2910}
2911
2912/// Closed observation for one product-owned direct directory component.
2913#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2914enum DirectDirectoryState {
2915    /// The exact component is absent.
2916    Missing,
2917    /// The exact component is a real directory.
2918    Real,
2919    /// The exact component is a symlink, junction, or non-directory entry.
2920    Unsafe,
2921}
2922
2923/// Inspect one exact component without following a symlink at that component.
2924fn direct_directory_state(
2925    path: &Path,
2926) -> Result<DirectDirectoryState, OptionalParserPackLifecycleError> {
2927    match fs::symlink_metadata(path) {
2928        Ok(metadata) if metadata.file_type().is_dir() => Ok(DirectDirectoryState::Real),
2929        Ok(_) => Ok(DirectDirectoryState::Unsafe),
2930        Err(source) if source.kind() == io::ErrorKind::NotFound => {
2931            Ok(DirectDirectoryState::Missing)
2932        }
2933        Err(source) => Err(io_error(
2934            "inspect product-owned lifecycle component",
2935            path,
2936            source,
2937        )),
2938    }
2939}
2940
2941/// Remove one exact file or symlink without interpreting its content.
2942fn remove_file_if_present(path: &Path) -> Result<bool, OptionalParserPackLifecycleError> {
2943    match fs::symlink_metadata(path) {
2944        Ok(metadata) if metadata.file_type().is_dir() => Err(invalid_data(
2945            "project parser-pack selection path is a directory",
2946        )),
2947        Ok(_) => {
2948            make_path_writable(path)?;
2949            fs::remove_file(path)
2950                .map_err(|source| io_error("remove project parser-pack selection", path, source))?;
2951            Ok(true)
2952        }
2953        Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(false),
2954        Err(source) => Err(io_error(
2955            "inspect project parser-pack selection",
2956            path,
2957            source,
2958        )),
2959    }
2960}
2961
2962/// Remove one exact lifecycle-owned tree, or its symlink leaf, without following it.
2963fn remove_tree_if_present(path: &Path) -> Result<bool, OptionalParserPackLifecycleError> {
2964    match fs::symlink_metadata(path) {
2965        Ok(metadata) if metadata.file_type().is_symlink() => {
2966            make_path_writable(path)?;
2967            remove_symlink_leaf(path)?;
2968            Ok(true)
2969        }
2970        Ok(metadata) if metadata.file_type().is_dir() => {
2971            make_tree_writable(path)?;
2972            fs::remove_dir_all(path)
2973                .map_err(|source| io_error("remove parser-pack storage", path, source))?;
2974            Ok(true)
2975        }
2976        Ok(_) => {
2977            make_path_writable(path)?;
2978            fs::remove_file(path)
2979                .map_err(|source| io_error("remove parser-pack storage entry", path, source))?;
2980            Ok(true)
2981        }
2982        Err(source) if source.kind() == io::ErrorKind::NotFound => Ok(false),
2983        Err(source) => Err(io_error("inspect parser-pack storage", path, source)),
2984    }
2985}
2986
2987/// Remove only a symlink or junction leaf, never its target.
2988#[cfg(windows)]
2989fn remove_symlink_leaf(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
2990    match fs::remove_dir(path) {
2991        Ok(()) => Ok(()),
2992        Err(directory_error) => fs::remove_file(path).map_err(|file_error| {
2993            invalid_data(format!(
2994                "could not remove parser-pack storage link: directory={directory_error}; file={file_error}"
2995            ))
2996        }),
2997    }
2998}
2999
3000/// Remove only a symlink leaf, never its target.
3001#[cfg(not(windows))]
3002fn remove_symlink_leaf(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3003    fs::remove_file(path)
3004        .map_err(|source| io_error("remove parser-pack storage symlink", path, source))
3005}
3006
3007/// Move one writable Linux extraction into a unique direct publication sibling.
3008///
3009/// Linux requires write permission on a directory moved across parents because
3010/// its `..` entry changes. Staging before sealing lets the final immutable
3011/// rename stay within one parent and preserves atomic slot publication.
3012#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3013fn stage_parser_pack_for_atomic_publication(
3014    pack_root: &mut PathBuf,
3015    publication_parent: &Path,
3016) -> Result<TempDir, OptionalParserPackLifecycleError> {
3017    let staging = tempfile::Builder::new()
3018        .prefix(".projectatlas-install-")
3019        .tempdir_in(publication_parent)
3020        .map_err(|source| {
3021            io_error(
3022                "create parser-pack publication staging directory",
3023                publication_parent,
3024                source,
3025            )
3026        })?;
3027    for entry in fs::read_dir(&*pack_root).map_err(|source| {
3028        io_error(
3029            "list parser-pack extraction for publication",
3030            pack_root.as_path(),
3031            source,
3032        )
3033    })? {
3034        let entry = entry.map_err(|source| {
3035            io_error(
3036                "read parser-pack extraction entry for publication",
3037                pack_root.as_path(),
3038                source,
3039            )
3040        })?;
3041        let destination = staging.path().join(entry.file_name());
3042        fs::rename(entry.path(), &destination).map_err(|source| {
3043            io_error(
3044                "stage parser-pack extraction entry for publication",
3045                destination,
3046                source,
3047            )
3048        })?;
3049    }
3050    fs::remove_dir(&*pack_root).map_err(|source| {
3051        io_error(
3052            "remove empty parser-pack extraction root",
3053            pack_root.as_path(),
3054            source,
3055        )
3056    })?;
3057    pack_root.clone_from(&staging.path().to_path_buf());
3058    Ok(staging)
3059}
3060
3061/// Recursively remove write permissions so a published slot is immutable to normal lifecycle code.
3062fn seal_immutable_tree(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3063    let metadata = fs::symlink_metadata(path)
3064        .map_err(|source| io_error("inspect parser-pack staging tree", path, source))?;
3065    if metadata.file_type().is_symlink() {
3066        return Err(invalid_data("parser-pack staging tree contains a symlink"));
3067    }
3068    if metadata.is_dir() {
3069        for entry in fs::read_dir(path)
3070            .map_err(|source| io_error("list parser-pack staging tree", path, source))?
3071        {
3072            let entry =
3073                entry.map_err(|source| io_error("read parser-pack staging entry", path, source))?;
3074            seal_immutable_tree(&entry.path())?;
3075        }
3076    }
3077    set_path_immutable(path, metadata.is_dir())
3078}
3079
3080/// Verify that an installed slot has no writable file or directory entry.
3081fn verify_immutable_tree(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3082    let metadata = fs::symlink_metadata(path)
3083        .map_err(|source| io_error("inspect immutable parser-pack slot", path, source))?;
3084    if metadata.file_type().is_symlink() {
3085        return Err(invalid_data(
3086            "immutable parser-pack slot contains a symlink",
3087        ));
3088    }
3089    verify_path_immutable(path, &metadata)?;
3090    if metadata.is_dir() {
3091        for entry in fs::read_dir(path)
3092            .map_err(|source| io_error("list immutable parser-pack slot", path, source))?
3093        {
3094            let entry = entry.map_err(|source| {
3095                io_error("read immutable parser-pack slot entry", path, source)
3096            })?;
3097            verify_immutable_tree(&entry.path())?;
3098        }
3099    }
3100    Ok(())
3101}
3102
3103/// Restore writable permissions recursively before exact cleanup.
3104fn make_tree_writable(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3105    let metadata = match fs::symlink_metadata(path) {
3106        Ok(metadata) => metadata,
3107        Err(source) if source.kind() == io::ErrorKind::NotFound => return Ok(()),
3108        Err(source) => return Err(io_error("inspect parser-pack cleanup tree", path, source)),
3109    };
3110    make_path_writable(path)?;
3111    if metadata.is_dir() && !metadata.file_type().is_symlink() {
3112        for entry in fs::read_dir(path)
3113            .map_err(|source| io_error("list parser-pack cleanup tree", path, source))?
3114        {
3115            let entry =
3116                entry.map_err(|source| io_error("read parser-pack cleanup entry", path, source))?;
3117            make_tree_writable(&entry.path())?;
3118        }
3119    }
3120    Ok(())
3121}
3122
3123#[cfg(unix)]
3124/// Restore the canonical archive mode after extraction.
3125fn set_extracted_mode(path: &Path, mode: u32) -> Result<(), OptionalParserPackLifecycleError> {
3126    use std::os::unix::fs::PermissionsExt as _;
3127    fs::set_permissions(path, fs::Permissions::from_mode(mode))
3128        .map_err(|source| io_error("set extracted parser-pack mode", path, source))
3129}
3130
3131#[cfg(unix)]
3132/// Remove write bits while retaining executable bits from the canonical archive.
3133fn set_path_immutable(
3134    path: &Path,
3135    _directory: bool,
3136) -> Result<(), OptionalParserPackLifecycleError> {
3137    use std::os::unix::fs::PermissionsExt as _;
3138    let metadata = fs::symlink_metadata(path)
3139        .map_err(|source| io_error("inspect parser-pack permissions", path, source))?;
3140    let mode = metadata.permissions().mode() & !0o222;
3141    fs::set_permissions(path, fs::Permissions::from_mode(mode))
3142        .map_err(|source| io_error("seal parser-pack permissions", path, source))
3143}
3144
3145#[cfg(windows)]
3146/// Mark one installed file read-only; directories retain normal traversal semantics.
3147fn set_path_immutable(
3148    path: &Path,
3149    directory: bool,
3150) -> Result<(), OptionalParserPackLifecycleError> {
3151    if directory {
3152        return Ok(());
3153    }
3154    let mut permissions = fs::symlink_metadata(path)
3155        .map_err(|source| io_error("inspect parser-pack permissions", path, source))?
3156        .permissions();
3157    permissions.set_readonly(true);
3158    fs::set_permissions(path, permissions)
3159        .map_err(|source| io_error("seal parser-pack permissions", path, source))
3160}
3161
3162#[cfg(not(any(unix, windows)))]
3163/// Preserve permissions on hosts that cannot activate the optional pack.
3164fn set_path_immutable(
3165    _path: &Path,
3166    _directory: bool,
3167) -> Result<(), OptionalParserPackLifecycleError> {
3168    Ok(())
3169}
3170
3171#[cfg(unix)]
3172/// Require that no installed entry retains a Unix write bit.
3173fn verify_path_immutable(
3174    path: &Path,
3175    metadata: &fs::Metadata,
3176) -> Result<(), OptionalParserPackLifecycleError> {
3177    use std::os::unix::fs::PermissionsExt as _;
3178    if metadata.permissions().mode() & 0o222 != 0 {
3179        return Err(invalid_data(format!(
3180            "installed parser-pack entry {} is writable",
3181            path.file_name().unwrap_or_default().display()
3182        )));
3183    }
3184    Ok(())
3185}
3186
3187#[cfg(windows)]
3188/// Require that every installed regular file retains its read-only marker.
3189fn verify_path_immutable(
3190    _path: &Path,
3191    metadata: &fs::Metadata,
3192) -> Result<(), OptionalParserPackLifecycleError> {
3193    if metadata.is_file() && !metadata.permissions().readonly() {
3194        return Err(invalid_data("installed parser-pack file is writable"));
3195    }
3196    Ok(())
3197}
3198
3199#[cfg(not(any(unix, windows)))]
3200/// Accept metadata on hosts that cannot activate the optional pack.
3201fn verify_path_immutable(
3202    _path: &Path,
3203    _metadata: &fs::Metadata,
3204) -> Result<(), OptionalParserPackLifecycleError> {
3205    Ok(())
3206}
3207
3208#[cfg(unix)]
3209/// Restore owner traversal and write bits before exact tree cleanup.
3210fn make_path_writable(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3211    use std::os::unix::fs::PermissionsExt as _;
3212    let metadata = fs::symlink_metadata(path)
3213        .map_err(|source| io_error("inspect parser-pack cleanup permissions", path, source))?;
3214    if metadata.file_type().is_symlink() {
3215        return Ok(());
3216    }
3217    let mode = metadata.permissions().mode() | 0o700;
3218    fs::set_permissions(path, fs::Permissions::from_mode(mode))
3219        .map_err(|source| io_error("restore parser-pack cleanup permissions", path, source))
3220}
3221
3222#[cfg(windows)]
3223/// Clear only the Windows read-only file attribute before exact tree cleanup.
3224#[allow(clippy::permissions_set_readonly_false)]
3225fn make_path_writable(path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3226    let metadata = fs::symlink_metadata(path)
3227        .map_err(|source| io_error("inspect parser-pack cleanup permissions", path, source))?;
3228    if metadata.file_type().is_symlink() {
3229        return Ok(());
3230    }
3231    let mut permissions = metadata.permissions();
3232    // On Windows this toggles only FILE_ATTRIBUTE_READONLY; the Unix world-writable
3233    // behavior guarded by Clippy is compiled in the separate PermissionsExt branch above.
3234    permissions.set_readonly(false);
3235    fs::set_permissions(path, permissions)
3236        .map_err(|source| io_error("restore parser-pack cleanup permissions", path, source))
3237}
3238
3239#[cfg(not(any(unix, windows)))]
3240/// Preserve permissions on hosts that cannot activate the optional pack.
3241fn make_path_writable(_path: &Path) -> Result<(), OptionalParserPackLifecycleError> {
3242    Ok(())
3243}
3244
3245/// Resolve the user-owned parser-pack storage root for normal operation.
3246fn default_storage_root() -> Result<PathBuf, OptionalParserPackLifecycleError> {
3247    #[cfg(windows)]
3248    {
3249        env::var_os("LOCALAPPDATA")
3250            .map(PathBuf::from)
3251            .map(|root| root.join("ProjectAtlas").join("parser-packs"))
3252            .ok_or(OptionalParserPackLifecycleError::StorageRootUnavailable)
3253    }
3254    #[cfg(target_os = "macos")]
3255    {
3256        env::var_os("HOME")
3257            .map(PathBuf::from)
3258            .map(|root| {
3259                root.join("Library")
3260                    .join("Application Support")
3261                    .join("ProjectAtlas")
3262                    .join("parser-packs")
3263            })
3264            .ok_or(OptionalParserPackLifecycleError::StorageRootUnavailable)
3265    }
3266    #[cfg(all(unix, not(target_os = "macos")))]
3267    {
3268        if let Some(root) = env::var_os("XDG_DATA_HOME") {
3269            return Ok(PathBuf::from(root)
3270                .join("projectatlas")
3271                .join("parser-packs"));
3272        }
3273        env::var_os("HOME")
3274            .map(PathBuf::from)
3275            .map(|root| root.join(".local/share/projectatlas/parser-packs"))
3276            .ok_or(OptionalParserPackLifecycleError::StorageRootUnavailable)
3277    }
3278    #[cfg(not(any(unix, windows)))]
3279    Err(OptionalParserPackLifecycleError::StorageRootUnavailable)
3280}
3281
3282/// Current accepted optional-pack target, if any.
3283#[cfg(test)]
3284fn host_pack_platform() -> Option<PackPlatform> {
3285    OptionalParserCapability::current().pack_platform()
3286}
3287
3288/// Canonical lowercase hexadecimal without another dependency.
3289fn lowercase_hex(bytes: &[u8]) -> String {
3290    const HEX: &[u8; 16] = b"0123456789abcdef";
3291    let mut encoded = String::with_capacity(bytes.len().saturating_mul(2));
3292    for byte in bytes {
3293        encoded.push(char::from(HEX[usize::from(byte >> 4)]));
3294        encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
3295    }
3296    encoded
3297}
3298
3299/// Construct one bounded data-contract error.
3300fn invalid_data(reason: impl Into<String>) -> OptionalParserPackLifecycleError {
3301    OptionalParserPackLifecycleError::InvalidData {
3302        reason: reason.into(),
3303    }
3304}
3305
3306/// Construct one filesystem error retaining exact operation and path context.
3307fn io_error(
3308    operation: &'static str,
3309    path: impl Into<PathBuf>,
3310    source: io::Error,
3311) -> OptionalParserPackLifecycleError {
3312    OptionalParserPackLifecycleError::Io {
3313        operation,
3314        path: path.into(),
3315        source,
3316    }
3317}
3318
3319#[cfg(test)]
3320mod tests {
3321    use super::*;
3322    use std::error::Error;
3323    use std::sync::Arc;
3324    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
3325    use std::sync::atomic::AtomicBool;
3326    use std::sync::atomic::{AtomicUsize, Ordering};
3327
3328    /// Child-only storage root used to prove lease release without running destructors.
3329    const ABRUPT_LEASE_STORAGE_ENV: &str = "PROJECTATLAS_TEST_ABRUPT_LEASE_STORAGE";
3330    /// Child-only marker proving the shared lease was acquired before abrupt exit.
3331    const ABRUPT_LEASE_MARKER_ENV: &str = "PROJECTATLAS_TEST_ABRUPT_LEASE_MARKER";
3332    /// Child-only lease kind selected by the cross-process lifecycle tests.
3333    const ABRUPT_LEASE_KIND_ENV: &str = "PROJECTATLAS_TEST_ABRUPT_LEASE_KIND";
3334    /// Deliberate non-success code used by the no-destructor child process.
3335    const ABRUPT_LEASE_EXIT_CODE: i32 = 86;
3336
3337    type TestResult = Result<(), Box<dyn Error>>;
3338
3339    fn require(condition: bool, message: &str) -> TestResult {
3340        if condition {
3341            Ok(())
3342        } else {
3343            Err(Box::new(io::Error::other(message.to_owned())))
3344        }
3345    }
3346
3347    /// Require one lifecycle call to fail without panicking the test process.
3348    fn require_lifecycle_error<T>(
3349        result: Result<T, OptionalParserPackLifecycleError>,
3350        message: &str,
3351    ) -> Result<OptionalParserPackLifecycleError, Box<dyn Error>> {
3352        match result {
3353            Ok(_) => Err(Box::new(io::Error::other(message.to_owned()))),
3354            Err(error) => Ok(error),
3355        }
3356    }
3357
3358    fn test_slot(byte: char) -> PackSlotIdentity {
3359        PackSlotIdentity {
3360            projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_owned(),
3361            artifact: std::iter::repeat_n(byte, 64).collect(),
3362        }
3363    }
3364
3365    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3366    #[test]
3367    fn linux_publication_stages_before_immutable_same_parent_rename() -> TestResult {
3368        let root = tempfile::tempdir()?;
3369        let versions = root.path().join("versions");
3370        let version = versions.join(OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION);
3371        fs::create_dir_all(&version)?;
3372        let extraction = TempDir::new_in(&versions)?;
3373        let mut pack_root = extraction.path().join(ARCHIVE_ROOT);
3374        fs::create_dir(&pack_root)?;
3375        fs::write(pack_root.join("payload"), b"parser")?;
3376
3377        let staging = stage_parser_pack_for_atomic_publication(&mut pack_root, &version)?;
3378        require(
3379            pack_root.parent() == Some(version.as_path()),
3380            "Linux publication staging was not a direct destination sibling",
3381        )?;
3382        seal_immutable_tree(&pack_root)?;
3383        let destination = version.join(std::iter::repeat_n('a', 64).collect::<String>());
3384        fs::rename(&pack_root, &destination)?;
3385        drop(staging);
3386
3387        verify_immutable_tree(&destination)?;
3388        require(
3389            fs::read(destination.join("payload"))? == b"parser",
3390            "Linux immutable publication changed staged bytes",
3391        )
3392    }
3393
3394    /// Count actual guard cleanup calls without invoking platform profiles.
3395    fn counted_temporary_profile(
3396        root: &Path,
3397        fail: bool,
3398    ) -> (TemporaryParserArtifactProfile, Arc<AtomicUsize>) {
3399        let attempts = Arc::new(AtomicUsize::new(0));
3400        let observed = Arc::clone(&attempts);
3401        let mut profile = TemporaryParserArtifactProfile::new(
3402            root,
3403            ParserArtifactIdentity::for_bytes(b"artifact-manifest"),
3404        );
3405        profile.cleanup_override = Some(Box::new(move |_, _| {
3406            observed.fetch_add(1, Ordering::Relaxed);
3407            if fail {
3408                Err(invalid_data("injected profile cleanup failure"))
3409            } else {
3410                Ok(())
3411            }
3412        }));
3413        (profile, attempts)
3414    }
3415
3416    #[test]
3417    fn explicit_temporary_profile_cleanup_failure_runs_once() -> TestResult {
3418        let directory = tempfile::tempdir()?;
3419        let (profile, attempts) = counted_temporary_profile(directory.path(), true);
3420        let error = require_lifecycle_error(profile.cleanup(), "injected cleanup was accepted")?;
3421        require(
3422            matches!(error, OptionalParserPackLifecycleError::InvalidData { reason }
3423                if reason == "injected profile cleanup failure"),
3424            "explicit cleanup lost its original typed failure",
3425        )?;
3426        let observed = attempts.load(Ordering::Relaxed);
3427        require(
3428            observed == 1,
3429            &format!("explicit cleanup plus Drop made {observed} attempts instead of one"),
3430        )
3431    }
3432
3433    #[test]
3434    fn temporary_profile_success_and_forgotten_cleanup_run_once() -> TestResult {
3435        let directory = tempfile::tempdir()?;
3436        for explicit in [true, false] {
3437            let (profile, attempts) = counted_temporary_profile(directory.path(), false);
3438            if explicit {
3439                profile.cleanup()?;
3440            } else {
3441                drop(profile);
3442            }
3443            require(
3444                attempts.load(Ordering::Relaxed) == 1,
3445                "successful or forgotten cleanup did not use exactly one attempt",
3446            )?;
3447        }
3448        Ok(())
3449    }
3450
3451    #[test]
3452    fn installed_slot_transfer_disarms_temporary_profile_cleanup() -> TestResult {
3453        let directory = tempfile::tempdir()?;
3454        let (mut profile, attempts) = counted_temporary_profile(directory.path(), false);
3455        profile.transfer_to_installed_slot();
3456        require(
3457            !profile.cleanup_pending,
3458            "installed-slot transfer retained temporary cleanup ownership",
3459        )?;
3460        profile.cleanup()?;
3461        require(
3462            attempts.load(Ordering::Relaxed) == 0,
3463            "installed-slot transfer still invoked temporary profile cleanup",
3464        )
3465    }
3466
3467    #[test]
3468    fn lifecycle_operation_and_cleanup_failures_are_both_retained() -> TestResult {
3469        let error = require_lifecycle_error(
3470            finish_with_cleanup::<()>(
3471                Err(invalid_data("operation failed")),
3472                Err(OptionalParserPackLifecycleError::CleanupIncomplete {
3473                    message: "cleanup failed".to_owned(),
3474                }),
3475            ),
3476            "dual lifecycle failure was accepted",
3477        )?;
3478        match error {
3479            OptionalParserPackLifecycleError::OperationAndCleanup { operation, cleanup } => {
3480                require(
3481                    matches!(
3482                        &*operation,
3483                        OptionalParserPackLifecycleError::InvalidData { .. }
3484                    ) && matches!(
3485                        &*cleanup,
3486                        OptionalParserPackLifecycleError::CleanupIncomplete { .. }
3487                    ),
3488                    "dual lifecycle failure changed the validation or cleanup error kind",
3489                )?;
3490                require(
3491                    operation.to_string().contains("operation failed")
3492                        && cleanup.to_string().contains("cleanup failed"),
3493                    "dual lifecycle failure lost one typed cause",
3494                )
3495            }
3496            other => Err(Box::new(io::Error::other(format!(
3497                "dual lifecycle failure returned the wrong variant: {other}"
3498            )))),
3499        }
3500    }
3501
3502    #[test]
3503    fn shared_pack_leases_exclude_storage_mutation_until_every_reader_releases() -> TestResult {
3504        let root = tempfile::tempdir()?;
3505        let storage = root.path().join("storage");
3506        let lifecycle = OptionalParserPackLifecycle::for_test(
3507            root.path().join("project"),
3508            storage.clone(),
3509            Some(PackPlatform::LinuxX86_64),
3510        );
3511        fs::create_dir_all(lifecycle.versions_root()?)?;
3512
3513        let first_reader = lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?;
3514        let second_reader = lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?;
3515        let error = require_lifecycle_error(
3516            lifecycle.remove(),
3517            "exclusive removal succeeded while shared leases were retained",
3518        )?;
3519        require(
3520            matches!(error, OptionalParserPackLifecycleError::Busy { .. }),
3521            "shared/exclusive contention did not retain a typed busy failure",
3522        )?;
3523        require(
3524            lifecycle.pack_root()?.is_dir(),
3525            "contended removal changed immutable pack storage",
3526        )?;
3527
3528        drop(first_reader);
3529        require(
3530            matches!(
3531                lifecycle.remove(),
3532                Err(OptionalParserPackLifecycleError::Busy { .. })
3533            ),
3534            "one remaining shared lease did not retain exclusion",
3535        )?;
3536        drop(second_reader);
3537        require(
3538            lifecycle.remove()?.changed,
3539            "removal did not proceed after every shared lease released",
3540        )?;
3541
3542        let writer = lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Exclusive)?;
3543        require(
3544            matches!(
3545                lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Shared),
3546                Err(OptionalParserPackLifecycleError::Busy { .. })
3547            ),
3548            "exclusive lease did not exclude a later shared reader",
3549        )?;
3550        drop(writer);
3551        let _reader_after_release =
3552            lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?;
3553        require(
3554            storage.join(OPTIONAL_PARSER_PACK_LEASE_FILE_NAME).is_file(),
3555            "stable lifecycle lease disappeared with logical pack storage",
3556        )
3557    }
3558
3559    #[test]
3560    fn abrupt_lease_holder_process() -> TestResult {
3561        let Some(storage) = env::var_os(ABRUPT_LEASE_STORAGE_ENV).map(PathBuf::from) else {
3562            return Ok(());
3563        };
3564        let marker = env::var_os(ABRUPT_LEASE_MARKER_ENV)
3565            .map(PathBuf::from)
3566            .ok_or_else(|| io::Error::other("abrupt lease marker path is missing"))?;
3567        let lifecycle = OptionalParserPackLifecycle::for_test(
3568            storage.join("project"),
3569            storage,
3570            Some(PackPlatform::LinuxX86_64),
3571        );
3572        let kind = env::var(ABRUPT_LEASE_KIND_ENV)?;
3573        let _lease = match kind.as_str() {
3574            "pack" => lifecycle.acquire_pack_lease(OptionalParserPackLeaseMode::Shared)?,
3575            "selection" => lifecycle.acquire_selection_mutation_lease()?,
3576            _ => return Err(io::Error::other("unknown abrupt lease kind").into()),
3577        };
3578        fs::write(marker, kind)?;
3579        std::thread::sleep(std::time::Duration::from_secs(30));
3580        std::process::exit(ABRUPT_LEASE_EXIT_CODE);
3581    }
3582
3583    fn wait_for_child_marker(marker: &Path) -> TestResult {
3584        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
3585        while !marker.is_file() {
3586            if std::time::Instant::now() >= deadline {
3587                return Err(
3588                    io::Error::other("lease-holder child did not publish readiness").into(),
3589                );
3590            }
3591            std::thread::sleep(std::time::Duration::from_millis(10));
3592        }
3593        Ok(())
3594    }
3595
3596    #[test]
3597    fn live_child_excludes_pack_mutation_and_abrupt_exit_releases_lease() -> TestResult {
3598        let root = tempfile::tempdir()?;
3599        let storage = root.path().join("storage");
3600        let marker = root.path().join("lease-acquired");
3601        let lifecycle = OptionalParserPackLifecycle::for_test(
3602            root.path().join("project"),
3603            storage.clone(),
3604            Some(PackPlatform::LinuxX86_64),
3605        );
3606        fs::create_dir_all(lifecycle.versions_root()?.join("retained"))?;
3607        let mut child = std::process::Command::new(std::env::current_exe()?)
3608            .arg("--exact")
3609            .arg("optional_parser_lifecycle::tests::abrupt_lease_holder_process")
3610            .arg("--nocapture")
3611            .env(ABRUPT_LEASE_STORAGE_ENV, &storage)
3612            .env(ABRUPT_LEASE_MARKER_ENV, &marker)
3613            .env(ABRUPT_LEASE_KIND_ENV, "pack")
3614            .spawn()?;
3615        wait_for_child_marker(&marker)?;
3616        require(
3617            matches!(
3618                lifecycle.remove(),
3619                Err(OptionalParserPackLifecycleError::Busy { .. })
3620            ),
3621            "live child execution did not exclude immutable storage removal",
3622        )?;
3623        child.kill()?;
3624        let _status = child.wait()?;
3625        require(
3626            lifecycle.remove()?.changed,
3627            "post-exit removal did not succeed",
3628        )
3629    }
3630
3631    #[test]
3632    fn live_child_serializes_selection_mutation_and_abrupt_exit_releases_lease() -> TestResult {
3633        let root = tempfile::tempdir()?;
3634        let storage = root.path().join("storage");
3635        let project = storage.join("project");
3636        let marker = root.path().join("selection-lease-acquired");
3637        let lifecycle = OptionalParserPackLifecycle::for_test(
3638            project,
3639            storage.clone(),
3640            Some(PackPlatform::LinuxX86_64),
3641        );
3642        lifecycle.write_selection(&ProjectSelection::new(test_slot('a'), None))?;
3643        let mut child = std::process::Command::new(std::env::current_exe()?)
3644            .arg("--exact")
3645            .arg("optional_parser_lifecycle::tests::abrupt_lease_holder_process")
3646            .arg("--nocapture")
3647            .env(ABRUPT_LEASE_STORAGE_ENV, &storage)
3648            .env(ABRUPT_LEASE_MARKER_ENV, &marker)
3649            .env(ABRUPT_LEASE_KIND_ENV, "selection")
3650            .spawn()?;
3651        wait_for_child_marker(&marker)?;
3652        for (operation, result) in [
3653            ("enable", lifecycle.enable(&test_slot('b').artifact)),
3654            (
3655                "update",
3656                lifecycle.update(&root.path().join("candidate.tar.zst")),
3657            ),
3658            ("disable", lifecycle.disable()),
3659            ("remove", lifecycle.remove()),
3660        ] {
3661            require(
3662                matches!(result, Err(OptionalParserPackLifecycleError::Busy { .. })),
3663                &format!("live child selection transition did not serialize {operation}"),
3664            )?;
3665        }
3666        require(
3667            lifecycle.selection_path().is_file(),
3668            "contended selection operations changed project selection",
3669        )?;
3670        child.kill()?;
3671        let _status = child.wait()?;
3672        require(
3673            lifecycle.disable()?.changed,
3674            "post-exit disable did not succeed",
3675        )
3676    }
3677
3678    /// Create a directory link for hostile-storage tests.
3679    #[cfg(unix)]
3680    fn create_directory_link(target: &Path, link: &Path) -> io::Result<()> {
3681        std::os::unix::fs::symlink(target, link)
3682    }
3683
3684    /// Create a directory symlink, falling back to a junction without following it later.
3685    #[cfg(windows)]
3686    fn create_directory_link(target: &Path, link: &Path) -> io::Result<()> {
3687        match std::os::windows::fs::symlink_dir(target, link) {
3688            Ok(()) => Ok(()),
3689            Err(source) if source.raw_os_error() == Some(1314) => {
3690                let status = std::process::Command::new("cmd")
3691                    .arg("/C")
3692                    .arg("mklink")
3693                    .arg("/J")
3694                    .arg(link)
3695                    .arg(target)
3696                    .status()?;
3697                if status.success() {
3698                    Ok(())
3699                } else {
3700                    Err(source)
3701                }
3702            }
3703            Err(source) => Err(source),
3704        }
3705    }
3706
3707    #[test]
3708    fn unsupported_operations_refuse_before_archive_or_state_access() -> TestResult {
3709        let root = tempfile::tempdir()?;
3710        let project = root.path().join("missing-project");
3711        let storage = root.path().join("missing-storage");
3712        let archive = root.path().join("missing-archive.tar.zst");
3713        let lifecycle = OptionalParserPackLifecycle::for_test(project, storage.clone(), None);
3714
3715        for error in [
3716            require_lifecycle_error(lifecycle.verify(&archive), "verify must refuse")?,
3717            require_lifecycle_error(lifecycle.install(&archive), "install must refuse")?,
3718            require_lifecycle_error(lifecycle.enable(&"a".repeat(64)), "enable must refuse")?,
3719            require_lifecycle_error(lifecycle.update(&archive), "update must refuse")?,
3720        ] {
3721            require(
3722                error.is_unsupported_containment(),
3723                "failure was not typed unsupported containment",
3724            )?;
3725        }
3726        require(!storage.exists(), "unsupported operation created storage")
3727    }
3728
3729    #[test]
3730    fn runtime_handoff_is_absent_everywhere_and_refuses_present_unsupported_state() -> TestResult {
3731        let root = tempfile::tempdir()?;
3732        let project = root.path().join("project");
3733        let storage = root.path().join("storage");
3734        let lifecycle =
3735            OptionalParserPackLifecycle::for_test(project.clone(), storage.clone(), None);
3736        require(
3737            lifecycle.resolve_selected_pack()?.is_none(),
3738            "absent selection did not preserve default-core operation",
3739        )?;
3740        let selection = project.join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH);
3741        fs::create_dir_all(
3742            selection
3743                .parent()
3744                .ok_or_else(|| io::Error::other("selection parent missing"))?,
3745        )?;
3746        fs::write(&selection, b"not inspected on an unsupported host")?;
3747        let error = require_lifecycle_error(
3748            lifecycle.resolve_selected_pack(),
3749            "present unsupported selection must refuse",
3750        )?;
3751        require(
3752            error.is_unsupported_containment(),
3753            "present selection was not typed unsupported containment",
3754        )?;
3755        require(!storage.exists(), "runtime handoff touched pack storage")
3756    }
3757
3758    #[test]
3759    fn project_selection_derivation_is_content_free_strict_and_storage_independent() -> TestResult {
3760        let root = tempfile::tempdir()?;
3761        let project = root.path().join("project");
3762        let storage = root.path().join("storage-is-a-file");
3763        fs::write(&storage, b"must not be inspected")?;
3764        let lifecycle = OptionalParserPackLifecycle::for_test(
3765            project,
3766            storage,
3767            Some(PackPlatform::LinuxX86_64),
3768        );
3769
3770        require(
3771            lifecycle.derive_project_selection()? == OptionalParserPackProjectSelection::Inactive,
3772            "absent project selection was not inactive",
3773        )?;
3774        let selected = test_slot('a');
3775        lifecycle.write_selection(&ProjectSelection::new(selected.clone(), None))?;
3776        let derivation = lifecycle.derive_project_selection()?;
3777        let key = derivation
3778            .selection_key()
3779            .ok_or_else(|| io::Error::other("selected derivation omitted its key"))?;
3780        require(
3781            key.as_str()
3782                == format!(
3783                    "{}:{}:{}",
3784                    OPTIONAL_PARSER_PACK_ID, selected.projectatlas_version, selected.artifact
3785                ),
3786            "selected derivation key was not stable",
3787        )?;
3788        require(
3789            derivation.artifact() == Some(key.artifact()),
3790            "selected derivation artifact did not delegate to its key",
3791        )?;
3792        require(
3793            OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH == ".projectatlas/optional-parser-pack.json",
3794            "public selection policy path drifted",
3795        )?;
3796
3797        fs::write(lifecycle.selection_path(), b"malformed")?;
3798        require(
3799            lifecycle.derive_project_selection().is_err(),
3800            "supported derivation accepted malformed selection JSON",
3801        )
3802    }
3803
3804    #[test]
3805    fn public_constructor_defers_storage_and_prioritizes_unsupported_state() -> TestResult {
3806        let root = tempfile::tempdir()?;
3807        let project = root.path().join("project");
3808        let mut lifecycle = OptionalParserPackLifecycle::new(project.clone(), None)?;
3809        require(
3810            lifecycle.storage_root.get().is_none(),
3811            "public constructor eagerly resolved the user storage root",
3812        )?;
3813        lifecycle.capability = OptionalParserCapability::BuiltInOnly;
3814        let archive = root.path().join("missing.tar.zst");
3815        let error = require_lifecycle_error(
3816            lifecycle.verify(&archive),
3817            "unsupported verify did not fail",
3818        )?;
3819        require(
3820            error.is_unsupported_containment(),
3821            "unsupported verify lost typed priority",
3822        )?;
3823        require(
3824            lifecycle.storage_root.get().is_none(),
3825            "unsupported verify resolved user storage",
3826        )?;
3827
3828        let selection = project.join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH);
3829        fs::create_dir_all(
3830            selection
3831                .parent()
3832                .ok_or_else(|| io::Error::other("selection parent missing"))?,
3833        )?;
3834        fs::write(&selection, b"stale")?;
3835        if lifecycle.storage_root.set(None).is_err() {
3836            return Err(io::Error::other("deferred storage root was already initialized").into());
3837        }
3838        require(
3839            lifecycle.disable()?.changed,
3840            "disable required a user storage root",
3841        )?;
3842        require(
3843            !selection.exists(),
3844            "disable did not remove project selection",
3845        )
3846    }
3847
3848    #[test]
3849    fn present_unsupported_derivation_refuses_before_malformed_content() -> TestResult {
3850        let root = tempfile::tempdir()?;
3851        let project = root.path().join("project");
3852        let selection = project.join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH);
3853        fs::create_dir_all(
3854            selection
3855                .parent()
3856                .ok_or_else(|| io::Error::other("selection parent missing"))?,
3857        )?;
3858        fs::write(&selection, b"malformed and must not be read")?;
3859        let lifecycle =
3860            OptionalParserPackLifecycle::for_test(project, root.path().join("storage"), None);
3861        let error = require_lifecycle_error(
3862            lifecycle.derive_project_selection(),
3863            "present unsupported derivation did not fail",
3864        )?;
3865        require(
3866            error.is_unsupported_containment(),
3867            "present unsupported derivation inspected malformed contents",
3868        )
3869    }
3870
3871    #[test]
3872    fn unsupported_cleanup_is_idempotent_for_stale_metadata() -> TestResult {
3873        let root = tempfile::tempdir()?;
3874        let project = root.path().join("project");
3875        let storage = root.path().join("storage");
3876        let selection = project.join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH);
3877        fs::create_dir_all(
3878            selection
3879                .parent()
3880                .ok_or_else(|| io::Error::other("selection parent missing"))?,
3881        )?;
3882        fs::write(&selection, b"stale")?;
3883        let pack_root = storage.join(OPTIONAL_PARSER_PACK_ID);
3884        fs::create_dir_all(pack_root.join("versions/stale/slot"))?;
3885        let payload = pack_root.join("versions/stale/slot/payload.bin");
3886        fs::write(&payload, b"payload")?;
3887        let mut permissions = fs::metadata(&payload)?.permissions();
3888        permissions.set_readonly(true);
3889        fs::set_permissions(&payload, permissions)?;
3890        let lifecycle = OptionalParserPackLifecycle::for_test(project, storage, None);
3891
3892        require(
3893            lifecycle.disable()?.changed,
3894            "first disable did not remove stale selection",
3895        )?;
3896        require(
3897            !lifecycle.disable()?.changed,
3898            "second disable was not idempotent",
3899        )?;
3900        require(
3901            lifecycle.remove()?.changed,
3902            "first remove did not delete storage",
3903        )?;
3904        require(
3905            !lifecycle.remove()?.changed,
3906            "second remove was not idempotent",
3907        )
3908    }
3909
3910    #[test]
3911    fn unsupported_remove_does_not_create_absent_storage() -> TestResult {
3912        let root = tempfile::tempdir()?;
3913        let project = root.path().join("project");
3914        let storage = root.path().join("storage");
3915        let source = project.join("src/lib.rs");
3916        let selection = project.join(OPTIONAL_PARSER_PACK_SELECTION_POLICY_PATH);
3917        fs::create_dir_all(
3918            selection
3919                .parent()
3920                .ok_or_else(|| io::Error::other("selection parent missing"))?,
3921        )?;
3922        fs::create_dir_all(
3923            source
3924                .parent()
3925                .ok_or_else(|| io::Error::other("source parent missing"))?,
3926        )?;
3927        fs::write(&source, b"source must survive")?;
3928        fs::write(&selection, b"stale")?;
3929        let lifecycle = OptionalParserPackLifecycle::for_test(project, storage.clone(), None);
3930
3931        require(
3932            lifecycle.remove()?.changed,
3933            "first remove did not delete stale selection",
3934        )?;
3935        require(!selection.exists(), "stale selection survived remove")?;
3936        require(!storage.exists(), "remove created absent storage")?;
3937        require(
3938            !lifecycle.remove()?.changed,
3939            "second remove was not idempotent",
3940        )?;
3941        require(
3942            fs::read(&source)? == b"source must survive",
3943            "unsupported remove touched source",
3944        )
3945    }
3946
3947    #[test]
3948    fn removal_never_follows_product_owned_storage_links() -> TestResult {
3949        let root = tempfile::tempdir()?;
3950        let project = root.path().join("project");
3951        let storage = root.path().join("storage");
3952        let external_pack = root.path().join("external-pack");
3953        fs::create_dir_all(&storage)?;
3954        fs::create_dir_all(&external_pack)?;
3955        let external_marker = external_pack.join("must-survive.txt");
3956        fs::write(&external_marker, b"outside")?;
3957        let pack_link = storage.join(OPTIONAL_PARSER_PACK_ID);
3958        create_directory_link(&external_pack, &pack_link)?;
3959        let lifecycle = OptionalParserPackLifecycle::for_test(project, storage.clone(), None);
3960
3961        require(
3962            lifecycle.remove()?.changed,
3963            "pack-root link was not removed",
3964        )?;
3965        require(
3966            external_marker.is_file(),
3967            "pack-root link target was deleted",
3968        )?;
3969        require(!pack_link.exists(), "pack-root link leaf survived")?;
3970
3971        let pack_root = storage.join(OPTIONAL_PARSER_PACK_ID);
3972        fs::create_dir_all(&pack_root)?;
3973        let external_versions = root.path().join("external-versions");
3974        fs::create_dir_all(&external_versions)?;
3975        let versions_marker = external_versions.join("must-survive.txt");
3976        fs::write(&versions_marker, b"outside")?;
3977        create_directory_link(&external_versions, &pack_root.join("versions"))?;
3978
3979        require(lifecycle.remove()?.changed, "versions link was not removed")?;
3980        require(
3981            versions_marker.is_file(),
3982            "versions link target was deleted",
3983        )
3984    }
3985
3986    #[test]
3987    fn selection_operations_never_follow_project_state_parent_link() -> TestResult {
3988        let root = tempfile::tempdir()?;
3989        let project = root.path().join("project");
3990        let external_state = root.path().join("external-state");
3991        fs::create_dir_all(&project)?;
3992        fs::create_dir_all(&external_state)?;
3993        let external_selection = external_state.join("optional-parser-pack.json");
3994        fs::write(&external_selection, b"must survive")?;
3995        create_directory_link(&external_state, &project.join(".projectatlas"))?;
3996        let lifecycle =
3997            OptionalParserPackLifecycle::for_test(project, root.path().join("storage"), None);
3998
3999        require(
4000            lifecycle.status()?.state == OptionalParserPackState::Stale,
4001            "linked selection parent was not reported stale",
4002        )?;
4003        require(
4004            !lifecycle.disable()?.changed,
4005            "disable claimed to mutate a linked selection parent",
4006        )?;
4007        require(
4008            external_selection.is_file(),
4009            "disable deleted an external selection through a linked parent",
4010        )?;
4011        let _report = lifecycle.remove()?;
4012        require(
4013            external_selection.is_file(),
4014            "remove deleted an external selection through a linked parent",
4015        )
4016    }
4017
4018    #[test]
4019    fn status_rejects_and_removal_does_not_follow_selected_slot_link() -> TestResult {
4020        let root = tempfile::tempdir()?;
4021        let lifecycle = OptionalParserPackLifecycle::for_test(
4022            root.path().join("project"),
4023            root.path().join("storage"),
4024            None,
4025        );
4026        let selected = test_slot('d');
4027        let slot = lifecycle.slot_path(&selected)?;
4028        fs::create_dir_all(
4029            slot.parent()
4030                .ok_or_else(|| io::Error::other("slot parent missing"))?,
4031        )?;
4032        let external = root.path().join("external-slot");
4033        fs::create_dir_all(&external)?;
4034        let marker = external.join("must-survive.txt");
4035        fs::write(&marker, b"outside")?;
4036        create_directory_link(&external, &slot)?;
4037        lifecycle.write_selection(&ProjectSelection::new(selected, None))?;
4038
4039        require(
4040            lifecycle.status()?.state == OptionalParserPackState::Stale,
4041            "linked selected slot was reported present",
4042        )?;
4043        let _report = lifecycle.remove()?;
4044        require(marker.is_file(), "selected slot link target was deleted")
4045    }
4046
4047    #[test]
4048    fn install_refuses_product_owned_storage_link_before_archive_open() -> TestResult {
4049        let Some(platform) = host_pack_platform() else {
4050            return Ok(());
4051        };
4052        let root = tempfile::tempdir()?;
4053        let storage = root.path().join("storage");
4054        let external = root.path().join("external");
4055        fs::create_dir_all(&storage)?;
4056        fs::create_dir_all(&external)?;
4057        let marker = external.join("must-survive.txt");
4058        fs::write(&marker, b"outside")?;
4059        create_directory_link(&external, &storage.join(OPTIONAL_PARSER_PACK_ID))?;
4060        let lifecycle = OptionalParserPackLifecycle::for_test(
4061            root.path().join("project"),
4062            storage,
4063            Some(platform),
4064        );
4065        let missing_archive = root.path().join("missing.tar.zst");
4066
4067        require(
4068            lifecycle.install(&missing_archive).is_err(),
4069            "install followed a product-owned storage link",
4070        )?;
4071        require(marker.is_file(), "install mutated the link target")
4072    }
4073
4074    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4075    #[test]
4076    fn windows_cleanup_paths_preserve_verbatim_forms_and_bound_current_directory() -> TestResult {
4077        require(
4078            windows_verbatim_path(Path::new(r"C:\pack\parser")) == Path::new(r"\\?\C:\pack\parser"),
4079            "drive path did not gain its verbatim prefix",
4080        )?;
4081        require(
4082            windows_verbatim_path(Path::new(r"\\server\share\pack"))
4083                == Path::new(r"\\?\UNC\server\share\pack"),
4084            "UNC path did not gain its verbatim prefix",
4085        )?;
4086        require(
4087            windows_verbatim_path(Path::new(r"\\?\C:\pack\parser"))
4088                == Path::new(r"\\?\C:\pack\parser"),
4089            "existing verbatim path changed",
4090        )?;
4091        let maximum = PathBuf::from(format!(
4092            r"C:\{}",
4093            "a".repeat(WINDOWS_PROCESS_CURRENT_DIRECTORY_MAX_UTF16_UNITS - 3)
4094        ));
4095        require(
4096            require_windows_framework_path(&maximum).is_ok(),
4097            "maximum supported working directory was rejected",
4098        )?;
4099        let overlong = PathBuf::from(format!(
4100            r"C:\{}",
4101            "a".repeat(WINDOWS_PROCESS_CURRENT_DIRECTORY_MAX_UTF16_UNITS - 2)
4102        ));
4103        require(
4104            require_windows_framework_path(&overlong).is_err(),
4105            "overlong working directory was accepted",
4106        )?;
4107        require(
4108            require_windows_framework_path(Path::new(r"\\?\C:\pack")).is_err(),
4109            "verbatim working directory was accepted",
4110        )
4111    }
4112
4113    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4114    #[test]
4115    fn cleaned_tombstone_makes_partial_slot_removal_retryable() -> TestResult {
4116        let root = tempfile::tempdir()?;
4117        let lifecycle = OptionalParserPackLifecycle::for_test(
4118            root.path().join("project"),
4119            root.path().join("storage"),
4120            Some(PackPlatform::WindowsX86_64),
4121        );
4122        let artifact_manifest = b"cleaned-tombstone-artifact";
4123        let identity = PackSlotIdentity {
4124            projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_owned(),
4125            artifact: ParserArtifactIdentity::for_bytes(artifact_manifest)
4126                .digest()
4127                .as_str()
4128                .to_owned(),
4129        };
4130        let version_root = lifecycle
4131            .versions_root()?
4132            .join(&identity.projectatlas_version);
4133        fs::create_dir_all(&version_root)?;
4134        let tombstone = version_root.join(format!(
4135            "{WINDOWS_CLEANED_TOMBSTONE_PREFIX}{}-retry1",
4136            &identity.artifact[..WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS]
4137        ));
4138        fs::create_dir(&tombstone)?;
4139        fs::write(
4140            tombstone.join(ARTIFACT_MANIFEST_FILE_NAME),
4141            artifact_manifest,
4142        )?;
4143        let partial_file = tombstone.join("partial.bin");
4144        fs::write(&partial_file, b"partial")?;
4145
4146        require(
4147            lifecycle.remove()?.changed,
4148            "partial cleaned tombstone retry did not remove storage",
4149        )?;
4150        require(!tombstone.exists(), "successful retry left its tombstone")?;
4151        require(
4152            !lifecycle.remove()?.changed,
4153            "partial tombstone retry was not idempotent",
4154        )
4155    }
4156
4157    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4158    #[test]
4159    fn slot_cleanup_uses_unique_atomic_tombstone_states() -> TestResult {
4160        let root = tempfile::tempdir()?;
4161        let lifecycle = OptionalParserPackLifecycle::for_test(
4162            root.path().join("project"),
4163            root.path().join("storage"),
4164            Some(PackPlatform::WindowsX86_64),
4165        );
4166        let artifact_manifest = b"atomic-tombstone-artifact";
4167        let identity = PackSlotIdentity {
4168            projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_owned(),
4169            artifact: ParserArtifactIdentity::for_bytes(artifact_manifest)
4170                .digest()
4171                .as_str()
4172                .to_owned(),
4173        };
4174        let slot_root = lifecycle.slot_path(&identity)?;
4175        fs::create_dir_all(&slot_root)?;
4176        fs::write(
4177            slot_root.join(ARTIFACT_MANIFEST_FILE_NAME),
4178            artifact_manifest,
4179        )?;
4180        fs::write(slot_root.join("must-move.bin"), b"slot")?;
4181        let installed = InstalledSlotPath {
4182            projectatlas_version: identity.projectatlas_version.clone(),
4183            artifact: identity.artifact.clone(),
4184            entry_root: slot_root.clone(),
4185            pack_root: Some(slot_root.clone()),
4186            state: InstalledSlotCleanupState::Installed,
4187        };
4188
4189        let pending = transition_slot_to_removing_tombstone(&installed)?;
4190        require(
4191            !slot_root.exists(),
4192            "deterministic slot survived transition",
4193        )?;
4194        require(
4195            pending.entry_root.is_dir(),
4196            "profile-pending tombstone was not published",
4197        )?;
4198        require(
4199            parse_windows_tombstone_name(
4200                pending
4201                    .entry_root
4202                    .file_name()
4203                    .and_then(|name| name.to_str())
4204                    .ok_or_else(|| io::Error::other("pending tombstone name missing"))?,
4205            ) == Some((
4206                InstalledSlotCleanupState::ProfilePending,
4207                identity.artifact[..WINDOWS_TOMBSTONE_ARTIFACT_PREFIX_HEX_CHARS].to_owned(),
4208            )),
4209            "profile-pending tombstone name was not strict",
4210        )?;
4211        require(
4212            windows_slot_cleanup_in_progress(
4213                pending
4214                    .entry_root
4215                    .parent()
4216                    .ok_or_else(|| io::Error::other("pending tombstone parent missing"))?,
4217                &identity.artifact,
4218            )?,
4219            "pending cleanup did not block artifact reuse",
4220        )?;
4221
4222        let cleaned = transition_tombstone_to_profile_cleaned(&pending)?;
4223        require(
4224            !pending.entry_root.exists() && cleaned.entry_root.is_dir(),
4225            "profile-cleaned tombstone transition was not atomic",
4226        )?;
4227        require(
4228            windows_slot_cleanup_in_progress(
4229                cleaned
4230                    .entry_root
4231                    .parent()
4232                    .ok_or_else(|| io::Error::other("cleaned tombstone parent missing"))?,
4233                &identity.artifact,
4234            )?,
4235            "cleaned tombstone did not block artifact reuse",
4236        )?;
4237        require(
4238            remove_tree_if_present(&cleaned.entry_root)?,
4239            "cleaned tombstone was not removable",
4240        )
4241    }
4242
4243    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4244    #[test]
4245    fn reusable_sibling_marker_never_authorizes_profile_cleanup_skip() -> TestResult {
4246        let root = tempfile::tempdir()?;
4247        let lifecycle = OptionalParserPackLifecycle::for_test(
4248            root.path().join("project"),
4249            root.path().join("storage"),
4250            Some(PackPlatform::WindowsX86_64),
4251        );
4252        let artifact_manifest = b"invalid";
4253        let identity = PackSlotIdentity {
4254            projectatlas_version: OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION.to_owned(),
4255            artifact: ParserArtifactIdentity::for_bytes(artifact_manifest)
4256                .digest()
4257                .as_str()
4258                .to_owned(),
4259        };
4260        let slot_root = lifecycle.slot_path(&identity)?;
4261        fs::create_dir_all(&slot_root)?;
4262        fs::write(
4263            slot_root.join(ARTIFACT_MANIFEST_FILE_NAME),
4264            artifact_manifest,
4265        )?;
4266        let marker = slot_root
4267            .parent()
4268            .ok_or_else(|| io::Error::other("slot parent missing"))?
4269            .join(format!(".{}.profile-cleaned", identity.artifact));
4270        fs::write(&marker, b"projectatlas-parser-profile-cleaned-v1\n")?;
4271
4272        let error = require_lifecycle_error(
4273            lifecycle.remove(),
4274            "reusable sibling marker bypassed profile verification",
4275        )?;
4276        require(
4277            matches!(
4278                error,
4279                OptionalParserPackLifecycleError::CleanupIncomplete { .. }
4280            ),
4281            "invalid marked slot did not fail closed",
4282        )?;
4283        require(
4284            marker.is_file(),
4285            "legacy marker was unexpectedly consumed as cleanup authority",
4286        )?;
4287        require(
4288            !slot_root.exists(),
4289            "invalid slot was not isolated from its deterministic path",
4290        )?;
4291        let tombstones = installed_slot_paths(&lifecycle.pack_root()?)?;
4292        require(
4293            tombstones.iter().any(|slot| {
4294                slot.artifact == identity.artifact
4295                    && slot.state == InstalledSlotCleanupState::ProfilePending
4296            }),
4297            "failed cleanup did not retain a unique retry tombstone",
4298        )
4299    }
4300
4301    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4302    #[test]
4303    fn cleanup_broker_timeout_terminates_reaps_and_drains() -> TestResult {
4304        let ping = validated_windows_directory()?
4305            .join("System32")
4306            .join("PING.EXE");
4307        let mut child = Command::new(&ping)
4308            .arg("-n")
4309            .arg("30")
4310            .arg("127.0.0.1")
4311            .stdin(Stdio::null())
4312            .stdout(Stdio::piped())
4313            .stderr(Stdio::piped())
4314            .spawn()?;
4315        let broker = PathBuf::from("cleanup-timeout-test");
4316        let operation_timeout = Duration::from_millis(50);
4317        let cleanup_timeout = Duration::from_secs(2);
4318        let directory = tempfile::tempdir()?;
4319        let attempts = Arc::new(AtomicUsize::new(0));
4320        let observed_attempts = Arc::clone(&attempts);
4321        let reaped = Arc::new(AtomicBool::new(false));
4322        let observed_reaped = Arc::clone(&reaped);
4323        let mut profile = TemporaryParserArtifactProfile::new(
4324            directory.path(),
4325            ParserArtifactIdentity::for_bytes(b"cleanup-timeout-artifact"),
4326        );
4327        profile.cleanup_override = Some(Box::new(move |_, _| {
4328            observed_attempts.fetch_add(1, Ordering::Relaxed);
4329            let result = supervise_cleanup_broker(
4330                &mut child,
4331                &broker,
4332                "test-profile",
4333                operation_timeout,
4334                cleanup_timeout,
4335            );
4336            observed_reaped.store(
4337                child
4338                    .try_wait()
4339                    .map_err(|source| io_error("observe test cleanup broker", &broker, source))?
4340                    .is_some(),
4341                Ordering::Relaxed,
4342            );
4343            result
4344        }));
4345        let started = Instant::now();
4346        let error = require_lifecycle_error(
4347            profile.cleanup(),
4348            "hung cleanup broker unexpectedly succeeded",
4349        )?;
4350        let elapsed = started.elapsed();
4351        require(
4352            matches!(&error, OptionalParserPackLifecycleError::CleanupIncomplete { message }
4353                if message.contains("test-profile") && message.contains("deadline")),
4354            &format!("timeout was not an operational cleanup failure: {error}"),
4355        )?;
4356        require(
4357            attempts.load(Ordering::Relaxed) == 1,
4358            "timed-out explicit cleanup was repeated by Drop",
4359        )?;
4360        require(
4361            reaped.load(Ordering::Relaxed),
4362            "hung cleanup broker was not reaped",
4363        )?;
4364        require(
4365            elapsed <= operation_timeout + cleanup_timeout + Duration::from_secs(1),
4366            "cleanup and destructor exceeded one timeout and reap/drain allowance",
4367        )
4368    }
4369
4370    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
4371    #[test]
4372    fn cleanup_broker_post_spawn_pipe_fault_still_reaps_child() -> TestResult {
4373        let ping = validated_windows_directory()?
4374            .join("System32")
4375            .join("PING.EXE");
4376        let mut child = Command::new(&ping)
4377            .arg("-n")
4378            .arg("30")
4379            .arg("127.0.0.1")
4380            .stdin(Stdio::null())
4381            .stdout(Stdio::piped())
4382            .stderr(Stdio::piped())
4383            .spawn()?;
4384        let retained_stdout = child.stdout.take();
4385        let broker = PathBuf::from("cleanup-pipe-fault-test");
4386        let error = require_lifecycle_error(
4387            supervise_cleanup_broker(
4388                &mut child,
4389                &broker,
4390                "test-profile",
4391                Duration::from_secs(1),
4392                Duration::from_secs(2),
4393            ),
4394            "missing cleanup pipe unexpectedly succeeded",
4395        )?;
4396        require(
4397            matches!(error, OptionalParserPackLifecycleError::InvalidData { .. }),
4398            "pipe-fault operation was misclassified as a cleanup failure",
4399        )?;
4400        require(
4401            child.try_wait()?.is_some(),
4402            "cleanup broker with a post-spawn pipe fault was not reaped",
4403        )?;
4404        drop(retained_stdout);
4405        Ok(())
4406    }
4407
4408    #[test]
4409    fn status_reports_installed_enabled_rollback_and_stale_states() -> TestResult {
4410        let root = tempfile::tempdir()?;
4411        let project = root.path().join("project");
4412        let storage = root.path().join("storage");
4413        let lifecycle = OptionalParserPackLifecycle::for_test(
4414            project,
4415            storage,
4416            Some(PackPlatform::LinuxX86_64),
4417        );
4418        require(
4419            lifecycle.status()?.state == OptionalParserPackState::Absent,
4420            "initial state was not absent",
4421        )?;
4422        let selected = test_slot('a');
4423        fs::create_dir_all(lifecycle.slot_path(&selected)?)?;
4424        require(
4425            lifecycle.status()?.state == OptionalParserPackState::InstalledDisabled,
4426            "installed slot was not reported disabled",
4427        )?;
4428        lifecycle.write_selection(&ProjectSelection::new(selected.clone(), None))?;
4429        require(
4430            lifecycle.status()?.state == OptionalParserPackState::Enabled,
4431            "enabled state missing",
4432        )?;
4433        let rollback = test_slot('b');
4434        fs::create_dir_all(lifecycle.slot_path(&rollback)?)?;
4435        lifecycle.write_selection(&ProjectSelection::new(selected.clone(), Some(rollback)))?;
4436        require(
4437            lifecycle.status()?.state == OptionalParserPackState::RollbackReady,
4438            "rollback-ready state missing",
4439        )?;
4440        fs::remove_dir_all(lifecycle.slot_path(&selected)?)?;
4441        require(
4442            lifecycle.status()?.state == OptionalParserPackState::Stale,
4443            "missing slot was not stale",
4444        )
4445    }
4446
4447    #[test]
4448    fn lifecycle_metadata_entry_bound_precedes_remove_mutation() -> TestResult {
4449        let root = tempfile::tempdir()?;
4450        let project = root.path().join("project");
4451        let storage = root.path().join("storage");
4452        let lifecycle = OptionalParserPackLifecycle::for_test(
4453            project,
4454            storage,
4455            Some(PackPlatform::LinuxX86_64),
4456        );
4457        lifecycle.write_selection(&ProjectSelection::new(test_slot('a'), None))?;
4458        let selection_before = fs::read(lifecycle.selection_path())?;
4459        let versions = lifecycle.pack_root()?.join("versions");
4460        fs::create_dir_all(&versions)?;
4461        for index in 0..=LIFECYCLE_METADATA_ENTRY_LIMIT {
4462            fs::create_dir(versions.join(format!("empty-{index:04}")))?;
4463        }
4464
4465        let status = lifecycle.status()?;
4466        require(
4467            status.installed_slots == 0
4468                && status.installed_slots_truncated
4469                && status.state == OptionalParserPackState::Stale,
4470            "over-limit empty version metadata was not reported as bounded stale state",
4471        )?;
4472        let error = require_lifecycle_error(
4473            lifecycle.remove(),
4474            "over-limit lifecycle metadata unexpectedly allowed removal",
4475        )?;
4476        require(
4477            matches!(
4478                error,
4479                OptionalParserPackLifecycleError::InvalidData { ref reason }
4480                    if reason == "parser-pack metadata entries exceed the cleanup bound"
4481            ),
4482            "over-limit lifecycle metadata returned the wrong removal failure",
4483        )?;
4484        require(
4485            fs::read(lifecycle.selection_path())? == selection_before
4486                && fs::read_dir(&versions)?.count()
4487                    == LIFECYCLE_METADATA_ENTRY_LIMIT.saturating_add(1),
4488            "bounded removal partially mutated selection or storage",
4489        )?;
4490
4491        let child_root = tempfile::tempdir()?;
4492        let child_lifecycle = OptionalParserPackLifecycle::for_test(
4493            child_root.path().join("project"),
4494            child_root.path().join("storage"),
4495            Some(PackPlatform::LinuxX86_64),
4496        );
4497        child_lifecycle.write_selection(&ProjectSelection::new(test_slot('b'), None))?;
4498        let child_selection_before = fs::read(child_lifecycle.selection_path())?;
4499        let child_version = child_lifecycle.pack_root()?.join("versions").join("0.4.0");
4500        fs::create_dir_all(&child_version)?;
4501        for index in 0..LIFECYCLE_METADATA_ENTRY_LIMIT {
4502            fs::write(child_version.join(format!("stale-{index:04}")), [])?;
4503        }
4504        let child_status = child_lifecycle.status()?;
4505        require(
4506            child_status.installed_slots == 0
4507                && child_status.installed_slots_truncated
4508                && child_status.state == OptionalParserPackState::Stale,
4509            "over-limit non-directory slot metadata was not reported as bounded stale state",
4510        )?;
4511        let child_error = require_lifecycle_error(
4512            child_lifecycle.remove(),
4513            "over-limit child metadata unexpectedly allowed removal",
4514        )?;
4515        require(
4516            matches!(
4517                child_error,
4518                OptionalParserPackLifecycleError::InvalidData { ref reason }
4519                    if reason == "parser-pack metadata entries exceed the cleanup bound"
4520            ),
4521            "over-limit child metadata returned the wrong removal failure",
4522        )?;
4523        require(
4524            fs::read(child_lifecycle.selection_path())? == child_selection_before
4525                && fs::read_dir(&child_version)?.count() == LIFECYCLE_METADATA_ENTRY_LIMIT,
4526            "child metadata overflow partially mutated selection or storage",
4527        )?;
4528
4529        let exact_root = tempfile::tempdir()?;
4530        let exact_lifecycle = OptionalParserPackLifecycle::for_test(
4531            exact_root.path().join("project"),
4532            exact_root.path().join("storage"),
4533            Some(PackPlatform::LinuxX86_64),
4534        );
4535        exact_lifecycle.write_selection(&ProjectSelection::new(test_slot('c'), None))?;
4536        let exact_pack_root = exact_lifecycle.pack_root()?;
4537        let exact_version = exact_pack_root.join("versions").join("0.4.0");
4538        fs::create_dir_all(&exact_version)?;
4539        for index in 0..LIFECYCLE_METADATA_ENTRY_LIMIT.saturating_sub(1) {
4540            fs::write(exact_version.join(format!("stale-{index:04}")), [])?;
4541        }
4542        require(
4543            !exact_lifecycle.status()?.installed_slots_truncated,
4544            "exact lifecycle metadata bound was reported as truncated",
4545        )?;
4546        let removed = exact_lifecycle.remove()?;
4547        require(
4548            removed.changed
4549                && removed.state == OptionalParserPackState::Absent
4550                && !exact_lifecycle.selection_path().exists()
4551                && !exact_pack_root.exists(),
4552            "exact lifecycle metadata bound did not permit complete removal",
4553        )
4554    }
4555
4556    /// Return one typed process failure for mutation-order tests.
4557    fn injected_admission_failure(_path: &Path) -> ParserSupervisorError {
4558        ParserSupervisorError::Cancelled {
4559            phase: "test artifact admission",
4560        }
4561    }
4562
4563    #[test]
4564    fn failed_archive_admission_publishes_no_slot_and_preserves_selection() -> TestResult {
4565        let root = tempfile::tempdir()?;
4566        let project = root.path().join("project");
4567        let storage = root.path().join("storage");
4568        let lifecycle = OptionalParserPackLifecycle::for_test(
4569            project,
4570            storage,
4571            Some(PackPlatform::LinuxX86_64),
4572        )
4573        .with_admission_failure(injected_admission_failure);
4574        let selected = test_slot('a');
4575        lifecycle.write_selection(&ProjectSelection::new(selected, None))?;
4576        let selection_before = fs::read(lifecycle.selection_path())?;
4577
4578        let error = require_lifecycle_error(
4579            lifecycle.install(&root.path().join("candidate.tar.zst")),
4580            "failed admission unexpectedly installed an archive",
4581        )?;
4582        require(
4583            matches!(
4584                error,
4585                OptionalParserPackLifecycleError::Supervisor(
4586                    ParserSupervisorError::Cancelled { .. }
4587                )
4588            ),
4589            "injected archive admission failure lost its typed source",
4590        )?;
4591        let (installed, truncated, cleanup_pending) =
4592            count_installed_slots(&lifecycle.pack_root()?)?;
4593        require(
4594            installed == 0 && !truncated && !cleanup_pending,
4595            "failed archive admission published an installed slot",
4596        )?;
4597        require(
4598            fs::read(lifecycle.selection_path())? == selection_before,
4599            "failed archive admission changed project selection",
4600        )
4601    }
4602
4603    #[test]
4604    fn failed_enable_admission_preserves_previous_selection_bytes() -> TestResult {
4605        let root = tempfile::tempdir()?;
4606        let project = root.path().join("project");
4607        let storage = root.path().join("storage");
4608        let lifecycle = OptionalParserPackLifecycle::for_test(
4609            project,
4610            storage,
4611            Some(PackPlatform::LinuxX86_64),
4612        )
4613        .with_admission_failure(injected_admission_failure);
4614        let selected = test_slot('a');
4615        let candidate = test_slot('b');
4616        lifecycle.write_selection(&ProjectSelection::new(selected, None))?;
4617        let selection_before = fs::read(lifecycle.selection_path())?;
4618
4619        let error = require_lifecycle_error(
4620            lifecycle.enable(&candidate.artifact),
4621            "failed admission unexpectedly selected a candidate",
4622        )?;
4623        require(
4624            matches!(
4625                error,
4626                OptionalParserPackLifecycleError::Supervisor(
4627                    ParserSupervisorError::Cancelled { .. }
4628                )
4629            ),
4630            "injected enable admission failure lost its typed source",
4631        )?;
4632        require(
4633            fs::read(lifecycle.selection_path())? == selection_before,
4634            "failed enable admission changed project selection",
4635        )
4636    }
4637
4638    #[test]
4639    fn failed_update_preserves_previous_selection_bytes() -> TestResult {
4640        let Some(platform) = host_pack_platform() else {
4641            return Ok(());
4642        };
4643        let root = tempfile::tempdir()?;
4644        let project = root.path().join("project");
4645        let storage = root.path().join("storage");
4646        let lifecycle = OptionalParserPackLifecycle::for_test(project, storage, Some(platform));
4647        let selected = test_slot('a');
4648        let slot_root = lifecycle.slot_path(&selected)?;
4649        fs::create_dir_all(&slot_root)?;
4650        fs::write(slot_root.join(ARTIFACT_MANIFEST_FILE_NAME), b"invalid")?;
4651        lifecycle.write_selection(&ProjectSelection::new(selected, None))?;
4652        let before = fs::read(lifecycle.selection_path())?;
4653        let archive = root.path().join("invalid.tar.zst");
4654        fs::write(&archive, b"invalid")?;
4655
4656        require(
4657            lifecycle.update(&archive).is_err(),
4658            "invalid update unexpectedly succeeded",
4659        )?;
4660        let after = fs::read(lifecycle.selection_path())?;
4661        require(before == after, "failed update changed the prior selection")
4662    }
4663
4664    #[test]
4665    fn failed_selection_publication_keeps_candidate_for_deterministic_retry() -> TestResult {
4666        let root = tempfile::tempdir()?;
4667        let project = root.path().join("project");
4668        let storage = root.path().join("storage");
4669        let lifecycle = OptionalParserPackLifecycle::for_test(
4670            project.clone(),
4671            storage.clone(),
4672            Some(PackPlatform::LinuxX86_64),
4673        );
4674        let selected = test_slot('a');
4675        let candidate = test_slot('b');
4676        let prior_rollback = test_slot('c');
4677        for slot in [&selected, &candidate, &prior_rollback] {
4678            fs::create_dir_all(lifecycle.slot_path(slot)?)?;
4679        }
4680        let previous = ProjectSelection::new(selected.clone(), Some(prior_rollback.clone()));
4681        lifecycle.write_selection(&previous)?;
4682        let selection_before = fs::read(lifecycle.selection_path())?;
4683
4684        let failing = OptionalParserPackLifecycle::for_test(
4685            project.clone(),
4686            storage.clone(),
4687            Some(PackPlatform::LinuxX86_64),
4688        )
4689        .with_selection_publication_failure();
4690        let error = require_lifecycle_error(
4691            failing.publish_installed_update(&previous, &candidate),
4692            "injected selection publication unexpectedly succeeded",
4693        )?;
4694        require(
4695            matches!(
4696                error,
4697                OptionalParserPackLifecycleError::InvalidData { ref reason }
4698                    if reason == "injected project selection publication failure"
4699            ),
4700            "selection publication failure lost its typed source",
4701        )?;
4702        require(
4703            fs::read(failing.selection_path())? == selection_before
4704                && failing.read_selection()?.as_ref() == Some(&previous),
4705            "failed selection publication changed selected or rollback state",
4706        )?;
4707        require(
4708            failing.slot_path(&selected)?.is_dir()
4709                && failing.slot_path(&prior_rollback)?.is_dir()
4710                && failing.slot_path(&candidate)?.is_dir(),
4711            "failed selection publication removed an immutable lifecycle slot",
4712        )?;
4713
4714        let retry = OptionalParserPackLifecycle::for_test(
4715            project,
4716            storage,
4717            Some(PackPlatform::LinuxX86_64),
4718        );
4719        require(
4720            retry.publish_installed_update(&previous, &candidate)?,
4721            "retry did not publish the retained candidate",
4722        )?;
4723        let selected_candidate = retry
4724            .read_selection()?
4725            .ok_or_else(|| invalid_data("retried selection is absent"))?;
4726        require(
4727            selected_candidate == ProjectSelection::new(candidate.clone(), Some(selected)),
4728            "retry did not select the candidate with the immediate prior slot as rollback",
4729        )?;
4730        let selection_after_retry = fs::read(retry.selection_path())?;
4731        require(
4732            !retry.publish_installed_update(&selected_candidate, &candidate)?
4733                && fs::read(retry.selection_path())? == selection_after_retry,
4734            "identical retry rewrote or changed the selected candidate",
4735        )
4736    }
4737
4738    #[test]
4739    fn archive_name_requires_the_versioned_release_asset() -> TestResult {
4740        let version = OPTIONAL_PARSER_PACK_PROJECTATLAS_VERSION;
4741        let platform = PackPlatform::LinuxX86_64;
4742        let expected = format!(
4743            "projectatlas-v{version}-broad-parser-{}.tar.zst",
4744            platform.as_str()
4745        );
4746        require(
4747            require_archive_name(Path::new(&expected), version, platform).is_ok(),
4748            "published versioned parser-pack archive was rejected",
4749        )?;
4750        for name in [
4751            format!("{ARCHIVE_ROOT}-{}.tar.zst", platform.as_str()),
4752            format!(
4753                "projectatlas-v0.0.0-broad-parser-{}.tar.zst",
4754                platform.as_str()
4755            ),
4756            format!(
4757                "projectatlas-v{version}-broad-parser-{}.tar.zst",
4758                PackPlatform::WindowsX86_64.as_str()
4759            ),
4760            "renamed-parser-pack.tar.zst".to_owned(),
4761        ] {
4762            require(
4763                require_archive_name(Path::new(&name), version, platform).is_err(),
4764                "non-canonical parser-pack archive name was accepted: {name}",
4765            )?;
4766        }
4767        Ok(())
4768    }
4769
4770    #[test]
4771    fn archive_extraction_rejects_non_regular_entries() -> TestResult {
4772        let root = tempfile::tempdir()?;
4773        let archive_path = root.path().join("invalid.tar.zst");
4774        let output = File::create(&archive_path)?;
4775        let encoder = zstd::Encoder::new(output, 1)?;
4776        let mut builder = tar::Builder::new(encoder);
4777        let mut header = tar::Header::new_gnu();
4778        header.set_entry_type(EntryType::Symlink);
4779        header.set_size(0);
4780        header.set_uid(0);
4781        header.set_gid(0);
4782        header.set_mtime(0);
4783        header.set_mode(PAYLOAD_MODE);
4784        header.set_cksum();
4785        builder.append_link(&mut header, format!("{ARCHIVE_ROOT}/payload"), "target")?;
4786        let encoder = builder.into_inner()?;
4787        encoder.finish()?.sync_all()?;
4788
4789        require(
4790            extract_archive(&archive_path, None).is_err(),
4791            "non-regular archive entry was accepted",
4792        )
4793    }
4794}