Skip to main content

car_inference/
model_management.rs

1//! Crash-safe, identity-bound state for CAR-managed local model artifacts.
2//!
3//! Raw model IDs never enter filenames. Every record is stored under the
4//! SHA-256 of the exact UTF-8 ID and repeats that ID inside a
5//! `deny_unknown_fields` payload so a collision or corrupt file fails closed.
6
7use std::fs::{File, OpenOptions};
8use std::io::{Read, Seek, Write};
9use std::path::{Path, PathBuf};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Mutex, OnceLock};
12
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16const MAX_MODEL_ID_BYTES: usize = 4 * 1024;
17const MAX_STATE_BYTES: u64 = 64 * 1024;
18/// Refusal reason for a quarantine that disappeared between CAR's own
19/// no-replace rename and the delete — on a first attempt or a resume, at the
20/// path-based guard or at the narrower window before the binding descriptor
21/// opens. Every one of those paths returns before the receipt and journal
22/// unlinks, so the removal journal is still on disk and running the removal
23/// again resumes it. Shared by both raise sites so the wording cannot drift.
24const QUARANTINE_VANISHED_REASON: &str = "quarantine vanished before deletion; the removal journal is retained, so retrying the removal resumes and completes it";
25
26#[derive(Debug, thiserror::Error)]
27pub enum ModelManagementError {
28    #[error("model-management I/O failed at {path}: {source}")]
29    Io {
30        path: PathBuf,
31        #[source]
32        source: std::io::Error,
33    },
34    #[error("invalid model-management state at {path}: {message}")]
35    InvalidState { path: PathBuf, message: String },
36    #[error("model-management identity mismatch at {path}: expected {expected}, found {actual}")]
37    IdentityMismatch {
38        path: PathBuf,
39        expected: String,
40        actual: String,
41    },
42    #[error("no CAR install receipt exists for {model_id}")]
43    MissingReceipt { model_id: String },
44    #[error("unsafe CAR-managed path for {model_id}: {path} ({reason})")]
45    UnsafeManagedPath {
46        model_id: String,
47        path: PathBuf,
48        reason: String,
49    },
50    #[error("local model {model_id} is in use")]
51    ModelInUse { model_id: String },
52    #[error("local model-management operation is already active for {model_id}")]
53    MutationInProgress { model_id: String },
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "snake_case")]
58pub enum ManagedArtifactKind {
59    Symlink,
60    Directory,
61    File,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct InstallReceipt {
67    pub model_id: String,
68    pub managed_path: PathBuf,
69    pub artifact_kind: ManagedArtifactKind,
70    pub source_model_id: String,
71    pub source_revision: Option<String>,
72    pub creation_generation: u64,
73    pub shared_cache_references: Vec<PathBuf>,
74    pub adopted: bool,
75}
76
77#[derive(Debug, Clone)]
78pub struct ModelManagementStore {
79    models_dir: PathBuf,
80    management_state_dir: PathBuf,
81    receipts_root: PathBuf,
82    tombstones_root: PathBuf,
83    installs_root: PathBuf,
84    removals_root: PathBuf,
85    leases_root: PathBuf,
86    mutation_locks_root: PathBuf,
87    /// Test-only seam that runs between the phases of a directory removal so
88    /// tests can race it the way a same-UID process would. Compiled out of
89    /// release builds and of every target without directory removal.
90    #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
91    removal_hook: Option<RemovalTestHook>,
92    #[cfg(test)]
93    identity_init_hook: Option<fn(&Path)>,
94}
95
96/// Whether this build can delete a CAR-managed directory through a
97/// descriptor-bound walk (`ModelManagementStore::remove_quarantined_directory`).
98/// Everywhere else recursive pathname deletion stays refused: directory
99/// receipts remain usable evidence but are never removable.
100pub(crate) const fn directory_removal_supported() -> bool {
101    cfg!(any(target_os = "macos", target_os = "linux"))
102}
103
104#[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub(crate) enum RemovalPhase {
107    /// The managed leaf has been renamed into its quarantine; nothing deleted yet.
108    AfterRename,
109    /// The quarantine passed the path-based identity check; the descriptor
110    /// that binds every later operation has not been opened yet.
111    BeforeRootOpen,
112    /// Every entry of the quarantined directory has been captured by descriptor;
113    /// nothing deleted yet.
114    AfterCapture,
115}
116
117#[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
118#[derive(Clone)]
119pub(crate) struct RemovalTestHook(std::sync::Arc<dyn Fn(RemovalPhase, &Path) + Send + Sync>);
120
121#[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
122impl std::fmt::Debug for RemovalTestHook {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.write_str("RemovalTestHook")
125    }
126}
127
128impl ModelManagementStore {
129    pub fn new(state_root: PathBuf, models_dir: PathBuf) -> Self {
130        let state_root = crate::resource_policy::normalized_state_root_key(&state_root);
131        let management_state_dir = state_root.join("model-management");
132        // `models_dir` is machine-shared even when two daemons use different
133        // CAR_HOME roots. Resolve aliases before deriving lock paths so those
134        // daemons coordinate on one immutable set of lock sentinels.
135        let models_dir = crate::resource_policy::normalized_state_root_key(&models_dir);
136        let shared_coordination_root = models_dir
137            .parent()
138            .unwrap_or(&models_dir)
139            .join(".car-model-management");
140        let store = Self {
141            models_dir,
142            receipts_root: management_state_dir.join("receipts"),
143            tombstones_root: management_state_dir.join("tombstones"),
144            installs_root: management_state_dir.join("installs"),
145            removals_root: management_state_dir.join("removals"),
146            leases_root: shared_coordination_root.join("activity"),
147            mutation_locks_root: shared_coordination_root.join("mutation"),
148            management_state_dir,
149            #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
150            removal_hook: None,
151            #[cfg(test)]
152            identity_init_hook: None,
153        };
154        // A crash can leave a CAR-owned quarantine after the managed leaf has
155        // already been atomically detached. Resume only those journaled,
156        // identity-matching transactions whose cross-process locks are idle.
157        store.resume_pending_quarantines();
158        store
159    }
160
161    pub fn models_dir(&self) -> &Path {
162        &self.models_dir
163    }
164
165    #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
166    pub(crate) fn with_removal_hook(
167        mut self,
168        hook: impl Fn(RemovalPhase, &Path) + Send + Sync + 'static,
169    ) -> Self {
170        self.removal_hook = Some(RemovalTestHook(std::sync::Arc::new(hook)));
171        self
172    }
173
174    pub fn management_state_dir(&self) -> &Path {
175        &self.management_state_dir
176    }
177
178    pub fn receipts_root(&self) -> &Path {
179        &self.receipts_root
180    }
181
182    pub fn receipt_path(&self, model_id: &str) -> PathBuf {
183        self.receipts_root.join(hashed_filename(model_id))
184    }
185
186    pub fn tombstones_root(&self) -> &Path {
187        &self.tombstones_root
188    }
189
190    pub fn tombstone_path(&self, model_id: &str) -> PathBuf {
191        self.tombstones_root.join(hashed_filename(model_id))
192    }
193
194    fn removal_journal_path(&self, model_id: &str) -> PathBuf {
195        self.removals_root.join(hashed_filename(model_id))
196    }
197
198    fn install_journal_path(&self, model_id: &str) -> PathBuf {
199        self.installs_root.join(hashed_filename(model_id))
200    }
201
202    pub fn leases_root(&self) -> &Path {
203        &self.leases_root
204    }
205
206    pub fn lease_path(&self, model_id: &str) -> PathBuf {
207        self.leases_root.join(hashed_filename(model_id))
208    }
209
210    pub fn mutation_lock_path(&self, model_id: &str) -> PathBuf {
211        self.mutation_locks_root.join(hashed_filename(model_id))
212    }
213
214    pub fn load_receipt(
215        &self,
216        model_id: &str,
217    ) -> Result<Option<InstallReceipt>, ModelManagementError> {
218        validate_model_id(model_id)?;
219        let path = self.receipt_path(model_id);
220        let bytes = match read_private_state(&path) {
221            Ok(bytes) => bytes,
222            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
223            Err(source) => return Err(ModelManagementError::Io { path, source }),
224        };
225        let receipt: InstallReceipt =
226            serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
227                path: path.clone(),
228                message: error.to_string(),
229            })?;
230        ensure_identity(&path, model_id, &receipt.model_id)?;
231        Ok(Some(receipt))
232    }
233
234    pub(crate) fn write_receipt(
235        &self,
236        receipt: &InstallReceipt,
237    ) -> Result<(), ModelManagementError> {
238        validate_model_id(&receipt.model_id)?;
239        if let Some(existing) = self.load_receipt(&receipt.model_id)? {
240            ensure_identity(
241                &self.receipt_path(&receipt.model_id),
242                &receipt.model_id,
243                &existing.model_id,
244            )?;
245        }
246        write_private_json(&self.receipt_path(&receipt.model_id), receipt)
247    }
248
249    /// Return whether CAR has a receipt whose current artifact still matches
250    /// the ownership evidence recorded at install/adoption time.
251    pub fn can_remove(&self, model_id: &str) -> Result<bool, ModelManagementError> {
252        let Some(receipt) = self.load_receipt(model_id)? else {
253            return Ok(false);
254        };
255        self.validate_managed_artifact(&receipt)?;
256        // Recursive pathname deletion cannot be made object-bound portably.
257        // Directory receipts are removable only where CAR has a
258        // descriptor-bound walk (`directory_removal_supported`); elsewhere they
259        // remain usable evidence but are never removable.
260        Ok(
261            receipt.artifact_kind != ManagedArtifactKind::Directory
262                || directory_removal_supported(),
263        )
264    }
265
266    pub fn acquire_lease(&self, model_id: &str) -> Result<ModelLease, ModelManagementError> {
267        validate_model_id(model_id)?;
268        // A load first enters the shared side of the immutable mutation gate,
269        // then takes its durable activity lease. Removal takes the exclusive
270        // side before it starts draining, closing the check/delete TOCTOU.
271        let mutation = self.open_identity_lock(&self.mutation_lock_path(model_id), model_id)?;
272        mutation
273            .lock_shared()
274            .map_err(|source| ModelManagementError::Io {
275                path: self.mutation_lock_path(model_id),
276                source,
277            })?;
278        let mut mutation = OwnedFileLock::new(mutation);
279        validate_lock_identity(&mut mutation, &self.mutation_lock_path(model_id), model_id)?;
280        let file = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
281        file.lock_shared()
282            .map_err(|source| ModelManagementError::Io {
283                path: self.lease_path(model_id),
284                source,
285            })?;
286        let mut file = OwnedFileLock::new(file);
287        validate_lock_identity(&mut file, &self.lease_path(model_id), model_id)?;
288        drop(mutation);
289        Ok(ModelLease { _file: file })
290    }
291
292    pub(crate) fn begin_mutation(
293        &self,
294        model_id: &str,
295    ) -> Result<ModelMutationGuard, ModelManagementError> {
296        validate_model_id(model_id)?;
297        let mut file = self.open_identity_lock(&self.mutation_lock_path(model_id), model_id)?;
298        validate_lock_identity(&mut file, &self.mutation_lock_path(model_id), model_id)?;
299        match file.try_lock() {
300            Ok(()) => Ok(ModelMutationGuard {
301                store: self.clone(),
302                model_id: model_id.to_string(),
303                _file: OwnedFileLock::new(file),
304            }),
305            Err(std::fs::TryLockError::WouldBlock) => {
306                Err(ModelManagementError::MutationInProgress {
307                    model_id: model_id.to_string(),
308                })
309            }
310            Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
311                path: self.mutation_lock_path(model_id),
312                source,
313            }),
314        }
315    }
316
317    pub fn model_in_use(&self, model_id: &str) -> Result<bool, ModelManagementError> {
318        validate_model_id(model_id)?;
319        let mut file = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
320        validate_lock_identity(&mut file, &self.lease_path(model_id), model_id)?;
321        match file.try_lock() {
322            Ok(()) => {
323                let _lock = OwnedFileLock::new(file);
324                Ok(false)
325            }
326            Err(std::fs::TryLockError::WouldBlock) => Ok(true),
327            Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
328                path: self.lease_path(model_id),
329                source,
330            }),
331        }
332    }
333
334    pub fn car_enabled(&self, model_id: &str) -> Result<bool, ModelManagementError> {
335        Ok(self.load_tombstone(model_id)?.is_none())
336    }
337
338    fn load_tombstone(
339        &self,
340        model_id: &str,
341    ) -> Result<Option<RemovalTombstone>, ModelManagementError> {
342        validate_model_id(model_id)?;
343        let path = self.tombstone_path(model_id);
344        let bytes = match read_private_state(&path) {
345            Ok(bytes) => bytes,
346            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
347            Err(source) => return Err(ModelManagementError::Io { path, source }),
348        };
349        let tombstone: RemovalTombstone =
350            serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
351                path: path.clone(),
352                message: error.to_string(),
353            })?;
354        ensure_identity(&path, model_id, &tombstone.model_id)?;
355        Ok(Some(tombstone))
356    }
357
358    pub(crate) fn clear_tombstone(&self, model_id: &str) -> Result<(), ModelManagementError> {
359        validate_model_id(model_id)?;
360        let _ = self.car_enabled(model_id)?;
361        let path = self.tombstone_path(model_id);
362        match std::fs::remove_file(&path) {
363            Ok(()) => sync_directory(path.parent().expect("tombstone path has parent"))
364                .map_err(|source| ModelManagementError::Io { path, source }),
365            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
366            Err(source) => Err(ModelManagementError::Io { path, source }),
367        }
368    }
369
370    /// Bind a usable artifact under the CAR models root to a durable receipt.
371    /// The artifact path is derived by CAR; callers cannot pass arbitrary
372    /// deletion targets through the public API.
373    pub(crate) fn record_managed_artifact(
374        &self,
375        model_id: &str,
376        source_model_id: &str,
377        source_revision: Option<String>,
378        creation_generation: u64,
379        adopted: bool,
380        managed_path: PathBuf,
381    ) -> Result<InstallReceipt, ModelManagementError> {
382        validate_model_id(model_id)?;
383        let artifact_kind = artifact_kind(&managed_path, model_id)?;
384        let shared_cache_references = collect_shared_references(&managed_path, model_id)?;
385        let receipt = InstallReceipt {
386            model_id: model_id.to_string(),
387            managed_path,
388            artifact_kind,
389            source_model_id: source_model_id.to_string(),
390            source_revision,
391            creation_generation,
392            shared_cache_references,
393            adopted,
394        };
395        self.validate_managed_artifact(&receipt)?;
396        self.write_receipt(&receipt)?;
397        self.clear_tombstone(model_id)?;
398        Ok(receipt)
399    }
400
401    pub(crate) fn begin_install_intent(
402        &self,
403        receipt: InstallReceipt,
404        staging_path: Option<&Path>,
405    ) -> Result<(), ModelManagementError> {
406        validate_model_id(&receipt.model_id)?;
407        validate_direct_child(&self.models_dir, &receipt.managed_path, &receipt.model_id)?;
408        let staging_path = staging_path.map(Path::to_path_buf);
409        let staging_identity = staging_path
410            .as_deref()
411            .map(|path| artifact_identity(path, &receipt.model_id))
412            .transpose()?;
413        if let Some(path) = staging_path.as_deref() {
414            validate_direct_child(&self.models_dir, path, &receipt.model_id)?;
415        }
416        let intent = InstallJournal {
417            model_id: receipt.model_id.clone(),
418            receipt,
419            staging_path,
420            staging_identity,
421        };
422        let path = self.install_journal_path(&intent.model_id);
423        if let Some(existing) = self.load_install_intent(&intent.model_id)? {
424            if existing == intent {
425                return Ok(());
426            }
427            return Err(ModelManagementError::InvalidState {
428                path,
429                message: "a different CAR install intent already exists for this model".into(),
430            });
431        }
432        if std::fs::symlink_metadata(&intent.receipt.managed_path).is_ok() {
433            return Err(ModelManagementError::UnsafeManagedPath {
434                model_id: intent.model_id,
435                path: intent.receipt.managed_path,
436                reason: "pre-existing unreceipted leaf cannot become CAR-owned through an install intent"
437                    .into(),
438            });
439        }
440        write_private_json(&path, &intent)
441    }
442
443    pub(crate) fn install_receipt_for_publication(
444        &self,
445        model_id: &str,
446        source_model_id: &str,
447        creation_generation: u64,
448        adopted: bool,
449        managed_path: PathBuf,
450        artifact_kind: ManagedArtifactKind,
451        artifact_source: &Path,
452    ) -> Result<InstallReceipt, ModelManagementError> {
453        validate_model_id(model_id)?;
454        let shared_cache_references = match artifact_kind {
455            ManagedArtifactKind::Symlink => {
456                vec![artifact_source
457                    .canonicalize()
458                    .map_err(|source| ModelManagementError::Io {
459                        path: artifact_source.to_path_buf(),
460                        source,
461                    })?]
462            }
463            ManagedArtifactKind::Directory => collect_shared_references(artifact_source, model_id)?,
464            ManagedArtifactKind::File => Vec::new(),
465        };
466        Ok(InstallReceipt {
467            model_id: model_id.to_string(),
468            managed_path,
469            artifact_kind,
470            source_model_id: source_model_id.to_string(),
471            source_revision: None,
472            creation_generation,
473            shared_cache_references,
474            adopted,
475        })
476    }
477
478    pub(crate) fn resume_install_intent(
479        &self,
480        model_id: &str,
481    ) -> Result<Option<InstallReceipt>, ModelManagementError> {
482        let Some(intent) = self.load_install_intent(model_id)? else {
483            return Ok(None);
484        };
485        if std::fs::symlink_metadata(&intent.receipt.managed_path).is_err() {
486            if let (Some(staging), Some(identity)) = (
487                intent.staging_path.as_deref(),
488                intent.staging_identity.as_ref(),
489            ) {
490                if std::fs::symlink_metadata(staging).is_ok() {
491                    ensure_artifact_identity(staging, model_id, identity)?;
492                    atomic_rename_noreplace(staging, &intent.receipt.managed_path).map_err(
493                        |source| ModelManagementError::Io {
494                            path: intent.receipt.managed_path.clone(),
495                            source,
496                        },
497                    )?;
498                    sync_directory(&self.models_dir).map_err(|source| {
499                        ModelManagementError::Io {
500                            path: self.models_dir.clone(),
501                            source,
502                        }
503                    })?;
504                } else {
505                    self.clear_install_intent(model_id)?;
506                    return Ok(None);
507                }
508            } else {
509                // No CAR-created leaf was published. Clearing only the intent
510                // is safe and lets the caller create a fresh projection.
511                self.clear_install_intent(model_id)?;
512                return Ok(None);
513            }
514        }
515        if let Some(identity) = intent.staging_identity.as_ref() {
516            ensure_artifact_identity(&intent.receipt.managed_path, model_id, identity)?;
517        }
518        self.validate_managed_artifact(&intent.receipt)?;
519        self.write_receipt(&intent.receipt)?;
520        self.clear_install_intent(model_id)?;
521        self.clear_tombstone(model_id)?;
522        Ok(Some(intent.receipt))
523    }
524
525    fn load_install_intent(
526        &self,
527        model_id: &str,
528    ) -> Result<Option<InstallJournal>, ModelManagementError> {
529        let path = self.install_journal_path(model_id);
530        let bytes = match read_private_state(&path) {
531            Ok(bytes) => bytes,
532            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
533            Err(source) => return Err(ModelManagementError::Io { path, source }),
534        };
535        let intent: InstallJournal =
536            serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
537                path: path.clone(),
538                message: error.to_string(),
539            })?;
540        ensure_identity(&path, model_id, &intent.model_id)?;
541        ensure_identity(&path, model_id, &intent.receipt.model_id)?;
542        validate_direct_child(&self.models_dir, &intent.receipt.managed_path, model_id)?;
543        if let Some(staging) = intent.staging_path.as_deref() {
544            validate_direct_child(&self.models_dir, staging, model_id)?;
545            if !staging
546                .file_name()
547                .and_then(|name| name.to_str())
548                .is_some_and(|name| name.starts_with(".car-install-"))
549            {
550                return Err(ModelManagementError::UnsafeManagedPath {
551                    model_id: model_id.to_string(),
552                    path: staging.to_path_buf(),
553                    reason: "install journal staging path lacks CAR staging identity".into(),
554                });
555            }
556        }
557        Ok(Some(intent))
558    }
559
560    fn clear_install_intent(&self, model_id: &str) -> Result<(), ModelManagementError> {
561        let path = self.install_journal_path(model_id);
562        match std::fs::remove_file(&path) {
563            Ok(()) => sync_directory(&self.installs_root)
564                .map_err(|source| ModelManagementError::Io { path, source }),
565            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
566            Err(source) => Err(ModelManagementError::Io { path, source }),
567        }
568    }
569
570    /// Create or reuse a CAR-owned symlink projection for an artifact that
571    /// physically lives in a shared cache. Shared bytes remain outside CAR's
572    /// ownership and are never removed with the projection.
573    pub(crate) fn materialize_managed_projection(
574        &self,
575        managed_leaf: &str,
576        source: &Path,
577    ) -> Result<PathBuf, ModelManagementError> {
578        validate_managed_leaf(managed_leaf, &self.models_dir)?;
579        create_private_dir(&self.models_dir)?;
580        let models_root = canonical_directory(&self.models_dir)?;
581        let source_parent =
582            source
583                .parent()
584                .ok_or_else(|| ModelManagementError::UnsafeManagedPath {
585                    model_id: managed_leaf.to_string(),
586                    path: source.to_path_buf(),
587                    reason: "source has no parent".into(),
588                })?;
589        if canonical_directory(source_parent)? == models_root
590            && source == self.models_dir.join(managed_leaf)
591        {
592            return Err(ModelManagementError::UnsafeManagedPath {
593                model_id: managed_leaf.to_string(),
594                path: source.to_path_buf(),
595                reason: "pre-existing unreceipted managed projection cannot become CAR-owned"
596                    .into(),
597            });
598        }
599        let target = source
600            .canonicalize()
601            .map_err(|source_error| ModelManagementError::Io {
602                path: source.to_path_buf(),
603                source: source_error,
604            })?;
605        let managed = self.models_dir.join(managed_leaf);
606        if std::fs::symlink_metadata(&managed).is_ok() {
607            return Err(ModelManagementError::UnsafeManagedPath {
608                model_id: managed_leaf.to_string(),
609                path: managed,
610                reason: "pre-existing managed projection has no CAR creation receipt".into(),
611            });
612        }
613        static NEXT_PROJECTION: AtomicU64 = AtomicU64::new(1);
614        let tmp = self.models_dir.join(format!(
615            ".managed-projection.{}.{}",
616            std::process::id(),
617            NEXT_PROJECTION.fetch_add(1, Ordering::Relaxed)
618        ));
619        create_symlink(&target, &tmp)?;
620        let result = atomic_rename_noreplace(&tmp, &managed).map_err(|source| {
621            ModelManagementError::UnsafeManagedPath {
622                model_id: managed_leaf.to_string(),
623                path: managed.clone(),
624                reason: format!("managed projection publication raced or failed: {source}"),
625            }
626        });
627        if result.is_err() {
628            let _ = std::fs::remove_file(&tmp);
629        }
630        result?;
631        sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
632            path: self.models_dir.clone(),
633            source,
634        })?;
635        Ok(managed)
636    }
637
638    pub(crate) fn create_install_staging(
639        &self,
640        model_id: &str,
641    ) -> Result<PathBuf, ModelManagementError> {
642        validate_model_id(model_id)?;
643        create_private_dir(&self.models_dir)?;
644        let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
645        for _ in 0..16 {
646            let candidate = self.models_dir.join(format!(
647                ".car-install-{}-{:032x}",
648                &digest[..16],
649                rand::random::<u128>()
650            ));
651            match std::fs::create_dir(&candidate) {
652                Ok(()) => {
653                    harden_private_directory(&candidate)?;
654                    return Ok(candidate);
655                }
656                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue,
657                Err(source) => {
658                    return Err(ModelManagementError::Io {
659                        path: candidate,
660                        source,
661                    });
662                }
663            }
664        }
665        Err(ModelManagementError::InvalidState {
666            path: self.models_dir.clone(),
667            message: "could not allocate a collision-resistant install staging directory".into(),
668        })
669    }
670
671    pub(crate) fn publish_install_staging(
672        &self,
673        model_id: &str,
674        staging: &Path,
675        managed_leaf: &str,
676    ) -> Result<PathBuf, ModelManagementError> {
677        validate_model_id(model_id)?;
678        validate_direct_child(&self.models_dir, staging, model_id)?;
679        if !staging
680            .file_name()
681            .and_then(|name| name.to_str())
682            .is_some_and(|name| name.starts_with(".car-install-"))
683        {
684            return Err(ModelManagementError::UnsafeManagedPath {
685                model_id: model_id.to_string(),
686                path: staging.to_path_buf(),
687                reason: "install staging path lacks CAR staging identity".into(),
688            });
689        }
690        validate_managed_leaf(managed_leaf, &self.models_dir)?;
691        let managed = self.models_dir.join(managed_leaf);
692        atomic_rename_noreplace(staging, &managed).map_err(|source| {
693            ModelManagementError::UnsafeManagedPath {
694                model_id: model_id.to_string(),
695                path: managed.clone(),
696                reason: format!("could not atomically publish CAR-owned install: {source}"),
697            }
698        })?;
699        sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
700            path: self.models_dir.clone(),
701            source,
702        })?;
703        Ok(managed)
704    }
705
706    pub(crate) fn discard_install_staging(&self, staging: &Path) {
707        // Deliberately fail closed. A same-UID attacker can swap a verified
708        // directory before a pathname-recursive delete. Leave failed staging
709        // journal/evidence for future object-bound cleanup instead of risking
710        // deletion of shared model bytes.
711        let _ = staging;
712    }
713
714    /// Project a hand-installed or shared artifact into a distinct CAR-owned
715    /// leaf. The original path is never promoted into CAR ownership merely by
716    /// adoption, even when it already sits beneath `models_dir`.
717    pub(crate) fn materialize_adopted_projection(
718        &self,
719        model_id: &str,
720        source: &Path,
721    ) -> Result<PathBuf, ModelManagementError> {
722        validate_model_id(model_id)?;
723        let managed = self.adopted_projection_path(model_id)?;
724        let leaf = managed
725            .file_name()
726            .and_then(|name| name.to_str())
727            .expect("adopted projection is an UTF-8 direct child");
728        self.materialize_managed_projection(leaf, source)
729    }
730
731    pub(crate) fn adopted_projection_path(
732        &self,
733        model_id: &str,
734    ) -> Result<PathBuf, ModelManagementError> {
735        validate_model_id(model_id)?;
736        let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
737        Ok(self.models_dir.join(format!(".car-adopted-{digest}")))
738    }
739
740    fn remove_with_mutation(
741        &self,
742        model_id: &str,
743        removal_generation: u64,
744    ) -> Result<RemoveFromCarResult, ModelManagementError> {
745        let _activity = self.lock_idle_activity(model_id)?;
746
747        if let Some(journal) = self.load_removal_journal(model_id)? {
748            write_private_json(
749                &self.tombstone_path(model_id),
750                &RemovalTombstone {
751                    model_id: model_id.to_string(),
752                    removal_generation: journal.removal_generation,
753                    artifact_kind: Some(journal.receipt.artifact_kind),
754                    preserved_shared_cache_references: journal
755                        .receipt
756                        .shared_cache_references
757                        .clone(),
758                },
759            )?;
760            return self.resume_removal_journal(journal);
761        }
762
763        let Some(receipt) = self.load_receipt(model_id)? else {
764            if let Some(tombstone) = self.load_tombstone(model_id)? {
765                if let Some(artifact_kind) = tombstone.artifact_kind {
766                    return Ok(RemoveFromCarResult {
767                        model_id: model_id.to_string(),
768                        artifact_kind,
769                        preserved_shared_cache_references: tombstone
770                            .preserved_shared_cache_references,
771                    });
772                }
773            }
774            return Err(ModelManagementError::MissingReceipt {
775                model_id: model_id.to_string(),
776            });
777        };
778        self.validate_managed_artifact(&receipt)?;
779        if receipt.artifact_kind == ManagedArtifactKind::Directory && !directory_removal_supported()
780        {
781            return Err(ModelManagementError::UnsafeManagedPath {
782                model_id: model_id.to_string(),
783                path: receipt.managed_path,
784                reason: "recursive directory removal is unsupported without object-bound traversal"
785                    .into(),
786            });
787        }
788        let artifact_identity = artifact_identity(&receipt.managed_path, model_id)?;
789        let quarantine = self.allocate_quarantine(model_id)?;
790
791        write_private_json(
792            &self.tombstone_path(model_id),
793            &RemovalTombstone {
794                model_id: model_id.to_string(),
795                removal_generation,
796                artifact_kind: Some(receipt.artifact_kind),
797                preserved_shared_cache_references: receipt.shared_cache_references.clone(),
798            },
799        )?;
800        let journal = RemovalJournal {
801            model_id: model_id.to_string(),
802            removal_generation,
803            receipt,
804            quarantine_path: quarantine,
805            artifact_identity,
806        };
807        write_private_json(&self.removal_journal_path(model_id), &journal)?;
808        self.resume_removal_journal(journal)
809    }
810
811    fn lock_idle_activity(&self, model_id: &str) -> Result<OwnedFileLock, ModelManagementError> {
812        let mut activity = self.open_identity_lock(&self.lease_path(model_id), model_id)?;
813        validate_lock_identity(&mut activity, &self.lease_path(model_id), model_id)?;
814        match activity.try_lock() {
815            Ok(()) => Ok(OwnedFileLock::new(activity)),
816            Err(std::fs::TryLockError::WouldBlock) => Err(ModelManagementError::ModelInUse {
817                model_id: model_id.to_string(),
818            }),
819            Err(std::fs::TryLockError::Error(source)) => Err(ModelManagementError::Io {
820                path: self.lease_path(model_id),
821                source,
822            }),
823        }
824    }
825
826    fn load_removal_journal(
827        &self,
828        model_id: &str,
829    ) -> Result<Option<RemovalJournal>, ModelManagementError> {
830        let path = self.removal_journal_path(model_id);
831        let bytes = match read_private_state(&path) {
832            Ok(bytes) => bytes,
833            Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
834            Err(source) => return Err(ModelManagementError::Io { path, source }),
835        };
836        let journal: RemovalJournal =
837            serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
838                path: path.clone(),
839                message: error.to_string(),
840            })?;
841        ensure_identity(&path, model_id, &journal.model_id)?;
842        ensure_identity(&path, model_id, &journal.receipt.model_id)?;
843        validate_direct_child(&self.models_dir, &journal.receipt.managed_path, model_id)?;
844        if journal.receipt.artifact_kind == ManagedArtifactKind::Directory
845            && !directory_removal_supported()
846        {
847            return Err(ModelManagementError::UnsafeManagedPath {
848                model_id: model_id.to_string(),
849                path: journal.receipt.managed_path,
850                reason: "recursive directory removal journal is unsupported".into(),
851            });
852        }
853        validate_direct_child(&self.models_dir, &journal.quarantine_path, model_id)?;
854        if !journal
855            .quarantine_path
856            .file_name()
857            .and_then(|name| name.to_str())
858            .is_some_and(|name| name.starts_with(".car-remove-"))
859        {
860            return Err(ModelManagementError::UnsafeManagedPath {
861                model_id: model_id.to_string(),
862                path: journal.quarantine_path,
863                reason: "removal journal has an invalid quarantine path".into(),
864            });
865        }
866        Ok(Some(journal))
867    }
868
869    fn allocate_quarantine(&self, model_id: &str) -> Result<PathBuf, ModelManagementError> {
870        let digest = hex::encode(Sha256::digest(model_id.as_bytes()));
871        for _ in 0..16 {
872            let candidate = self.models_dir.join(format!(
873                ".car-remove-{}-{:032x}",
874                &digest[..16],
875                rand::random::<u128>()
876            ));
877            if std::fs::symlink_metadata(&candidate)
878                .is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
879            {
880                return Ok(candidate);
881            }
882        }
883        Err(ModelManagementError::InvalidState {
884            path: self.models_dir.clone(),
885            message: "could not allocate a collision-resistant removal quarantine".into(),
886        })
887    }
888
889    fn resume_removal_journal(
890        &self,
891        journal: RemovalJournal,
892    ) -> Result<RemoveFromCarResult, ModelManagementError> {
893        let model_id = &journal.model_id;
894        if journal.receipt.artifact_kind == ManagedArtifactKind::Directory
895            && !directory_removal_supported()
896        {
897            return Err(ModelManagementError::UnsafeManagedPath {
898                model_id: model_id.clone(),
899                path: journal.receipt.managed_path,
900                reason: "recursive directory removal is unsupported without object-bound traversal"
901                    .into(),
902            });
903        }
904        let original_exists = std::fs::symlink_metadata(&journal.receipt.managed_path).is_ok();
905        let quarantine_exists = std::fs::symlink_metadata(&journal.quarantine_path).is_ok();
906        let mut renamed_now = false;
907        if original_exists && quarantine_exists {
908            return Err(ModelManagementError::UnsafeManagedPath {
909                model_id: model_id.clone(),
910                path: journal.receipt.managed_path.clone(),
911                reason: "both managed artifact and removal quarantine exist".into(),
912            });
913        }
914        if original_exists {
915            self.validate_managed_artifact(&journal.receipt)?;
916            ensure_artifact_identity(
917                &journal.receipt.managed_path,
918                model_id,
919                &journal.artifact_identity,
920            )?;
921            // Revalidate the parent and leaf identity immediately before the
922            // same-root no-replace rename. Unsupported platforms fail closed.
923            validate_direct_child(&self.models_dir, &journal.receipt.managed_path, model_id)?;
924            ensure_artifact_identity(
925                &journal.receipt.managed_path,
926                model_id,
927                &journal.artifact_identity,
928            )?;
929            atomic_rename_noreplace(&journal.receipt.managed_path, &journal.quarantine_path)
930                .map_err(|source| ModelManagementError::Io {
931                    path: journal.receipt.managed_path.clone(),
932                    source,
933                })?;
934            sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
935                path: self.models_dir.clone(),
936                source,
937            })?;
938            renamed_now = true;
939            #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
940            if let Some(hook) = &self.removal_hook {
941                (hook.0)(RemovalPhase::AfterRename, &journal.quarantine_path);
942            }
943        }
944
945        if std::fs::symlink_metadata(&journal.quarantine_path).is_err() && renamed_now {
946            // We detached the leaf moments ago and it is already gone from its
947            // quarantine: something else moved it. Never report that as a
948            // completed removal; the journal stays so a later resume can look
949            // again.
950            return Err(ModelManagementError::UnsafeManagedPath {
951                model_id: model_id.clone(),
952                path: journal.quarantine_path,
953                reason: QUARANTINE_VANISHED_REASON.into(),
954            });
955        }
956
957        if std::fs::symlink_metadata(&journal.quarantine_path).is_ok() {
958            // A resumed directory removal may already be partially deleted, so
959            // the quarantine is matched on kind, device, and inode only.
960            ensure_removal_identity(
961                &journal.quarantine_path,
962                model_id,
963                &journal.artifact_identity,
964            )?;
965            match journal.receipt.artifact_kind {
966                #[cfg(any(target_os = "macos", target_os = "linux"))]
967                ManagedArtifactKind::Directory => {
968                    self.remove_quarantined_directory(
969                        &journal.quarantine_path,
970                        &journal.artifact_identity,
971                        model_id,
972                    )?;
973                }
974                #[cfg(not(any(target_os = "macos", target_os = "linux")))]
975                ManagedArtifactKind::Directory => {
976                    return Err(ModelManagementError::UnsafeManagedPath {
977                        model_id: model_id.clone(),
978                        path: journal.quarantine_path,
979                        reason:
980                            "recursive directory removal is unsupported without object-bound traversal"
981                                .into(),
982                    });
983                }
984                ManagedArtifactKind::Symlink | ManagedArtifactKind::File => {
985                    std::fs::remove_file(&journal.quarantine_path).map_err(|source| {
986                        ModelManagementError::Io {
987                            path: journal.quarantine_path.clone(),
988                            source,
989                        }
990                    })?;
991                }
992            }
993            sync_directory(&self.models_dir).map_err(|source| ModelManagementError::Io {
994                path: self.models_dir.clone(),
995                source,
996            })?;
997        }
998
999        let receipt_path = self.receipt_path(model_id);
1000        match std::fs::remove_file(&receipt_path) {
1001            Ok(()) => {
1002                sync_directory(&self.receipts_root).map_err(|source| ModelManagementError::Io {
1003                    path: self.receipts_root.clone(),
1004                    source,
1005                })?
1006            }
1007            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1008            Err(source) => {
1009                return Err(ModelManagementError::Io {
1010                    path: receipt_path,
1011                    source,
1012                });
1013            }
1014        }
1015        let journal_path = self.removal_journal_path(model_id);
1016        match std::fs::remove_file(&journal_path) {
1017            Ok(()) => {
1018                sync_directory(&self.removals_root).map_err(|source| ModelManagementError::Io {
1019                    path: self.removals_root.clone(),
1020                    source,
1021                })?
1022            }
1023            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
1024            Err(source) => {
1025                return Err(ModelManagementError::Io {
1026                    path: journal_path,
1027                    source,
1028                });
1029            }
1030        }
1031        Ok(RemoveFromCarResult {
1032            model_id: model_id.clone(),
1033            artifact_kind: journal.receipt.artifact_kind,
1034            preserved_shared_cache_references: journal.receipt.shared_cache_references,
1035        })
1036    }
1037
1038    fn resume_pending_quarantines(&self) {
1039        let Ok(entries) = std::fs::read_dir(&self.removals_root) else {
1040            return;
1041        };
1042        for entry in entries.flatten() {
1043            if !is_hashed_state_filename(&entry.file_name()) {
1044                continue;
1045            }
1046            let Ok(bytes) = read_private_state(&entry.path()) else {
1047                continue;
1048            };
1049            let Ok(candidate) = serde_json::from_slice::<RemovalJournal>(&bytes) else {
1050                continue;
1051            };
1052            if entry.path() != self.removal_journal_path(&candidate.model_id) {
1053                continue;
1054            }
1055            let Ok(guard) = self.begin_mutation(&candidate.model_id) else {
1056                continue;
1057            };
1058            let Ok(Some(journal)) = self.load_removal_journal(&candidate.model_id) else {
1059                continue;
1060            };
1061            if std::fs::symlink_metadata(&journal.receipt.managed_path).is_ok() {
1062                continue;
1063            }
1064            let _ = guard.resume_existing(journal);
1065        }
1066    }
1067
1068    fn open_identity_lock(
1069        &self,
1070        path: &Path,
1071        model_id: &str,
1072    ) -> Result<File, ModelManagementError> {
1073        let parent = path.parent().expect("lock path has parent");
1074        create_private_dir(parent)?;
1075        // A new identity inode is visible before it can be locked and filled.
1076        // Serialize that window with a permanent, non-JSON gate. The gate itself
1077        // needs no initialization payload, so whichever opener locks it first
1078        // can safely initialize an identity. Never rename or unlink either inode.
1079        let initialization_path = parent.join(".initialization.lock");
1080        let initialization = match open_private_new(&initialization_path) {
1081            Ok(file) => file,
1082            Err(ModelManagementError::Io { source, .. })
1083                if source.kind() == std::io::ErrorKind::AlreadyExists =>
1084            {
1085                open_existing_identity_lock(&initialization_path)?
1086            }
1087            Err(error) => return Err(error),
1088        };
1089        initialization
1090            .lock()
1091            .map_err(|source| ModelManagementError::Io {
1092                path: initialization_path,
1093                source,
1094            })?;
1095        let _initialization = OwnedFileLock::new(initialization);
1096        // This guard drops on return, before callers wait for any existing
1097        // activity/mutation lock; existing lease holders never need this gate.
1098        match open_private_new(path) {
1099            Ok(file) => {
1100                #[cfg(test)]
1101                if let Some(hook) = self.identity_init_hook {
1102                    hook(path);
1103                }
1104                file.lock().map_err(|source| ModelManagementError::Io {
1105                    path: path.to_path_buf(),
1106                    source,
1107                })?;
1108                let mut file = OwnedFileLock::new(file);
1109                let bytes = serde_json::to_vec(&IdentityRecord {
1110                    model_id: model_id.to_string(),
1111                })
1112                .map_err(|error| ModelManagementError::InvalidState {
1113                    path: path.to_path_buf(),
1114                    message: error.to_string(),
1115                })?;
1116                file.write_all(&bytes)
1117                    .and_then(|_| file.sync_all())
1118                    .map_err(|source| ModelManagementError::Io {
1119                        path: path.to_path_buf(),
1120                        source,
1121                    })?;
1122                return file
1123                    .into_unlocked_file()
1124                    .map_err(|source| ModelManagementError::Io {
1125                        path: path.to_path_buf(),
1126                        source,
1127                    });
1128            }
1129            Err(ModelManagementError::Io { source, .. })
1130                if source.kind() == std::io::ErrorKind::AlreadyExists => {}
1131            Err(error) => return Err(error),
1132        }
1133        open_existing_identity_lock(path)
1134    }
1135
1136    fn validate_managed_artifact(
1137        &self,
1138        receipt: &InstallReceipt,
1139    ) -> Result<(), ModelManagementError> {
1140        let models_root = validate_models_root(&self.models_dir, &receipt.model_id)?;
1141        if let Some(hf_root) = shared_hugging_face_root() {
1142            let hf_root = crate::resource_policy::normalized_state_root_key(&hf_root);
1143            if models_root.starts_with(&hf_root) || hf_root.starts_with(&models_root) {
1144                return Err(ModelManagementError::UnsafeManagedPath {
1145                    model_id: receipt.model_id.clone(),
1146                    path: receipt.managed_path.clone(),
1147                    reason: "managed models root overlaps the shared Hugging Face cache".into(),
1148                });
1149            }
1150        }
1151        let parent = receipt.managed_path.parent().ok_or_else(|| {
1152            ModelManagementError::UnsafeManagedPath {
1153                model_id: receipt.model_id.clone(),
1154                path: receipt.managed_path.clone(),
1155                reason: "managed path has no parent".into(),
1156            }
1157        })?;
1158        let canonical_parent = canonical_directory(parent)?;
1159        if canonical_parent != models_root || receipt.managed_path == models_root {
1160            return Err(ModelManagementError::UnsafeManagedPath {
1161                model_id: receipt.model_id.clone(),
1162                path: receipt.managed_path.clone(),
1163                reason: "artifact is not a direct child of the managed models root".into(),
1164            });
1165        }
1166        let actual = artifact_kind(&receipt.managed_path, &receipt.model_id)?;
1167        if actual != receipt.artifact_kind {
1168            return Err(ModelManagementError::UnsafeManagedPath {
1169                model_id: receipt.model_id.clone(),
1170                path: receipt.managed_path.clone(),
1171                reason: format!(
1172                    "receipt says {:?}, filesystem is {:?}",
1173                    receipt.artifact_kind, actual
1174                ),
1175            });
1176        }
1177        if actual == ManagedArtifactKind::Symlink {
1178            let target =
1179                receipt
1180                    .managed_path
1181                    .canonicalize()
1182                    .map_err(|source| ModelManagementError::Io {
1183                        path: receipt.managed_path.clone(),
1184                        source,
1185                    })?;
1186            if !receipt
1187                .shared_cache_references
1188                .iter()
1189                .any(|path| path == &target)
1190            {
1191                return Err(ModelManagementError::UnsafeManagedPath {
1192                    model_id: receipt.model_id.clone(),
1193                    path: receipt.managed_path.clone(),
1194                    reason: "symlink target does not match a preserved shared-cache reference"
1195                        .into(),
1196                });
1197            }
1198        } else {
1199            let current_references =
1200                collect_shared_references(&receipt.managed_path, &receipt.model_id)?;
1201            if current_references != receipt.shared_cache_references {
1202                return Err(ModelManagementError::UnsafeManagedPath {
1203                    model_id: receipt.model_id.clone(),
1204                    path: receipt.managed_path.clone(),
1205                    reason: "shared-cache references no longer match the install receipt".into(),
1206                });
1207            }
1208        }
1209        Ok(())
1210    }
1211}
1212
1213// Closing a descriptor alone does not release flock when a concurrently
1214// spawned child inherited the open-file description. End ownership explicitly
1215// at the Rust guard boundary, including temporary initialization/activity locks.
1216struct OwnedFileLock(Option<File>);
1217
1218impl OwnedFileLock {
1219    fn new(file: File) -> Self {
1220        Self(Some(file))
1221    }
1222
1223    fn into_unlocked_file(mut self) -> std::io::Result<File> {
1224        self.unlock()?;
1225        Ok(self.0.take().expect("owned lock has a file"))
1226    }
1227}
1228
1229impl std::ops::Deref for OwnedFileLock {
1230    type Target = File;
1231
1232    fn deref(&self) -> &File {
1233        self.0.as_ref().expect("owned lock has a file")
1234    }
1235}
1236
1237impl std::ops::DerefMut for OwnedFileLock {
1238    fn deref_mut(&mut self) -> &mut File {
1239        self.0.as_mut().expect("owned lock has a file")
1240    }
1241}
1242
1243impl Drop for OwnedFileLock {
1244    fn drop(&mut self) {
1245        if let Some(file) = &self.0 {
1246            let _ = file.unlock();
1247        }
1248    }
1249}
1250
1251pub(crate) struct ModelMutationGuard {
1252    store: ModelManagementStore,
1253    model_id: String,
1254    _file: OwnedFileLock,
1255}
1256
1257impl ModelMutationGuard {
1258    pub(crate) fn remove(
1259        self,
1260        removal_generation: u64,
1261    ) -> Result<RemoveFromCarResult, ModelManagementError> {
1262        self.store
1263            .remove_with_mutation(&self.model_id, removal_generation)
1264    }
1265
1266    fn resume_existing(
1267        self,
1268        journal: RemovalJournal,
1269    ) -> Result<RemoveFromCarResult, ModelManagementError> {
1270        if journal.model_id != self.model_id {
1271            return Err(ModelManagementError::IdentityMismatch {
1272                path: self.store.removal_journal_path(&self.model_id),
1273                expected: self.model_id,
1274                actual: journal.model_id,
1275            });
1276        }
1277        let _activity = self.store.lock_idle_activity(&self.model_id)?;
1278        write_private_json(
1279            &self.store.tombstone_path(&self.model_id),
1280            &RemovalTombstone {
1281                model_id: self.model_id.clone(),
1282                removal_generation: journal.removal_generation,
1283                artifact_kind: Some(journal.receipt.artifact_kind),
1284                preserved_shared_cache_references: journal.receipt.shared_cache_references.clone(),
1285            },
1286        )?;
1287        self.store.resume_removal_journal(journal)
1288    }
1289}
1290
1291pub struct ModelLease {
1292    _file: OwnedFileLock,
1293}
1294
1295#[derive(Debug, Clone, PartialEq, Eq)]
1296pub struct RemoveFromCarResult {
1297    pub model_id: String,
1298    pub artifact_kind: ManagedArtifactKind,
1299    pub preserved_shared_cache_references: Vec<PathBuf>,
1300}
1301
1302#[derive(Debug, Serialize, Deserialize)]
1303#[serde(deny_unknown_fields)]
1304struct IdentityRecord {
1305    model_id: String,
1306}
1307
1308#[derive(Debug, Serialize, Deserialize)]
1309#[serde(deny_unknown_fields)]
1310struct RemovalTombstone {
1311    model_id: String,
1312    removal_generation: u64,
1313    #[serde(default)]
1314    artifact_kind: Option<ManagedArtifactKind>,
1315    #[serde(default)]
1316    preserved_shared_cache_references: Vec<PathBuf>,
1317}
1318
1319#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1320#[serde(deny_unknown_fields)]
1321struct ArtifactIdentity {
1322    kind: ManagedArtifactKind,
1323    #[serde(default)]
1324    device: u64,
1325    #[serde(default)]
1326    inode: u64,
1327    #[serde(default)]
1328    file_len: u64,
1329}
1330
1331#[derive(Debug, Clone, Serialize, Deserialize)]
1332#[serde(deny_unknown_fields)]
1333struct RemovalJournal {
1334    model_id: String,
1335    removal_generation: u64,
1336    receipt: InstallReceipt,
1337    quarantine_path: PathBuf,
1338    artifact_identity: ArtifactIdentity,
1339}
1340
1341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1342#[serde(deny_unknown_fields)]
1343struct InstallJournal {
1344    model_id: String,
1345    receipt: InstallReceipt,
1346    #[serde(default)]
1347    staging_path: Option<PathBuf>,
1348    #[serde(default)]
1349    staging_identity: Option<ArtifactIdentity>,
1350}
1351
1352fn hashed_filename(model_id: &str) -> String {
1353    let digest = Sha256::digest(model_id.as_bytes());
1354    format!("{}.json", hex::encode(digest))
1355}
1356
1357fn is_hashed_state_filename(name: &std::ffi::OsStr) -> bool {
1358    let Some(name) = name.to_str() else {
1359        return false;
1360    };
1361    let Some(hex) = name.strip_suffix(".json") else {
1362        return false;
1363    };
1364    hex.len() == 64
1365        && hex
1366            .bytes()
1367            .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
1368}
1369
1370fn validate_model_id(model_id: &str) -> Result<(), ModelManagementError> {
1371    if !model_id.is_empty() && model_id.len() <= MAX_MODEL_ID_BYTES {
1372        return Ok(());
1373    }
1374    Err(ModelManagementError::InvalidState {
1375        path: PathBuf::new(),
1376        message: "model id must contain 1..=4096 UTF-8 bytes".into(),
1377    })
1378}
1379
1380fn validate_managed_leaf(
1381    managed_leaf: &str,
1382    models_dir: &Path,
1383) -> Result<(), ModelManagementError> {
1384    if Path::new(managed_leaf).components().count() == 1
1385        && matches!(
1386            Path::new(managed_leaf).components().next(),
1387            Some(std::path::Component::Normal(_))
1388        )
1389    {
1390        return Ok(());
1391    }
1392    Err(ModelManagementError::UnsafeManagedPath {
1393        model_id: managed_leaf.to_string(),
1394        path: models_dir.join(managed_leaf),
1395        reason: "managed model name must be one ordinary path component".into(),
1396    })
1397}
1398
1399fn validate_direct_child(
1400    root: &Path,
1401    candidate: &Path,
1402    model_id: &str,
1403) -> Result<(), ModelManagementError> {
1404    let canonical_root = validate_models_root(root, model_id)?;
1405    let parent = candidate
1406        .parent()
1407        .ok_or_else(|| ModelManagementError::UnsafeManagedPath {
1408            model_id: model_id.to_string(),
1409            path: candidate.to_path_buf(),
1410            reason: "managed path has no parent".into(),
1411        })?;
1412    if canonical_directory(parent)? != canonical_root {
1413        return Err(ModelManagementError::UnsafeManagedPath {
1414            model_id: model_id.to_string(),
1415            path: candidate.to_path_buf(),
1416            reason: "managed path is not a direct child of the models root".into(),
1417        });
1418    }
1419    Ok(())
1420}
1421
1422fn validate_models_root(root: &Path, model_id: &str) -> Result<PathBuf, ModelManagementError> {
1423    let metadata = std::fs::symlink_metadata(root).map_err(|source| ModelManagementError::Io {
1424        path: root.to_path_buf(),
1425        source,
1426    })?;
1427    if metadata.file_type().is_symlink() {
1428        return Err(ModelManagementError::UnsafeManagedPath {
1429            model_id: model_id.to_string(),
1430            path: root.to_path_buf(),
1431            reason: "managed models root must not be a symlink".into(),
1432        });
1433    }
1434    #[cfg(windows)]
1435    {
1436        use std::os::windows::fs::MetadataExt;
1437        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
1438        if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1439            return Err(ModelManagementError::UnsafeManagedPath {
1440                model_id: model_id.to_string(),
1441                path: root.to_path_buf(),
1442                reason: "managed models root must not be a reparse point".into(),
1443            });
1444        }
1445    }
1446    if !metadata.is_dir() {
1447        return Err(ModelManagementError::UnsafeManagedPath {
1448            model_id: model_id.to_string(),
1449            path: root.to_path_buf(),
1450            reason: "managed models root is not a directory".into(),
1451        });
1452    }
1453    canonical_directory(root)
1454}
1455
1456fn ensure_identity(path: &Path, expected: &str, actual: &str) -> Result<(), ModelManagementError> {
1457    if expected == actual {
1458        return Ok(());
1459    }
1460    Err(ModelManagementError::IdentityMismatch {
1461        path: path.to_path_buf(),
1462        expected: expected.to_string(),
1463        actual: actual.to_string(),
1464    })
1465}
1466
1467fn validate_lock_identity(
1468    file: &mut File,
1469    path: &Path,
1470    model_id: &str,
1471) -> Result<(), ModelManagementError> {
1472    file.rewind().map_err(|source| ModelManagementError::Io {
1473        path: path.to_path_buf(),
1474        source,
1475    })?;
1476    let mut bytes = Vec::new();
1477    file.take(16 * 1024)
1478        .read_to_end(&mut bytes)
1479        .map_err(|source| ModelManagementError::Io {
1480            path: path.to_path_buf(),
1481            source,
1482        })?;
1483    let record: IdentityRecord =
1484        serde_json::from_slice(&bytes).map_err(|error| ModelManagementError::InvalidState {
1485            path: path.to_path_buf(),
1486            message: error.to_string(),
1487        })?;
1488    ensure_identity(path, model_id, &record.model_id)
1489}
1490
1491fn canonical_directory(path: &Path) -> Result<PathBuf, ModelManagementError> {
1492    path.canonicalize()
1493        .map_err(|source| ModelManagementError::Io {
1494            path: path.to_path_buf(),
1495            source,
1496        })
1497}
1498
1499fn shared_hugging_face_root() -> Option<PathBuf> {
1500    std::env::var_os("HF_HOME").map(PathBuf::from).or_else(|| {
1501        std::env::var_os("HOME")
1502            .or_else(|| std::env::var_os("USERPROFILE"))
1503            .map(|home| PathBuf::from(home).join(".cache/huggingface"))
1504    })
1505}
1506
1507fn artifact_identity(
1508    path: &Path,
1509    model_id: &str,
1510) -> Result<ArtifactIdentity, ModelManagementError> {
1511    let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
1512        path: path.to_path_buf(),
1513        source,
1514    })?;
1515    reject_windows_reparse(&metadata, path, model_id)?;
1516    let kind = if metadata.file_type().is_symlink() {
1517        ManagedArtifactKind::Symlink
1518    } else if metadata.is_dir() {
1519        ManagedArtifactKind::Directory
1520    } else if metadata.is_file() {
1521        ManagedArtifactKind::File
1522    } else {
1523        return Err(ModelManagementError::UnsafeManagedPath {
1524            model_id: model_id.to_string(),
1525            path: path.to_path_buf(),
1526            reason: "unsupported artifact type".into(),
1527        });
1528    };
1529    #[cfg(unix)]
1530    {
1531        use std::os::unix::fs::MetadataExt;
1532        Ok(ArtifactIdentity {
1533            kind,
1534            device: metadata.dev(),
1535            inode: metadata.ino(),
1536            file_len: metadata.len(),
1537        })
1538    }
1539    #[cfg(not(unix))]
1540    {
1541        Ok(ArtifactIdentity {
1542            kind,
1543            device: 0,
1544            inode: 0,
1545            file_len: metadata.len(),
1546        })
1547    }
1548}
1549
1550fn ensure_artifact_identity(
1551    path: &Path,
1552    model_id: &str,
1553    expected: &ArtifactIdentity,
1554) -> Result<(), ModelManagementError> {
1555    let actual = artifact_identity(path, model_id)?;
1556    if &actual == expected {
1557        return Ok(());
1558    }
1559    Err(ModelManagementError::UnsafeManagedPath {
1560        model_id: model_id.to_string(),
1561        path: path.to_path_buf(),
1562        reason: "managed artifact identity changed during removal".into(),
1563    })
1564}
1565
1566impl ArtifactIdentity {
1567    /// Removal-phase match. A directory's length changes as its entries are
1568    /// deleted, so a resumed directory removal compares kind, device, and inode
1569    /// only; files and symlinks keep the exact comparison. Install-intent
1570    /// recovery keeps using exact equality.
1571    fn matches_for_removal(&self, actual: &ArtifactIdentity) -> bool {
1572        self.kind == actual.kind
1573            && self.device == actual.device
1574            && self.inode == actual.inode
1575            && (self.kind == ManagedArtifactKind::Directory || self.file_len == actual.file_len)
1576    }
1577}
1578
1579fn ensure_removal_identity(
1580    path: &Path,
1581    model_id: &str,
1582    expected: &ArtifactIdentity,
1583) -> Result<(), ModelManagementError> {
1584    let actual = artifact_identity(path, model_id)?;
1585    if expected.matches_for_removal(&actual) {
1586        return Ok(());
1587    }
1588    Err(ModelManagementError::UnsafeManagedPath {
1589        model_id: model_id.to_string(),
1590        path: path.to_path_buf(),
1591        reason: "managed artifact identity changed during removal".into(),
1592    })
1593}
1594
1595fn artifact_kind(path: &Path, model_id: &str) -> Result<ManagedArtifactKind, ModelManagementError> {
1596    let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
1597        path: path.to_path_buf(),
1598        source,
1599    })?;
1600    reject_windows_reparse(&metadata, path, model_id)?;
1601    if metadata.file_type().is_symlink() {
1602        Ok(ManagedArtifactKind::Symlink)
1603    } else if metadata.is_dir() {
1604        Ok(ManagedArtifactKind::Directory)
1605    } else if metadata.is_file() {
1606        Ok(ManagedArtifactKind::File)
1607    } else {
1608        Err(ModelManagementError::UnsafeManagedPath {
1609            model_id: model_id.to_string(),
1610            path: path.to_path_buf(),
1611            reason: "unsupported artifact type".into(),
1612        })
1613    }
1614}
1615
1616fn collect_shared_references(
1617    path: &Path,
1618    model_id: &str,
1619) -> Result<Vec<PathBuf>, ModelManagementError> {
1620    let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
1621        path: path.to_path_buf(),
1622        source,
1623    })?;
1624    if metadata.file_type().is_symlink() {
1625        return path
1626            .canonicalize()
1627            .map(|target| vec![target])
1628            .map_err(|source| ModelManagementError::Io {
1629                path: path.to_path_buf(),
1630                source,
1631            });
1632    }
1633    if !metadata.is_dir() {
1634        return Ok(Vec::new());
1635    }
1636    let mut references = Vec::new();
1637    for entry in std::fs::read_dir(path).map_err(|source| ModelManagementError::Io {
1638        path: path.to_path_buf(),
1639        source,
1640    })? {
1641        let entry = entry.map_err(|source| ModelManagementError::Io {
1642            path: path.to_path_buf(),
1643            source,
1644        })?;
1645        let entry_path = entry.path();
1646        let entry_metadata =
1647            std::fs::symlink_metadata(&entry_path).map_err(|source| ModelManagementError::Io {
1648                path: entry_path.clone(),
1649                source,
1650            })?;
1651        if entry_metadata.file_type().is_symlink() {
1652            let target = entry_path
1653                .canonicalize()
1654                .map_err(|source| ModelManagementError::Io {
1655                    path: entry_path.clone(),
1656                    source,
1657                })?;
1658            if std::fs::metadata(&target)
1659                .map_err(|source| ModelManagementError::Io {
1660                    path: target.clone(),
1661                    source,
1662                })?
1663                .is_dir()
1664            {
1665                return Err(ModelManagementError::UnsafeManagedPath {
1666                    model_id: model_id.to_string(),
1667                    path: entry_path,
1668                    reason: "managed directory contains a directory symlink".into(),
1669                });
1670            }
1671            references.push(target);
1672        } else if entry_metadata.is_dir() {
1673            references.extend(collect_shared_references(&entry_path, model_id)?);
1674        }
1675    }
1676    references.sort();
1677    references.dedup();
1678    Ok(references)
1679}
1680
1681#[cfg(unix)]
1682fn create_symlink(target: &Path, link: &Path) -> Result<(), ModelManagementError> {
1683    std::os::unix::fs::symlink(target, link).map_err(|source| ModelManagementError::Io {
1684        path: link.to_path_buf(),
1685        source,
1686    })
1687}
1688
1689#[cfg(windows)]
1690fn create_symlink(target: &Path, link: &Path) -> Result<(), ModelManagementError> {
1691    let result = if target.is_dir() {
1692        std::os::windows::fs::symlink_dir(target, link)
1693    } else {
1694        std::os::windows::fs::symlink_file(target, link)
1695    };
1696    result.map_err(|source| ModelManagementError::Io {
1697        path: link.to_path_buf(),
1698        source,
1699    })
1700}
1701
1702fn create_private_dir(path: &Path) -> Result<(), ModelManagementError> {
1703    std::fs::create_dir_all(path).map_err(|source| ModelManagementError::Io {
1704        path: path.to_path_buf(),
1705        source,
1706    })?;
1707    harden_private_directory(path)?;
1708    Ok(())
1709}
1710
1711fn harden_private_directory(path: &Path) -> Result<(), ModelManagementError> {
1712    let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
1713        path: path.to_path_buf(),
1714        source,
1715    })?;
1716    if metadata.file_type().is_symlink() {
1717        return Err(ModelManagementError::UnsafeManagedPath {
1718            model_id: "model-management-state".into(),
1719            path: path.to_path_buf(),
1720            reason: "private model-management directory must not be a symlink".into(),
1721        });
1722    }
1723    reject_windows_reparse(&metadata, path, "model-management-state")?;
1724    #[cfg(unix)]
1725    {
1726        use std::os::unix::fs::PermissionsExt;
1727        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).map_err(
1728            |source| ModelManagementError::Io {
1729                path: path.to_path_buf(),
1730                source,
1731            },
1732        )?;
1733    }
1734    Ok(())
1735}
1736
1737fn reject_windows_reparse(
1738    metadata: &std::fs::Metadata,
1739    path: &Path,
1740    model_id: &str,
1741) -> Result<(), ModelManagementError> {
1742    #[cfg(windows)]
1743    {
1744        use std::os::windows::fs::MetadataExt;
1745        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
1746        if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1747            return Err(ModelManagementError::UnsafeManagedPath {
1748                model_id: model_id.to_string(),
1749                path: path.to_path_buf(),
1750                reason: "model-management path is a Windows reparse point".into(),
1751            });
1752        }
1753    }
1754    #[cfg(not(windows))]
1755    let _ = (metadata, path, model_id);
1756    Ok(())
1757}
1758
1759fn open_private_new(path: &Path) -> Result<File, ModelManagementError> {
1760    let mut options = OpenOptions::new();
1761    options.read(true).write(true).create_new(true);
1762    #[cfg(unix)]
1763    {
1764        use std::os::unix::fs::OpenOptionsExt;
1765        options.mode(0o600);
1766    }
1767    options
1768        .open(path)
1769        .map_err(|source| ModelManagementError::Io {
1770            path: path.to_path_buf(),
1771            source,
1772        })
1773}
1774
1775fn open_existing_identity_lock(path: &Path) -> Result<File, ModelManagementError> {
1776    let metadata = std::fs::symlink_metadata(path).map_err(|source| ModelManagementError::Io {
1777        path: path.to_path_buf(),
1778        source,
1779    })?;
1780    if metadata.file_type().is_symlink() {
1781        return Err(ModelManagementError::InvalidState {
1782            path: path.to_path_buf(),
1783            message: "model-management lock sentinel must not be a symlink".into(),
1784        });
1785    }
1786    reject_windows_reparse(&metadata, path, "model-management-lock")?;
1787    #[cfg(unix)]
1788    {
1789        use std::os::unix::fs::MetadataExt;
1790        if metadata.nlink() != 1 {
1791            return Err(ModelManagementError::InvalidState {
1792                path: path.to_path_buf(),
1793                message: "model-management lock sentinel must not be hard-linked".into(),
1794            });
1795        }
1796    }
1797    let mut options = OpenOptions::new();
1798    options.read(true).write(true);
1799    #[cfg(unix)]
1800    {
1801        use std::os::unix::fs::OpenOptionsExt;
1802        options.custom_flags(libc::O_NOFOLLOW);
1803    }
1804    #[cfg(windows)]
1805    {
1806        use std::os::windows::fs::OpenOptionsExt;
1807        const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
1808        options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
1809    }
1810    let file = options
1811        .open(path)
1812        .map_err(|source| ModelManagementError::Io {
1813            path: path.to_path_buf(),
1814            source,
1815        })?;
1816    let opened_metadata = file.metadata().map_err(|source| ModelManagementError::Io {
1817        path: path.to_path_buf(),
1818        source,
1819    })?;
1820    if !opened_metadata.is_file() {
1821        return Err(ModelManagementError::InvalidState {
1822            path: path.to_path_buf(),
1823            message: "model-management lock sentinel is not a regular file".into(),
1824        });
1825    }
1826    reject_windows_reparse(&opened_metadata, path, "model-management-lock")?;
1827    #[cfg(unix)]
1828    {
1829        use std::os::unix::fs::MetadataExt;
1830        if opened_metadata.nlink() != 1 {
1831            return Err(ModelManagementError::InvalidState {
1832                path: path.to_path_buf(),
1833                message: "opened model-management lock sentinel is hard-linked".into(),
1834            });
1835        }
1836    }
1837    Ok(file)
1838}
1839
1840fn read_private_state(path: &Path) -> std::io::Result<Vec<u8>> {
1841    let mut options = OpenOptions::new();
1842    options.read(true);
1843    #[cfg(unix)]
1844    {
1845        use std::os::unix::fs::OpenOptionsExt;
1846        options.custom_flags(libc::O_NOFOLLOW);
1847    }
1848    let file = options.open(path)?;
1849    let metadata = file.metadata()?;
1850    if !metadata.is_file() || metadata.len() > MAX_STATE_BYTES {
1851        return Err(std::io::Error::new(
1852            std::io::ErrorKind::InvalidData,
1853            "model-management state must be a bounded regular file",
1854        ));
1855    }
1856    #[cfg(unix)]
1857    {
1858        use std::os::unix::fs::MetadataExt;
1859        if metadata.nlink() != 1 {
1860            return Err(std::io::Error::new(
1861                std::io::ErrorKind::InvalidData,
1862                "model-management state must not be hard-linked",
1863            ));
1864        }
1865    }
1866    let mut bytes = Vec::with_capacity(metadata.len() as usize);
1867    file.take(MAX_STATE_BYTES + 1).read_to_end(&mut bytes)?;
1868    if bytes.len() as u64 > MAX_STATE_BYTES {
1869        return Err(std::io::Error::new(
1870            std::io::ErrorKind::InvalidData,
1871            "model-management state exceeds the size limit",
1872        ));
1873    }
1874    Ok(bytes)
1875}
1876
1877fn write_private_json<T: Serialize>(path: &Path, value: &T) -> Result<(), ModelManagementError> {
1878    let _guard = state_mutation_lock()
1879        .lock()
1880        .unwrap_or_else(std::sync::PoisonError::into_inner);
1881    let parent = path
1882        .parent()
1883        .expect("model-management record path has a parent");
1884    create_private_dir(parent)?;
1885    let tmp = (0..16)
1886        .map(|_| {
1887            parent.join(format!(
1888                ".{}.{}-{:032x}.tmp",
1889                std::process::id(),
1890                path.file_name()
1891                    .and_then(|name| name.to_str())
1892                    .unwrap_or("state"),
1893                rand::random::<u128>()
1894            ))
1895        })
1896        .find(|candidate| {
1897            std::fs::symlink_metadata(candidate)
1898                .is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound)
1899        })
1900        .ok_or_else(|| ModelManagementError::InvalidState {
1901            path: parent.to_path_buf(),
1902            message: "could not allocate collision-resistant state staging file".into(),
1903        })?;
1904    let bytes =
1905        serde_json::to_vec_pretty(value).map_err(|error| ModelManagementError::InvalidState {
1906            path: path.to_path_buf(),
1907            message: error.to_string(),
1908        })?;
1909    let result = (|| {
1910        let mut file = open_private_new(&tmp)?;
1911        file.write_all(&bytes)
1912            .and_then(|_| file.sync_all())
1913            .map_err(|source| ModelManagementError::Io {
1914                path: tmp.clone(),
1915                source,
1916            })?;
1917        atomic_replace(&tmp, path).map_err(|source| ModelManagementError::Io {
1918            path: path.to_path_buf(),
1919            source,
1920        })?;
1921        car_secrets::harden_owner_only(path);
1922        sync_directory(parent).map_err(|source| ModelManagementError::Io {
1923            path: parent.to_path_buf(),
1924            source,
1925        })
1926    })();
1927    if result.is_err() {
1928        let _ = std::fs::remove_file(&tmp);
1929    }
1930    result
1931}
1932
1933fn state_mutation_lock() -> &'static Mutex<()> {
1934    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
1935    LOCK.get_or_init(|| Mutex::new(()))
1936}
1937
1938#[cfg(not(windows))]
1939fn atomic_replace(source: &Path, destination: &Path) -> std::io::Result<()> {
1940    std::fs::rename(source, destination)
1941}
1942
1943#[cfg(target_os = "macos")]
1944fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
1945    use std::ffi::CString;
1946    use std::os::unix::ffi::OsStrExt;
1947
1948    let source = CString::new(source.as_os_str().as_bytes())
1949        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
1950    let destination = CString::new(destination.as_os_str().as_bytes())
1951        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
1952    // SAFETY: both C strings are NUL-terminated and live for the call.
1953    let result =
1954        unsafe { libc::renamex_np(source.as_ptr(), destination.as_ptr(), libc::RENAME_EXCL) };
1955    if result == 0 {
1956        Ok(())
1957    } else {
1958        Err(std::io::Error::last_os_error())
1959    }
1960}
1961
1962#[cfg(target_os = "linux")]
1963fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
1964    use std::ffi::CString;
1965    use std::os::unix::ffi::OsStrExt;
1966
1967    let source = CString::new(source.as_os_str().as_bytes())
1968        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
1969    let destination = CString::new(destination.as_os_str().as_bytes())
1970        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
1971    // SAFETY: both C strings are NUL-terminated and live for the call.
1972    let result = unsafe {
1973        libc::renameat2(
1974            libc::AT_FDCWD,
1975            source.as_ptr(),
1976            libc::AT_FDCWD,
1977            destination.as_ptr(),
1978            libc::RENAME_NOREPLACE,
1979        )
1980    };
1981    if result == 0 {
1982        Ok(())
1983    } else {
1984        Err(std::io::Error::last_os_error())
1985    }
1986}
1987
1988#[cfg(windows)]
1989fn atomic_rename_noreplace(source: &Path, destination: &Path) -> std::io::Result<()> {
1990    use std::os::windows::ffi::OsStrExt;
1991
1992    const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
1993    #[link(name = "kernel32")]
1994    unsafe extern "system" {
1995        fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32;
1996    }
1997    let source = source
1998        .as_os_str()
1999        .encode_wide()
2000        .chain(Some(0))
2001        .collect::<Vec<_>>();
2002    let destination = destination
2003        .as_os_str()
2004        .encode_wide()
2005        .chain(Some(0))
2006        .collect::<Vec<_>>();
2007    // No REPLACE_EXISTING flag: a pre-existing destination fails closed.
2008    let moved = unsafe {
2009        MoveFileExW(
2010            source.as_ptr(),
2011            destination.as_ptr(),
2012            MOVEFILE_WRITE_THROUGH,
2013        )
2014    };
2015    if moved == 0 {
2016        Err(std::io::Error::last_os_error())
2017    } else {
2018        Ok(())
2019    }
2020}
2021
2022#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
2023fn atomic_rename_noreplace(_source: &Path, _destination: &Path) -> std::io::Result<()> {
2024    Err(std::io::Error::new(
2025        std::io::ErrorKind::Unsupported,
2026        "atomic no-replace publication is unavailable on this platform",
2027    ))
2028}
2029
2030#[cfg(windows)]
2031fn atomic_replace(source: &Path, destination: &Path) -> std::io::Result<()> {
2032    use std::os::windows::ffi::OsStrExt;
2033
2034    const MOVEFILE_REPLACE_EXISTING: u32 = 0x1;
2035    const MOVEFILE_WRITE_THROUGH: u32 = 0x8;
2036    #[link(name = "kernel32")]
2037    unsafe extern "system" {
2038        fn MoveFileExW(existing: *const u16, new: *const u16, flags: u32) -> i32;
2039    }
2040    let source = source
2041        .as_os_str()
2042        .encode_wide()
2043        .chain(Some(0))
2044        .collect::<Vec<_>>();
2045    let destination = destination
2046        .as_os_str()
2047        .encode_wide()
2048        .chain(Some(0))
2049        .collect::<Vec<_>>();
2050    // SAFETY: both pointers remain valid NUL-terminated UTF-16 buffers for
2051    // the duration of the call.
2052    let replaced = unsafe {
2053        MoveFileExW(
2054            source.as_ptr(),
2055            destination.as_ptr(),
2056            MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
2057        )
2058    };
2059    if replaced == 0 {
2060        Err(std::io::Error::last_os_error())
2061    } else {
2062        Ok(())
2063    }
2064}
2065
2066#[cfg(unix)]
2067fn sync_directory(path: &Path) -> std::io::Result<()> {
2068    File::open(path)?.sync_all()
2069}
2070
2071/// Object-bound deletion of a quarantined managed directory (macOS/Linux).
2072///
2073/// Everything after the first `open` is relative to descriptors CAR itself
2074/// opened: children are opened before they are trusted, symlinks are unlinked
2075/// and never followed, a change of device (and on Linux any mount crossing)
2076/// refuses the walk, entries are captured first and each is rechecked against
2077/// its captured identity immediately before its unlink, and a directory is
2078/// removed only when it is empty and still the object that was captured. POSIX
2079/// has no unlink-by-descriptor, so the one residual is the recheck → `unlinkat`
2080/// window on a captured name, the same window `std::fs::remove_dir_all` and
2081/// `rm -rf` accept; a name that appears after capture is never opened or
2082/// unlinked and makes its parent's `rmdir` fail closed instead.
2083#[cfg(any(target_os = "macos", target_os = "linux"))]
2084mod object_bound {
2085    use std::ffi::{CStr, CString};
2086    use std::fs::File;
2087    use std::os::fd::{AsRawFd, FromRawFd};
2088    use std::os::unix::ffi::OsStrExt;
2089    use std::os::unix::fs::MetadataExt;
2090    use std::path::Path;
2091
2092    pub(super) const MAX_REMOVAL_DEPTH: usize = 32;
2093
2094    const DIRECTORY_FLAGS: libc::c_int =
2095        libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW;
2096
2097    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2098    pub(super) struct Identity {
2099        pub(super) device: u64,
2100        pub(super) inode: u64,
2101    }
2102
2103    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2104    pub(super) enum EntryKind {
2105        Directory,
2106        RegularFile,
2107        Symlink,
2108        Other,
2109    }
2110
2111    pub(super) fn c_name(path: &Path) -> std::io::Result<CString> {
2112        CString::new(path.as_os_str().as_bytes())
2113            .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))
2114    }
2115
2116    /// Open a directory by path once, refusing to follow a symlink at the leaf.
2117    pub(super) fn open_directory(path: &Path) -> std::io::Result<File> {
2118        let name = c_name(path)?;
2119        // SAFETY: `name` is NUL-terminated and lives for the call.
2120        let raw = unsafe { libc::open(name.as_ptr(), DIRECTORY_FLAGS) };
2121        if raw < 0 {
2122            return Err(std::io::Error::last_os_error());
2123        }
2124        // SAFETY: `raw` is a newly-owned descriptor returned by `open`.
2125        Ok(unsafe { File::from_raw_fd(raw) })
2126    }
2127
2128    /// Open `name` relative to `parent` as a directory. Fails with `ENOTDIR`
2129    /// for a regular file and `ELOOP` for a symlink, which the walker treats
2130    /// as "not a directory".
2131    #[cfg(target_os = "macos")]
2132    pub(super) fn open_child_directory(parent: &File, name: &CStr) -> std::io::Result<File> {
2133        // SAFETY: the parent descriptor is open and `name` is NUL-terminated.
2134        let raw = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), DIRECTORY_FLAGS) };
2135        if raw < 0 {
2136            return Err(std::io::Error::last_os_error());
2137        }
2138        // SAFETY: `raw` is a newly-owned descriptor returned by `openat`.
2139        Ok(unsafe { File::from_raw_fd(raw) })
2140    }
2141
2142    /// Linux: `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS |
2143    /// RESOLVE_NO_XDEV` so the open can neither escape the parent, follow a
2144    /// symlink, nor cross a mount (bind mounts included, which share `st_dev`).
2145    #[cfg(target_os = "linux")]
2146    pub(super) fn open_child_directory(parent: &File, name: &CStr) -> std::io::Result<File> {
2147        // SAFETY: `open_how` is plain data; zero is a valid initial state.
2148        let mut how: libc::open_how = unsafe { std::mem::zeroed() };
2149        how.flags = DIRECTORY_FLAGS as u64;
2150        how.resolve = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_SYMLINKS | libc::RESOLVE_NO_XDEV;
2151        // SAFETY: the parent descriptor is open, `name` is NUL-terminated, and
2152        // `how` is a valid `open_how` of the size passed.
2153        let raw = unsafe {
2154            libc::syscall(
2155                libc::SYS_openat2,
2156                parent.as_raw_fd(),
2157                name.as_ptr(),
2158                &how as *const libc::open_how,
2159                std::mem::size_of::<libc::open_how>(),
2160            )
2161        };
2162        if raw < 0 {
2163            return Err(std::io::Error::last_os_error());
2164        }
2165        // SAFETY: `raw` is a newly-owned descriptor returned by `openat2`.
2166        Ok(unsafe { File::from_raw_fd(raw as libc::c_int) })
2167    }
2168
2169    /// A child that reports a different device than the root is on another
2170    /// filesystem; the walk never descends across one. Linux additionally
2171    /// refuses bind mounts through `RESOLVE_NO_XDEV`, which share `st_dev`.
2172    pub(super) fn same_filesystem(root: &Identity, child: &Identity) -> bool {
2173        root.device == child.device
2174    }
2175
2176    pub(super) fn is_not_a_directory(error: &std::io::Error) -> bool {
2177        matches!(
2178            error.raw_os_error(),
2179            Some(libc::ENOTDIR) | Some(libc::ELOOP)
2180        )
2181    }
2182
2183    pub(super) fn identity_of(file: &File) -> std::io::Result<Identity> {
2184        let metadata = file.metadata()?;
2185        Ok(Identity {
2186            device: metadata.dev(),
2187            inode: metadata.ino(),
2188        })
2189    }
2190
2191    /// `fstatat(AT_SYMLINK_NOFOLLOW)` of `name` relative to `parent`.
2192    // `st_dev` / `st_ino` widths differ per target (macOS `dev_t` is `i32`,
2193    // Linux `dev_t` is `u64`), so the casts are needed on some targets and
2194    // redundant on others.
2195    #[allow(clippy::unnecessary_cast)]
2196    pub(super) fn stat_entry(parent: &File, name: &CStr) -> std::io::Result<(EntryKind, Identity)> {
2197        // SAFETY: `stat` is plain data; zero is a valid initial state.
2198        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2199        // SAFETY: the parent descriptor is open, `name` is NUL-terminated, and
2200        // `stat` is a valid out-pointer.
2201        let result = unsafe {
2202            libc::fstatat(
2203                parent.as_raw_fd(),
2204                name.as_ptr(),
2205                &mut stat,
2206                libc::AT_SYMLINK_NOFOLLOW,
2207            )
2208        };
2209        if result != 0 {
2210            return Err(std::io::Error::last_os_error());
2211        }
2212        let kind = match stat.st_mode & libc::S_IFMT {
2213            libc::S_IFDIR => EntryKind::Directory,
2214            libc::S_IFREG => EntryKind::RegularFile,
2215            libc::S_IFLNK => EntryKind::Symlink,
2216            _ => EntryKind::Other,
2217        };
2218        Ok((
2219            kind,
2220            Identity {
2221                device: stat.st_dev as u64,
2222                inode: stat.st_ino as u64,
2223            },
2224        ))
2225    }
2226
2227    /// Enumerate `directory` through a duplicate descriptor so the retained
2228    /// one keeps its position; names are sorted for deterministic order.
2229    pub(super) fn list_names(directory: &File) -> std::io::Result<Vec<CString>> {
2230        let dot = c".";
2231        // SAFETY: the directory descriptor is open and `dot` is NUL-terminated.
2232        let raw = unsafe { libc::openat(directory.as_raw_fd(), dot.as_ptr(), DIRECTORY_FLAGS) };
2233        if raw < 0 {
2234            return Err(std::io::Error::last_os_error());
2235        }
2236        // SAFETY: `raw` is a newly-owned descriptor; `fdopendir` takes ownership.
2237        let stream = unsafe { libc::fdopendir(raw) };
2238        if stream.is_null() {
2239            let error = std::io::Error::last_os_error();
2240            // SAFETY: `fdopendir` failed, so the descriptor is still ours to close.
2241            unsafe {
2242                libc::close(raw);
2243            }
2244            return Err(error);
2245        }
2246        let mut names = Vec::new();
2247        let enumeration = loop {
2248            set_errno(0);
2249            // SAFETY: `stream` is a valid open directory stream.
2250            let entry = unsafe { libc::readdir(stream) };
2251            if entry.is_null() {
2252                let errno = errno();
2253                break if errno == 0 {
2254                    Ok(())
2255                } else {
2256                    Err(std::io::Error::from_raw_os_error(errno))
2257                };
2258            }
2259            // SAFETY: `entry` points at a valid dirent whose name is NUL-terminated.
2260            let bytes = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
2261            if bytes != b"." && bytes != b".." {
2262                match CString::new(bytes) {
2263                    Ok(name) => names.push(name),
2264                    Err(error) => {
2265                        break Err(std::io::Error::new(std::io::ErrorKind::InvalidData, error))
2266                    }
2267                }
2268            }
2269        };
2270        // SAFETY: `stream` is a valid open directory stream owned by us.
2271        let closed = unsafe { libc::closedir(stream) };
2272        enumeration?;
2273        if closed < 0 {
2274            return Err(std::io::Error::last_os_error());
2275        }
2276        names.sort();
2277        Ok(names)
2278    }
2279
2280    /// `unlinkat(parent, name, 0)`; a missing entry counts as already removed.
2281    pub(super) fn unlink_entry(parent: &File, name: &CStr) -> std::io::Result<()> {
2282        // SAFETY: the parent descriptor is open and `name` is NUL-terminated.
2283        let result = unsafe { libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), 0) };
2284        if result == 0 {
2285            return Ok(());
2286        }
2287        let error = std::io::Error::last_os_error();
2288        if error.kind() == std::io::ErrorKind::NotFound {
2289            return Ok(());
2290        }
2291        Err(error)
2292    }
2293
2294    /// `unlinkat(parent, name, AT_REMOVEDIR)`: only ever removes an empty
2295    /// directory. A missing entry counts as already removed.
2296    pub(super) fn remove_empty_directory(parent: &File, name: &CStr) -> std::io::Result<()> {
2297        // SAFETY: the parent descriptor is open and `name` is NUL-terminated.
2298        let result =
2299            unsafe { libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR) };
2300        if result == 0 {
2301            return Ok(());
2302        }
2303        let error = std::io::Error::last_os_error();
2304        if error.kind() == std::io::ErrorKind::NotFound {
2305            return Ok(());
2306        }
2307        Err(error)
2308    }
2309
2310    pub(super) fn is_not_empty(error: &std::io::Error) -> bool {
2311        matches!(
2312            error.raw_os_error(),
2313            Some(libc::ENOTEMPTY) | Some(libc::EEXIST)
2314        )
2315    }
2316
2317    #[cfg(target_os = "macos")]
2318    fn errno() -> i32 {
2319        // SAFETY: `__error` returns the thread-local errno pointer.
2320        unsafe { *libc::__error() }
2321    }
2322
2323    #[cfg(target_os = "macos")]
2324    fn set_errno(value: i32) {
2325        // SAFETY: `__error` returns the thread-local errno pointer.
2326        unsafe {
2327            *libc::__error() = value;
2328        }
2329    }
2330
2331    #[cfg(target_os = "linux")]
2332    fn errno() -> i32 {
2333        // SAFETY: `__errno_location` returns the thread-local errno pointer.
2334        unsafe { *libc::__errno_location() }
2335    }
2336
2337    #[cfg(target_os = "linux")]
2338    fn set_errno(value: i32) {
2339        // SAFETY: `__errno_location` returns the thread-local errno pointer.
2340        unsafe {
2341            *libc::__errno_location() = value;
2342        }
2343    }
2344}
2345
2346#[cfg(any(target_os = "macos", target_os = "linux"))]
2347impl ModelManagementStore {
2348    /// Delete the quarantined directory `quarantine` whose pre-rename identity
2349    /// is `expected`. See `object_bound` for the guarantees.
2350    fn remove_quarantined_directory(
2351        &self,
2352        quarantine: &Path,
2353        expected: &ArtifactIdentity,
2354        model_id: &str,
2355    ) -> Result<(), ModelManagementError> {
2356        use object_bound::{EntryKind, Identity, MAX_REMOVAL_DEPTH};
2357        use std::ffi::CString;
2358        use std::fs::File;
2359
2360        struct CapturedDirectory {
2361            file: File,
2362            name: CString,
2363            parent: usize,
2364            depth: usize,
2365            identity: Identity,
2366        }
2367        struct CapturedEntry {
2368            parent: usize,
2369            name: CString,
2370            identity: Identity,
2371        }
2372
2373        let unsafe_path = |reason: &str| ModelManagementError::UnsafeManagedPath {
2374            model_id: model_id.to_string(),
2375            path: quarantine.to_path_buf(),
2376            reason: reason.into(),
2377        };
2378        let io_error = |source: std::io::Error| ModelManagementError::Io {
2379            path: quarantine.to_path_buf(),
2380            source,
2381        };
2382
2383        #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
2384        if let Some(hook) = &self.removal_hook {
2385            (hook.0)(RemovalPhase::BeforeRootOpen, quarantine);
2386        }
2387
2388        // Bind the root: open it once, then trust only the descriptor. The
2389        // caller observed the quarantine moments ago, so its absence here means
2390        // something moved it; that is never a completed removal.
2391        let root = match object_bound::open_directory(quarantine) {
2392            Ok(root) => root,
2393            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2394                return Err(unsafe_path(QUARANTINE_VANISHED_REASON));
2395            }
2396            Err(source) => return Err(io_error(source)),
2397        };
2398        let root_identity = object_bound::identity_of(&root).map_err(io_error)?;
2399        if root_identity.device != expected.device || root_identity.inode != expected.inode {
2400            return Err(unsafe_path(
2401                "quarantined directory identity changed before deletion",
2402            ));
2403        }
2404
2405        // Capture phase: open every child by descriptor before trusting it.
2406        let mut directories = vec![CapturedDirectory {
2407            file: root,
2408            name: CString::default(),
2409            parent: 0,
2410            depth: 0,
2411            identity: root_identity,
2412        }];
2413        let mut entries: Vec<CapturedEntry> = Vec::new();
2414        let mut pending = vec![0usize];
2415        while let Some(index) = pending.pop() {
2416            let depth = directories[index].depth;
2417            if depth > MAX_REMOVAL_DEPTH {
2418                return Err(unsafe_path(
2419                    "managed directory nesting exceeds the removal depth cap",
2420                ));
2421            }
2422            let names = object_bound::list_names(&directories[index].file).map_err(io_error)?;
2423            for name in names {
2424                match object_bound::open_child_directory(&directories[index].file, &name) {
2425                    Ok(child) => {
2426                        let identity = object_bound::identity_of(&child).map_err(io_error)?;
2427                        if !object_bound::same_filesystem(&root_identity, &identity) {
2428                            return Err(unsafe_path(
2429                                "managed directory crosses a filesystem boundary",
2430                            ));
2431                        }
2432                        directories.push(CapturedDirectory {
2433                            file: child,
2434                            name,
2435                            parent: index,
2436                            depth: depth + 1,
2437                            identity,
2438                        });
2439                        pending.push(directories.len() - 1);
2440                    }
2441                    Err(error) if object_bound::is_not_a_directory(&error) => {
2442                        let (kind, identity) =
2443                            object_bound::stat_entry(&directories[index].file, &name)
2444                                .map_err(io_error)?;
2445                        match kind {
2446                            EntryKind::RegularFile | EntryKind::Symlink => {
2447                                entries.push(CapturedEntry {
2448                                    parent: index,
2449                                    name,
2450                                    identity,
2451                                });
2452                            }
2453                            EntryKind::Directory | EntryKind::Other => {
2454                                return Err(unsafe_path(
2455                                    "managed directory contains an entry CAR cannot classify",
2456                                ));
2457                            }
2458                        }
2459                    }
2460                    Err(error) if error.raw_os_error() == Some(libc::EXDEV) => {
2461                        return Err(unsafe_path(
2462                            "managed directory crosses a filesystem boundary",
2463                        ));
2464                    }
2465                    Err(error)
2466                        if matches!(
2467                            error.raw_os_error(),
2468                            Some(libc::ENOSYS) | Some(libc::EINVAL) | Some(libc::E2BIG)
2469                        ) =>
2470                    {
2471                        return Err(unsafe_path(
2472                            "descriptor-bound directory open is unavailable on this kernel",
2473                        ));
2474                    }
2475                    Err(source) => return Err(io_error(source)),
2476                }
2477            }
2478        }
2479
2480        #[cfg(all(test, any(target_os = "macos", target_os = "linux")))]
2481        if let Some(hook) = &self.removal_hook {
2482            (hook.0)(RemovalPhase::AfterCapture, quarantine);
2483        }
2484
2485        // Delete phase: only captured entries, each rechecked against its
2486        // captured identity immediately before the unlink.
2487        for entry in &entries {
2488            let parent = &directories[entry.parent].file;
2489            match object_bound::stat_entry(parent, &entry.name) {
2490                Ok((_, identity)) if identity == entry.identity => {
2491                    object_bound::unlink_entry(parent, &entry.name).map_err(io_error)?;
2492                }
2493                Ok(_) => {
2494                    return Err(unsafe_path(
2495                        "managed directory entry identity changed during deletion",
2496                    ));
2497                }
2498                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2499                Err(source) => return Err(io_error(source)),
2500            }
2501        }
2502
2503        // Directories deepest first: every child index is greater than its
2504        // parent's, so popping from the end empties children before parents.
2505        while directories.len() > 1 {
2506            let CapturedDirectory {
2507                file,
2508                name,
2509                parent,
2510                identity,
2511                ..
2512            } = directories.pop().expect("non-empty");
2513            drop(file);
2514            let parent = &directories[parent].file;
2515            match object_bound::stat_entry(parent, &name) {
2516                Ok((EntryKind::Directory, actual)) if actual == identity => {
2517                    object_bound::remove_empty_directory(parent, &name).map_err(|error| {
2518                        if object_bound::is_not_empty(&error) {
2519                            unsafe_path("managed directory contains entries that were not captured")
2520                        } else {
2521                            io_error(error)
2522                        }
2523                    })?;
2524                }
2525                Ok(_) => {
2526                    return Err(unsafe_path(
2527                        "managed directory entry identity changed during deletion",
2528                    ));
2529                }
2530                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
2531                Err(source) => return Err(io_error(source)),
2532            }
2533        }
2534
2535        // The root itself: relative to the validated models root, and only
2536        // if it is still the captured object and now empty.
2537        let CapturedDirectory { file: root, .. } = directories.pop().expect("root");
2538        drop(root);
2539        let models = object_bound::open_directory(&self.models_dir).map_err(|source| {
2540            ModelManagementError::Io {
2541                path: self.models_dir.clone(),
2542                source,
2543            }
2544        })?;
2545        let leaf = quarantine
2546            .file_name()
2547            .map(Path::new)
2548            .ok_or_else(|| unsafe_path("quarantine path has no file name"))?;
2549        let leaf = object_bound::c_name(leaf).map_err(io_error)?;
2550        match object_bound::stat_entry(&models, &leaf) {
2551            Ok((EntryKind::Directory, actual)) if actual == root_identity => {
2552                object_bound::remove_empty_directory(&models, &leaf).map_err(|error| {
2553                    if object_bound::is_not_empty(&error) {
2554                        unsafe_path("managed directory contains entries that were not captured")
2555                    } else {
2556                        io_error(error)
2557                    }
2558                })
2559            }
2560            Ok(_) => Err(unsafe_path(
2561                "quarantined directory identity changed during deletion",
2562            )),
2563            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
2564            Err(source) => Err(io_error(source)),
2565        }
2566    }
2567}
2568
2569// Keep both cfg halves of `sync_directory` here, ABOVE `mod tests`. The
2570// non-unix half used to sit at the end of the file, where it was invisible on
2571// Unix (it compiles out) but tripped `clippy::items_after_test_module` on the
2572// Windows leg of `-D warnings`.
2573#[cfg(not(unix))]
2574fn sync_directory(_path: &Path) -> std::io::Result<()> {
2575    Ok(())
2576}
2577
2578#[cfg(test)]
2579mod tests {
2580    use super::*;
2581
2582    // Invoked only by the parent process fixture below, never by a model backend.
2583    #[test]
2584    fn identity_initialization_child() {
2585        use std::io::{BufRead, Write};
2586        let Some(root) = std::env::var_os("CAR_IDENTITY_TEST_ROOT") else {
2587            return;
2588        };
2589        let root = PathBuf::from(root);
2590        let mut store = ModelManagementStore::new(root.join("state"), root.join("models"));
2591        if std::env::var("CAR_IDENTITY_TEST_ROLE").as_deref() == Ok("creator") {
2592            store.identity_init_hook = Some(|path| {
2593                let phase = std::env::var("CAR_IDENTITY_TEST_PHASE").unwrap();
2594                if path.parent().unwrap().file_name().unwrap() == phase.as_str() {
2595                    println!("IDENTITY_CREATED");
2596                    std::io::stdout().flush().unwrap();
2597                    let mut release = String::new();
2598                    std::io::stdin().lock().read_line(&mut release).unwrap();
2599                    assert_eq!(release.trim(), "release");
2600                }
2601            });
2602        } else {
2603            println!("READER_STARTED");
2604            std::io::stdout().flush().unwrap();
2605        }
2606        let _lease = store.acquire_lease("fixture/model").unwrap();
2607    }
2608
2609    #[test]
2610    fn identity_initialization_serializes_processes_before_json_is_written() {
2611        use std::io::{BufRead, BufReader, Write};
2612        use std::process::{Command, Stdio};
2613        fn wait_marker(reader: &mut impl BufRead, marker: &str) {
2614            let mut line = String::new();
2615            loop {
2616                line.clear();
2617                assert_ne!(
2618                    reader.read_line(&mut line).unwrap(),
2619                    0,
2620                    "child exited before {marker}"
2621                );
2622                if line.contains(marker) {
2623                    return;
2624                }
2625            }
2626        }
2627        for phase in ["mutation", "activity"] {
2628            let root = tempfile::tempdir().unwrap();
2629            let spawn = |role: &str| {
2630                Command::new(std::env::current_exe().unwrap())
2631                    .args([
2632                        "--exact",
2633                        "model_management::tests::identity_initialization_child",
2634                        "--nocapture",
2635                    ])
2636                    .env("CAR_IDENTITY_TEST_ROOT", root.path())
2637                    .env("CAR_IDENTITY_TEST_ROLE", role)
2638                    .env("CAR_IDENTITY_TEST_PHASE", phase)
2639                    .stdin(Stdio::piped())
2640                    .stdout(Stdio::piped())
2641                    .stderr(Stdio::inherit())
2642                    .spawn()
2643                    .unwrap()
2644            };
2645            let mut creator = spawn("creator");
2646            let mut creator_out = BufReader::new(creator.stdout.take().unwrap());
2647            wait_marker(&mut creator_out, "IDENTITY_CREATED");
2648            let store =
2649                ModelManagementStore::new(root.path().join("state"), root.path().join("models"));
2650            let path = if phase == "mutation" {
2651                store.mutation_lock_path("fixture/model")
2652            } else {
2653                store.lease_path("fixture/model")
2654            };
2655            assert!(
2656                std::fs::read(&path).unwrap().is_empty(),
2657                "probe must hit the original partial-publication window"
2658            );
2659            let gate =
2660                open_existing_identity_lock(&path.parent().unwrap().join(".initialization.lock"))
2661                    .unwrap();
2662            assert!(
2663                gate.try_lock().is_err(),
2664                "creator must hold the initialization gate before publishing an empty identity"
2665            );
2666            let mut contender = spawn("reader");
2667            let mut contender_out = BufReader::new(contender.stdout.take().unwrap());
2668            wait_marker(&mut contender_out, "READER_STARTED");
2669            assert!(contender.try_wait().unwrap().is_none());
2670            writeln!(creator.stdin.take().unwrap(), "release").unwrap();
2671            assert!(creator.wait().unwrap().success());
2672            assert!(contender.wait().unwrap().success());
2673            let before = std::fs::metadata(&path).unwrap();
2674            drop(store.acquire_lease("fixture/model").unwrap());
2675            #[cfg(unix)]
2676            {
2677                use std::os::unix::fs::MetadataExt;
2678                assert_eq!(before.ino(), std::fs::metadata(&path).unwrap().ino());
2679            }
2680            #[cfg(not(unix))]
2681            let _ = before;
2682        }
2683    }
2684
2685    #[test]
2686    fn identity_initialization_preserves_persistent_corruption_refusal() {
2687        for phase in ["mutation", "activity"] {
2688            for corrupt in [b"".as_slice(), b"not json".as_slice()] {
2689                let root = tempfile::tempdir().unwrap();
2690                let store = ModelManagementStore::new(
2691                    root.path().join("state"),
2692                    root.path().join("models"),
2693                );
2694                drop(store.acquire_lease("fixture/model").unwrap());
2695                let path = if phase == "mutation" {
2696                    store.mutation_lock_path("fixture/model")
2697                } else {
2698                    store.lease_path("fixture/model")
2699                };
2700                std::fs::write(&path, corrupt).unwrap();
2701                assert!(matches!(
2702                    store.acquire_lease("fixture/model"),
2703                    Err(ModelManagementError::InvalidState { .. })
2704                ));
2705                assert_eq!(std::fs::read(&path).unwrap(), corrupt);
2706            }
2707        }
2708    }
2709
2710    #[cfg(unix)]
2711    #[test]
2712    fn identity_initialization_rejects_linked_gates_and_identity_files() {
2713        for phase in ["mutation", "activity"] {
2714            for gate in [false, true] {
2715                for symlink in [false, true] {
2716                    let root = tempfile::tempdir().unwrap();
2717                    let store = ModelManagementStore::new(
2718                        root.path().join("state"),
2719                        root.path().join("models"),
2720                    );
2721                    let identity = if phase == "mutation" {
2722                        store.mutation_lock_path("fixture/model")
2723                    } else {
2724                        store.lease_path("fixture/model")
2725                    };
2726                    create_private_dir(identity.parent().unwrap()).unwrap();
2727                    let path = if gate {
2728                        identity.parent().unwrap().join(".initialization.lock")
2729                    } else {
2730                        identity
2731                    };
2732                    let outside = root.path().join("outside");
2733                    std::fs::write(&outside, b"untouched").unwrap();
2734                    if symlink {
2735                        std::os::unix::fs::symlink(&outside, &path).unwrap();
2736                    } else {
2737                        std::fs::hard_link(&outside, &path).unwrap();
2738                    }
2739                    assert!(matches!(
2740                        store.acquire_lease("fixture/model"),
2741                        Err(ModelManagementError::InvalidState { .. })
2742                    ));
2743                    assert_eq!(std::fs::read(&outside).unwrap(), b"untouched");
2744                }
2745            }
2746        }
2747    }
2748
2749    fn directory_receipt(model_id: &str, managed_path: PathBuf) -> InstallReceipt {
2750        InstallReceipt {
2751            model_id: model_id.into(),
2752            managed_path,
2753            artifact_kind: ManagedArtifactKind::Directory,
2754            source_model_id: model_id.into(),
2755            source_revision: None,
2756            creation_generation: 1,
2757            shared_cache_references: vec![],
2758            adopted: false,
2759        }
2760    }
2761
2762    #[test]
2763    fn raw_receipt_identity_is_private_and_collision_fails_closed() {
2764        let root = tempfile::tempdir().unwrap();
2765        let models = root.path().join("models");
2766        std::fs::create_dir_all(&models).unwrap();
2767        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
2768        let receipt = directory_receipt("org/model", models.join("Model"));
2769        store.write_receipt(&receipt).unwrap();
2770        assert_eq!(store.load_receipt("org/model").unwrap(), Some(receipt));
2771
2772        #[cfg(unix)]
2773        {
2774            use std::os::unix::fs::PermissionsExt;
2775            assert_eq!(
2776                std::fs::metadata(store.receipt_path("org/model"))
2777                    .unwrap()
2778                    .permissions()
2779                    .mode()
2780                    & 0o077,
2781                0
2782            );
2783        }
2784
2785        let collision = directory_receipt("different/model", models.join("Other"));
2786        std::fs::write(
2787            store.receipt_path("org/model"),
2788            serde_json::to_vec(&collision).unwrap(),
2789        )
2790        .unwrap();
2791        assert!(matches!(
2792            store.load_receipt("org/model"),
2793            Err(ModelManagementError::IdentityMismatch { .. })
2794        ));
2795    }
2796
2797    #[test]
2798    fn journaled_removal_rejects_leaf_identity_swap() {
2799        let root = tempfile::tempdir().unwrap();
2800        let models = root.path().join("models");
2801        std::fs::create_dir_all(&models).unwrap();
2802        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
2803        let managed = models.join("Managed");
2804        std::fs::create_dir_all(&managed).unwrap();
2805        std::fs::write(managed.join("weights"), b"original").unwrap();
2806        let receipt = directory_receipt("org/model", managed.clone());
2807        store.write_receipt(&receipt).unwrap();
2808        let journal = RemovalJournal {
2809            model_id: "org/model".into(),
2810            removal_generation: 2,
2811            artifact_identity: artifact_identity(&managed, "org/model").unwrap(),
2812            receipt,
2813            quarantine_path: store.allocate_quarantine("org/model").unwrap(),
2814        };
2815        write_private_json(&store.removal_journal_path("org/model"), &journal).unwrap();
2816        let displaced = models.join("displaced-original");
2817        std::fs::rename(&managed, &displaced).unwrap();
2818        std::fs::create_dir_all(&managed).unwrap();
2819        std::fs::write(managed.join("sentinel"), b"replacement").unwrap();
2820
2821        assert!(matches!(
2822            store.begin_mutation("org/model").unwrap().remove(2),
2823            Err(ModelManagementError::UnsafeManagedPath { .. })
2824        ));
2825        assert_eq!(
2826            std::fs::read(managed.join("sentinel")).unwrap(),
2827            b"replacement"
2828        );
2829    }
2830
2831    #[test]
2832    fn journaled_removal_rejects_models_parent_swap() {
2833        let root = tempfile::tempdir().unwrap();
2834        let models = root.path().join("models");
2835        std::fs::create_dir_all(&models).unwrap();
2836        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
2837        let managed = models.join("Managed");
2838        std::fs::create_dir_all(&managed).unwrap();
2839        std::fs::write(managed.join("weights"), b"original").unwrap();
2840        let receipt = directory_receipt("org/model", managed.clone());
2841        store.write_receipt(&receipt).unwrap();
2842        let journal = RemovalJournal {
2843            model_id: "org/model".into(),
2844            removal_generation: 2,
2845            artifact_identity: artifact_identity(&managed, "org/model").unwrap(),
2846            receipt,
2847            quarantine_path: store.allocate_quarantine("org/model").unwrap(),
2848        };
2849        write_private_json(&store.removal_journal_path("org/model"), &journal).unwrap();
2850        std::fs::rename(&models, root.path().join("models-original")).unwrap();
2851        std::fs::create_dir_all(&managed).unwrap();
2852        std::fs::write(managed.join("sentinel"), b"replacement-parent").unwrap();
2853
2854        assert!(matches!(
2855            store.begin_mutation("org/model").unwrap().remove(2),
2856            Err(ModelManagementError::UnsafeManagedPath { .. })
2857        ));
2858        assert_eq!(
2859            std::fs::read(managed.join("sentinel")).unwrap(),
2860            b"replacement-parent"
2861        );
2862    }
2863
2864    #[cfg(not(any(target_os = "macos", target_os = "linux")))]
2865    #[test]
2866    fn directory_receipts_fail_closed_without_recursive_cleanup() {
2867        let root = tempfile::tempdir().unwrap();
2868        let models = root.path().join("models");
2869        std::fs::create_dir_all(&models).unwrap();
2870        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
2871        let managed = models.join("Managed");
2872        std::fs::create_dir_all(&managed).unwrap();
2873        std::fs::write(managed.join("shared-sentinel"), b"preserve").unwrap();
2874        store
2875            .write_receipt(&directory_receipt("org/model", managed.clone()))
2876            .unwrap();
2877
2878        assert!(!store.can_remove("org/model").unwrap());
2879        let error = store
2880            .begin_mutation("org/model")
2881            .unwrap()
2882            .remove(2)
2883            .unwrap_err();
2884        assert!(matches!(
2885            error,
2886            ModelManagementError::UnsafeManagedPath { .. }
2887        ));
2888        assert_eq!(
2889            std::fs::read(managed.join("shared-sentinel")).unwrap(),
2890            b"preserve"
2891        );
2892    }
2893
2894    #[test]
2895    fn failed_directory_staging_is_left_for_object_bound_cleanup() {
2896        let root = tempfile::tempdir().unwrap();
2897        let models = root.path().join("models");
2898        std::fs::create_dir_all(&models).unwrap();
2899        let store = ModelManagementStore::new(root.path().join("state"), models);
2900        let staging = store.create_install_staging("org/model").unwrap();
2901        std::fs::write(staging.join("shared-sentinel"), b"preserve").unwrap();
2902
2903        store.discard_install_staging(&staging);
2904
2905        assert_eq!(
2906            std::fs::read(staging.join("shared-sentinel")).unwrap(),
2907            b"preserve"
2908        );
2909    }
2910
2911    #[cfg(unix)]
2912    #[test]
2913    fn completed_symlink_removal_retry_is_idempotent() {
2914        use std::os::unix::fs::symlink;
2915
2916        let root = tempfile::tempdir().unwrap();
2917        let models = root.path().join("models");
2918        let shared = root.path().join("shared");
2919        std::fs::create_dir_all(&models).unwrap();
2920        std::fs::create_dir_all(&shared).unwrap();
2921        std::fs::write(shared.join("sentinel"), b"preserve").unwrap();
2922        let managed = models.join("Managed");
2923        symlink(&shared, &managed).unwrap();
2924        let store = ModelManagementStore::new(root.path().join("state"), models);
2925        let receipt = InstallReceipt {
2926            model_id: "org/model".into(),
2927            managed_path: managed,
2928            artifact_kind: ManagedArtifactKind::Symlink,
2929            source_model_id: "org/model".into(),
2930            source_revision: None,
2931            creation_generation: 1,
2932            shared_cache_references: vec![shared.canonicalize().unwrap()],
2933            adopted: false,
2934        };
2935        store.write_receipt(&receipt).unwrap();
2936
2937        let first = store
2938            .begin_mutation("org/model")
2939            .unwrap()
2940            .remove(2)
2941            .unwrap();
2942        let retry = store
2943            .begin_mutation("org/model")
2944            .unwrap()
2945            .remove(3)
2946            .unwrap();
2947        assert_eq!(retry, first);
2948        assert!(shared.join("sentinel").exists());
2949    }
2950
2951    #[test]
2952    fn pulled_directory_publish_before_receipt_resumes_from_install_intent() {
2953        let root = tempfile::tempdir().unwrap();
2954        let models = root.path().join("models");
2955        std::fs::create_dir_all(&models).unwrap();
2956        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
2957        let staging = store.create_install_staging("org/model").unwrap();
2958        std::fs::write(staging.join("weights"), b"owned").unwrap();
2959        let receipt = store
2960            .install_receipt_for_publication(
2961                "org/model",
2962                "org/model",
2963                1,
2964                false,
2965                models.join("Managed"),
2966                ManagedArtifactKind::Directory,
2967                &staging,
2968            )
2969            .unwrap();
2970        store.begin_install_intent(receipt, Some(&staging)).unwrap();
2971        store
2972            .publish_install_staging("org/model", &staging, "Managed")
2973            .unwrap();
2974        assert!(store.load_receipt("org/model").unwrap().is_none());
2975
2976        let resumed = store.resume_install_intent("org/model").unwrap().unwrap();
2977        assert_eq!(resumed.managed_path, models.join("Managed"));
2978        assert!(store.load_receipt("org/model").unwrap().is_some());
2979        #[cfg(any(target_os = "macos", target_os = "linux"))]
2980        assert!(store.can_remove("org/model").unwrap());
2981        #[cfg(not(any(target_os = "macos", target_os = "linux")))]
2982        assert!(!store.can_remove("org/model").unwrap());
2983    }
2984
2985    #[cfg(unix)]
2986    #[test]
2987    fn adopted_projection_publish_before_receipt_resumes_from_install_intent() {
2988        let root = tempfile::tempdir().unwrap();
2989        let models = root.path().join("models");
2990        let source = root.path().join("hand-installed");
2991        std::fs::create_dir_all(&models).unwrap();
2992        std::fs::create_dir_all(&source).unwrap();
2993        std::fs::write(source.join("sentinel"), b"preserve").unwrap();
2994        let store = ModelManagementStore::new(root.path().join("state"), models);
2995        let managed = store.adopted_projection_path("org/model").unwrap();
2996        let receipt = store
2997            .install_receipt_for_publication(
2998                "org/model",
2999                "org/model",
3000                1,
3001                true,
3002                managed.clone(),
3003                ManagedArtifactKind::Symlink,
3004                &source,
3005            )
3006            .unwrap();
3007        store.begin_install_intent(receipt, None).unwrap();
3008        store
3009            .materialize_adopted_projection("org/model", &source)
3010            .unwrap();
3011        assert!(store.load_receipt("org/model").unwrap().is_none());
3012
3013        let resumed = store.resume_install_intent("org/model").unwrap().unwrap();
3014        assert_eq!(resumed.managed_path, managed);
3015        assert!(store.can_remove("org/model").unwrap());
3016        assert!(source.join("sentinel").exists());
3017    }
3018
3019    #[cfg(unix)]
3020    #[test]
3021    fn install_intent_never_claims_a_preexisting_unreceipted_leaf() {
3022        use std::os::unix::fs::symlink;
3023
3024        let root = tempfile::tempdir().unwrap();
3025        let models = root.path().join("models");
3026        let source = root.path().join("shared");
3027        std::fs::create_dir_all(&models).unwrap();
3028        std::fs::create_dir_all(&source).unwrap();
3029        let managed = models.join("Managed");
3030        symlink(&source, &managed).unwrap();
3031        let store = ModelManagementStore::new(root.path().join("state"), models);
3032        let receipt = store
3033            .install_receipt_for_publication(
3034                "org/model",
3035                "org/model",
3036                1,
3037                false,
3038                managed.clone(),
3039                ManagedArtifactKind::Symlink,
3040                &source,
3041            )
3042            .unwrap();
3043
3044        let error = store.begin_install_intent(receipt, None).unwrap_err();
3045        assert!(matches!(
3046            error,
3047            ModelManagementError::UnsafeManagedPath { .. }
3048        ));
3049        assert!(managed.exists());
3050        assert!(store.load_receipt("org/model").unwrap().is_none());
3051    }
3052
3053    #[test]
3054    fn forged_install_journal_paths_fail_before_filesystem_mutation() {
3055        let root = tempfile::tempdir().unwrap();
3056        let models = root.path().join("models");
3057        std::fs::create_dir_all(&models).unwrap();
3058        let outside = root.path().join("outside");
3059        std::fs::create_dir_all(&outside).unwrap();
3060        std::fs::write(outside.join("sentinel"), b"preserve").unwrap();
3061        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
3062
3063        let forged_managed = InstallJournal {
3064            model_id: "org/model".into(),
3065            receipt: directory_receipt("org/model", outside.clone()),
3066            staging_path: None,
3067            staging_identity: None,
3068        };
3069        write_private_json(&store.install_journal_path("org/model"), &forged_managed).unwrap();
3070        assert!(matches!(
3071            store.resume_install_intent("org/model"),
3072            Err(ModelManagementError::UnsafeManagedPath { .. })
3073        ));
3074        assert!(outside.join("sentinel").exists());
3075
3076        let forged_staging = InstallJournal {
3077            model_id: "org/other".into(),
3078            receipt: directory_receipt("org/other", models.join("Other")),
3079            staging_path: Some(outside.clone()),
3080            staging_identity: Some(artifact_identity(&outside, "org/other").unwrap()),
3081        };
3082        write_private_json(&store.install_journal_path("org/other"), &forged_staging).unwrap();
3083        assert!(matches!(
3084            store.resume_install_intent("org/other"),
3085            Err(ModelManagementError::UnsafeManagedPath { .. })
3086        ));
3087        assert!(outside.join("sentinel").exists());
3088    }
3089
3090    #[cfg(unix)]
3091    #[test]
3092    fn startup_ignores_noncanonical_removal_journal_without_touching_valid_model() {
3093        use std::os::unix::fs::symlink;
3094
3095        let root = tempfile::tempdir().unwrap();
3096        let state = root.path().join("state");
3097        let models = root.path().join("models");
3098        let shared = root.path().join("shared");
3099        let outside = root.path().join("outside");
3100        std::fs::create_dir_all(&models).unwrap();
3101        std::fs::create_dir_all(&shared).unwrap();
3102        std::fs::create_dir_all(&outside).unwrap();
3103        std::fs::write(shared.join("sentinel"), b"shared").unwrap();
3104        std::fs::write(outside.join("sentinel"), b"outside").unwrap();
3105        let managed = models.join("Managed");
3106        symlink(&shared, &managed).unwrap();
3107        let store = ModelManagementStore::new(state.clone(), models.clone());
3108        let legitimate = InstallReceipt {
3109            model_id: "org/model".into(),
3110            managed_path: managed.clone(),
3111            artifact_kind: ManagedArtifactKind::Symlink,
3112            source_model_id: "org/model".into(),
3113            source_revision: None,
3114            creation_generation: 1,
3115            shared_cache_references: vec![shared.canonicalize().unwrap()],
3116            adopted: false,
3117        };
3118        store.write_receipt(&legitimate).unwrap();
3119        create_private_dir(&store.removals_root).unwrap();
3120        let forged = RemovalJournal {
3121            model_id: "org/model".into(),
3122            removal_generation: 99,
3123            receipt: InstallReceipt {
3124                managed_path: outside.join("missing-leaf"),
3125                ..legitimate
3126            },
3127            quarantine_path: models.join(".car-remove-forged"),
3128            artifact_identity: ArtifactIdentity {
3129                kind: ManagedArtifactKind::Symlink,
3130                device: 1,
3131                inode: 1,
3132                file_len: 0,
3133            },
3134        };
3135        write_private_json(&store.removals_root.join("junk.json"), &forged).unwrap();
3136        drop(store);
3137
3138        let recovered = ModelManagementStore::new(state, models);
3139        assert!(std::fs::symlink_metadata(&managed).is_ok());
3140        assert!(recovered.car_enabled("org/model").unwrap());
3141        assert_eq!(std::fs::read(shared.join("sentinel")).unwrap(), b"shared");
3142        assert_eq!(std::fs::read(outside.join("sentinel")).unwrap(), b"outside");
3143    }
3144
3145    // try_clone retains the same open-file description as an inherited fork
3146    // descriptor, without forking a multithreaded test process.
3147    #[cfg(any(target_os = "macos", target_os = "linux"))]
3148    #[test]
3149    fn mutation_guard_releases_lock_even_with_an_inherited_descriptor() {
3150        let (_root, _models, store) = supported_platform_store();
3151        let guard = store.begin_mutation("org/model").unwrap();
3152        let inherited = guard._file.try_clone().unwrap();
3153        assert!(matches!(
3154            store.begin_mutation("org/model"),
3155            Err(ModelManagementError::MutationInProgress { .. })
3156        ));
3157        drop(guard);
3158        let replacement = store.begin_mutation("org/model").unwrap();
3159        // Releasing the old duplicate must not unlock the replacement owner.
3160        drop(inherited);
3161        assert!(matches!(
3162            store.begin_mutation("org/model"),
3163            Err(ModelManagementError::MutationInProgress { .. })
3164        ));
3165        drop(replacement);
3166        assert!(store.begin_mutation("org/model").is_ok());
3167    }
3168
3169    #[cfg(any(target_os = "macos", target_os = "linux"))]
3170    #[test]
3171    fn lease_and_idle_guards_release_despite_inherited_descriptors() {
3172        let (_root, _models, store) = supported_platform_store();
3173        let lease = store.acquire_lease("org/model").unwrap();
3174        let inherited = lease._file.try_clone().unwrap();
3175        assert!(store.model_in_use("org/model").unwrap());
3176        drop(lease);
3177        assert!(!store.model_in_use("org/model").unwrap());
3178        let idle = store.lock_idle_activity("org/model").unwrap();
3179        let inherited_idle = idle.try_clone().unwrap();
3180        assert!(store.model_in_use("org/model").unwrap());
3181        drop(idle);
3182        assert!(!store.model_in_use("org/model").unwrap());
3183        drop((inherited, inherited_idle));
3184    }
3185
3186    #[cfg(any(target_os = "macos", target_os = "linux"))]
3187    #[test]
3188    fn temporary_owner_releases_initialization_gate_despite_inherited_descriptor() {
3189        let (_root, _models, store) = supported_platform_store();
3190        let identity = store
3191            .open_identity_lock(&store.mutation_lock_path("org/model"), "org/model")
3192            .unwrap();
3193        let path = store
3194            .mutation_lock_path("org/model")
3195            .parent()
3196            .unwrap()
3197            .join(".initialization.lock");
3198        let gate = open_existing_identity_lock(&path).unwrap();
3199        gate.lock().unwrap();
3200        let gate = OwnedFileLock::new(gate);
3201        let inherited = gate.try_clone().unwrap();
3202        let contender = open_existing_identity_lock(&path).unwrap();
3203        assert!(contender.try_lock().is_err());
3204        drop(gate);
3205        contender.try_lock().unwrap();
3206        contender.unlock().unwrap();
3207        drop((identity, inherited));
3208    }
3209
3210    #[cfg(any(target_os = "macos", target_os = "linux"))]
3211    fn supported_platform_store() -> (tempfile::TempDir, PathBuf, ModelManagementStore) {
3212        let root = tempfile::tempdir().unwrap();
3213        let models = root.path().join("models");
3214        std::fs::create_dir_all(&models).unwrap();
3215        let store = ModelManagementStore::new(root.path().join("state"), models.clone());
3216        (root, models, store)
3217    }
3218
3219    #[cfg(any(target_os = "macos", target_os = "linux"))]
3220    fn no_quarantine_left(models: &Path) -> bool {
3221        std::fs::read_dir(models).unwrap().all(|entry| {
3222            !entry
3223                .unwrap()
3224                .file_name()
3225                .to_string_lossy()
3226                .starts_with(".car-remove-")
3227        })
3228    }
3229
3230    #[cfg(any(target_os = "macos", target_os = "linux"))]
3231    const MAX_REMOVAL_DEPTH_FOR_TESTS: usize = super::object_bound::MAX_REMOVAL_DEPTH;
3232
3233    /// Unwraps a fail-closed refusal to its reason. Every race test funnels
3234    /// through here so no single site can quietly weaken into accepting a
3235    /// different error variant.
3236    #[cfg(any(target_os = "macos", target_os = "linux"))]
3237    fn unsafe_managed_reason(error: &ModelManagementError) -> &str {
3238        match error {
3239            ModelManagementError::UnsafeManagedPath { reason, .. } => reason,
3240            other => panic!("expected UnsafeManagedPath, got {other:?}"),
3241        }
3242    }
3243
3244    #[cfg(any(target_os = "macos", target_os = "linux"))]
3245    #[test]
3246    fn directory_receipt_is_removable_and_leaves_a_tombstone() {
3247        let (_root, models, store) = supported_platform_store();
3248        let managed = models.join("Managed");
3249        std::fs::create_dir_all(&managed).unwrap();
3250        std::fs::write(managed.join("weights"), b"owned").unwrap();
3251        store
3252            .write_receipt(&directory_receipt("org/model", managed.clone()))
3253            .unwrap();
3254
3255        assert!(store.can_remove("org/model").unwrap());
3256        let result = store
3257            .begin_mutation("org/model")
3258            .unwrap()
3259            .remove(2)
3260            .unwrap();
3261
3262        assert_eq!(result.artifact_kind, ManagedArtifactKind::Directory);
3263        assert!(std::fs::symlink_metadata(&managed).is_err());
3264        assert!(store.load_receipt("org/model").unwrap().is_none());
3265        let tombstone = store.load_tombstone("org/model").unwrap().unwrap();
3266        assert_eq!(
3267            tombstone.artifact_kind,
3268            Some(ManagedArtifactKind::Directory)
3269        );
3270        assert_eq!(tombstone.removal_generation, 2);
3271        assert!(no_quarantine_left(&models));
3272        assert!(store.load_removal_journal("org/model").unwrap().is_none());
3273    }
3274
3275    #[cfg(any(target_os = "macos", target_os = "linux"))]
3276    #[test]
3277    fn directory_removal_preserves_shared_cache_symlink_targets() {
3278        use std::os::unix::fs::symlink;
3279
3280        let (root, models, store) = supported_platform_store();
3281        let shared = root.path().join("shared-cache");
3282        std::fs::create_dir_all(&shared).unwrap();
3283        std::fs::write(shared.join("blob"), b"shared-bytes").unwrap();
3284        let managed = models.join("Managed");
3285        let nested = managed.join("a").join("b").join("c");
3286        std::fs::create_dir_all(&nested).unwrap();
3287        std::fs::write(managed.join("config.json"), b"{}").unwrap();
3288        std::fs::write(nested.join("weights"), b"owned").unwrap();
3289        symlink(shared.join("blob"), managed.join("blob.safetensors")).unwrap();
3290        let blob = shared.join("blob").canonicalize().unwrap();
3291        let mut receipt = directory_receipt("org/model", managed.clone());
3292        receipt.shared_cache_references = vec![blob.clone()];
3293        store.write_receipt(&receipt).unwrap();
3294
3295        let result = store
3296            .begin_mutation("org/model")
3297            .unwrap()
3298            .remove(2)
3299            .unwrap();
3300
3301        assert!(std::fs::symlink_metadata(&managed).is_err());
3302        assert_eq!(std::fs::read(&blob).unwrap(), b"shared-bytes");
3303        assert_eq!(std::fs::read_dir(&shared).unwrap().count(), 1);
3304        assert_eq!(result.preserved_shared_cache_references, vec![blob]);
3305        assert!(no_quarantine_left(&models));
3306    }
3307
3308    #[cfg(any(target_os = "macos", target_os = "linux"))]
3309    #[test]
3310    fn directory_removal_resumes_after_a_partial_quarantine_delete() {
3311        let (_root, models, store) = supported_platform_store();
3312        let managed = models.join("Managed");
3313        std::fs::create_dir_all(managed.join("sub")).unwrap();
3314        std::fs::write(managed.join("weights"), b"owned").unwrap();
3315        std::fs::write(managed.join("sub").join("more"), b"owned").unwrap();
3316        let receipt = directory_receipt("org/model", managed.clone());
3317        store.write_receipt(&receipt).unwrap();
3318        let quarantine = store.allocate_quarantine("org/model").unwrap();
3319        let journal = RemovalJournal {
3320            model_id: "org/model".into(),
3321            removal_generation: 2,
3322            artifact_identity: artifact_identity(&managed, "org/model").unwrap(),
3323            receipt,
3324            quarantine_path: quarantine.clone(),
3325        };
3326        write_private_json(&store.removal_journal_path("org/model"), &journal).unwrap();
3327        // A crash after the rename and after some entries were already deleted.
3328        std::fs::rename(&managed, &quarantine).unwrap();
3329        std::fs::remove_file(quarantine.join("weights")).unwrap();
3330
3331        let resumed = store
3332            .begin_mutation("org/model")
3333            .unwrap()
3334            .remove(2)
3335            .unwrap();
3336        assert_eq!(resumed.artifact_kind, ManagedArtifactKind::Directory);
3337        assert!(std::fs::symlink_metadata(&quarantine).is_err());
3338        assert!(no_quarantine_left(&models));
3339
3340        // A third request replays the durable tombstone result. The generation
3341        // it reports must stay the one that actually performed the removal (2),
3342        // not the generation this caller asked for (3), and the preserved
3343        // reference list must replay empty as the receipt recorded it.
3344        let replayed = store
3345            .begin_mutation("org/model")
3346            .unwrap()
3347            .remove(3)
3348            .unwrap();
3349        assert_eq!(replayed.artifact_kind, ManagedArtifactKind::Directory);
3350        let tombstone = store.load_tombstone("org/model").unwrap().unwrap();
3351        assert_eq!(tombstone.removal_generation, 2);
3352        assert!(replayed.preserved_shared_cache_references.is_empty());
3353    }
3354
3355    #[cfg(any(target_os = "macos", target_os = "linux"))]
3356    #[test]
3357    fn directory_removal_refuses_trees_deeper_than_the_cap() {
3358        let (_root, models, store) = supported_platform_store();
3359        let managed = models.join("Managed");
3360        let mut deep = managed.clone();
3361        for level in 0..(MAX_REMOVAL_DEPTH_FOR_TESTS + 1) {
3362            deep = deep.join(format!("level{level}"));
3363        }
3364        std::fs::create_dir_all(&deep).unwrap();
3365        std::fs::write(deep.join("sentinel"), b"deep").unwrap();
3366        store
3367            .write_receipt(&directory_receipt("org/model", managed.clone()))
3368            .unwrap();
3369
3370        let error = store
3371            .begin_mutation("org/model")
3372            .unwrap()
3373            .remove(2)
3374            .unwrap_err();
3375
3376        let reason = unsafe_managed_reason(&error);
3377        assert!(reason.contains("depth"), "unexpected reason: {reason}");
3378        // The leaf was already detached into its quarantine; nothing in it was deleted.
3379        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3380        let quarantined_leaf = deep
3381            .strip_prefix(&managed)
3382            .map(|rest| journal.quarantine_path.join(rest))
3383            .unwrap();
3384        assert_eq!(
3385            std::fs::read(quarantined_leaf.join("sentinel")).unwrap(),
3386            b"deep"
3387        );
3388    }
3389
3390    // Deliberately ungated: `matches_for_removal` is pure struct logic that
3391    // compiles on every target, so narrowing this to macOS/Linux like its
3392    // siblings would silently drop Windows coverage.
3393    #[test]
3394    fn removal_identity_ignores_length_for_directories_only() {
3395        let directory = ArtifactIdentity {
3396            kind: ManagedArtifactKind::Directory,
3397            device: 7,
3398            inode: 42,
3399            file_len: 96,
3400        };
3401        let shrunk = ArtifactIdentity {
3402            file_len: 32,
3403            ..directory.clone()
3404        };
3405        assert!(directory.matches_for_removal(&shrunk));
3406        assert!(!directory.matches_for_removal(&ArtifactIdentity {
3407            inode: 43,
3408            ..directory.clone()
3409        }));
3410        assert!(!directory.matches_for_removal(&ArtifactIdentity {
3411            device: 8,
3412            ..directory.clone()
3413        }));
3414
3415        let file = ArtifactIdentity {
3416            kind: ManagedArtifactKind::File,
3417            device: 7,
3418            inode: 42,
3419            file_len: 96,
3420        };
3421        assert!(file.matches_for_removal(&file));
3422        assert!(!file.matches_for_removal(&ArtifactIdentity {
3423            file_len: 95,
3424            ..file.clone()
3425        }));
3426        assert!(!file.matches_for_removal(&ArtifactIdentity {
3427            kind: ManagedArtifactKind::Symlink,
3428            ..file.clone()
3429        }));
3430    }
3431
3432    #[cfg(any(target_os = "macos", target_os = "linux"))]
3433    #[test]
3434    fn same_filesystem_predicate_compares_devices_only() {
3435        use super::object_bound::{same_filesystem, Identity};
3436        let root = Identity {
3437            device: 5,
3438            inode: 1,
3439        };
3440        assert!(same_filesystem(
3441            &root,
3442            &Identity {
3443                device: 5,
3444                inode: 999
3445            }
3446        ));
3447        assert!(!same_filesystem(
3448            &root,
3449            &Identity {
3450                device: 6,
3451                inode: 1
3452            }
3453        ));
3454    }
3455
3456    #[cfg(any(target_os = "macos", target_os = "linux"))]
3457    #[test]
3458    fn directory_removal_refuses_entries_added_after_capture() {
3459        let (_root, models, store) = supported_platform_store();
3460        let managed = models.join("Managed");
3461        std::fs::create_dir_all(managed.join("sub")).unwrap();
3462        std::fs::write(managed.join("weights"), b"owned").unwrap();
3463        std::fs::write(managed.join("sub").join("more"), b"owned").unwrap();
3464        let store = store.with_removal_hook(|phase, quarantine| {
3465            if phase == RemovalPhase::AfterCapture {
3466                std::fs::write(quarantine.join("sub").join("late"), b"not captured").unwrap();
3467            }
3468        });
3469        store
3470            .write_receipt(&directory_receipt("org/model", managed.clone()))
3471            .unwrap();
3472
3473        let error = store
3474            .begin_mutation("org/model")
3475            .unwrap()
3476            .remove(2)
3477            .unwrap_err();
3478
3479        let reason = unsafe_managed_reason(&error);
3480        assert!(
3481            reason.contains("not captured"),
3482            "unexpected reason: {reason}"
3483        );
3484        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3485        let late = journal.quarantine_path.join("sub").join("late");
3486        assert_eq!(std::fs::read(&late).unwrap(), b"not captured");
3487
3488        // Once the foreign entry is gone the journaled removal completes.
3489        std::fs::remove_file(&late).unwrap();
3490        let store = store.with_removal_hook(|_, _| {});
3491        let resumed = store
3492            .begin_mutation("org/model")
3493            .unwrap()
3494            .remove(2)
3495            .unwrap();
3496        assert_eq!(resumed.artifact_kind, ManagedArtifactKind::Directory);
3497        assert!(no_quarantine_left(&models));
3498    }
3499
3500    #[cfg(any(target_os = "macos", target_os = "linux"))]
3501    #[test]
3502    fn directory_removal_refuses_root_swap_after_rename() {
3503        let (_root, models, store) = supported_platform_store();
3504        let managed = models.join("Managed");
3505        std::fs::create_dir_all(&managed).unwrap();
3506        std::fs::write(managed.join("weights"), b"owned").unwrap();
3507        let displaced = models.join("displaced");
3508        let store = store.with_removal_hook(move |phase, quarantine| {
3509            if phase == RemovalPhase::AfterRename {
3510                std::fs::rename(quarantine, &displaced).unwrap();
3511                std::fs::create_dir_all(quarantine).unwrap();
3512                std::fs::write(quarantine.join("sentinel"), b"replacement").unwrap();
3513            }
3514        });
3515        store
3516            .write_receipt(&directory_receipt("org/model", managed.clone()))
3517            .unwrap();
3518
3519        let error = store
3520            .begin_mutation("org/model")
3521            .unwrap()
3522            .remove(2)
3523            .unwrap_err();
3524
3525        let reason = unsafe_managed_reason(&error);
3526        assert!(
3527            reason.contains("identity changed"),
3528            "unexpected reason: {reason}"
3529        );
3530        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3531        assert_eq!(
3532            std::fs::read(journal.quarantine_path.join("sentinel")).unwrap(),
3533            b"replacement"
3534        );
3535        assert_eq!(
3536            std::fs::read(models.join("displaced").join("weights")).unwrap(),
3537            b"owned"
3538        );
3539    }
3540
3541    #[cfg(any(target_os = "macos", target_os = "linux"))]
3542    #[test]
3543    fn directory_symlink_planted_after_rename_is_unlinked_not_followed() {
3544        use std::os::unix::fs::symlink;
3545
3546        let (root, models, store) = supported_platform_store();
3547        let outside = root.path().join("outside");
3548        std::fs::create_dir_all(&outside).unwrap();
3549        std::fs::write(outside.join("sentinel"), b"outside").unwrap();
3550        let managed = models.join("Managed");
3551        std::fs::create_dir_all(&managed).unwrap();
3552        std::fs::write(managed.join("weights"), b"owned").unwrap();
3553        let target = outside.clone();
3554        let store = store.with_removal_hook(move |phase, quarantine| {
3555            if phase == RemovalPhase::AfterRename {
3556                symlink(&target, quarantine.join("escape")).unwrap();
3557            }
3558        });
3559        store
3560            .write_receipt(&directory_receipt("org/model", managed.clone()))
3561            .unwrap();
3562
3563        let result = store.begin_mutation("org/model").unwrap().remove(2);
3564
3565        assert_eq!(
3566            std::fs::read(outside.join("sentinel")).map_err(|e| e.to_string()),
3567            Ok(b"outside".to_vec()),
3568            "outside sentinel was deleted; removal result: {result:?}"
3569        );
3570        assert!(result.is_ok(), "{result:?}");
3571        assert!(std::fs::symlink_metadata(&managed).is_err());
3572        assert!(no_quarantine_left(&models));
3573    }
3574
3575    #[cfg(any(target_os = "macos", target_os = "linux"))]
3576    #[test]
3577    fn directory_removal_refuses_root_swap_between_path_check_and_descriptor_open() {
3578        let (_root, models, store) = supported_platform_store();
3579        let managed = models.join("Managed");
3580        std::fs::create_dir_all(&managed).unwrap();
3581        std::fs::write(managed.join("weights"), b"owned").unwrap();
3582        let displaced = models.join("displaced");
3583        let store = store.with_removal_hook(move |phase, quarantine| {
3584            if phase == RemovalPhase::BeforeRootOpen {
3585                std::fs::rename(quarantine, &displaced).unwrap();
3586                std::fs::create_dir_all(quarantine).unwrap();
3587                std::fs::write(quarantine.join("sentinel"), b"replacement").unwrap();
3588            }
3589        });
3590        store
3591            .write_receipt(&directory_receipt("org/model", managed.clone()))
3592            .unwrap();
3593
3594        let error = store
3595            .begin_mutation("org/model")
3596            .unwrap()
3597            .remove(2)
3598            .unwrap_err();
3599
3600        let reason = unsafe_managed_reason(&error);
3601        assert_eq!(
3602            reason,
3603            "quarantined directory identity changed before deletion"
3604        );
3605        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3606        assert_eq!(
3607            std::fs::read(journal.quarantine_path.join("sentinel")).unwrap(),
3608            b"replacement"
3609        );
3610        assert_eq!(
3611            std::fs::read(models.join("displaced").join("weights")).unwrap(),
3612            b"owned"
3613        );
3614    }
3615
3616    #[cfg(any(target_os = "macos", target_os = "linux"))]
3617    #[test]
3618    fn directory_removal_refuses_when_the_quarantine_vanishes_after_our_own_rename() {
3619        let (_root, models, store) = supported_platform_store();
3620        let managed = models.join("Managed");
3621        std::fs::create_dir_all(&managed).unwrap();
3622        std::fs::write(managed.join("weights"), b"owned").unwrap();
3623        let managed_for_hook = managed.clone();
3624        let store = store.with_removal_hook(move |phase, quarantine| {
3625            if phase == RemovalPhase::AfterRename {
3626                // A racer moves the quarantine straight back to its managed name.
3627                std::fs::rename(quarantine, &managed_for_hook).unwrap();
3628            }
3629        });
3630        store
3631            .write_receipt(&directory_receipt("org/model", managed.clone()))
3632            .unwrap();
3633
3634        let error = store
3635            .begin_mutation("org/model")
3636            .unwrap()
3637            .remove(2)
3638            .unwrap_err();
3639
3640        let reason = unsafe_managed_reason(&error);
3641        assert!(reason.contains("vanished"), "unexpected reason: {reason}");
3642        assert_eq!(std::fs::read(managed.join("weights")).unwrap(), b"owned");
3643        assert!(store.load_receipt("org/model").unwrap().is_some());
3644        assert!(store.load_removal_journal("org/model").unwrap().is_some());
3645    }
3646
3647    #[cfg(any(target_os = "macos", target_os = "linux"))]
3648    #[test]
3649    fn first_attempt_vanish_refusal_names_the_retry_that_completes_the_removal() {
3650        let (_root, models, store) = supported_platform_store();
3651        let managed = models.join("Managed");
3652        std::fs::create_dir_all(&managed).unwrap();
3653        std::fs::write(managed.join("weights"), b"owned").unwrap();
3654        let managed_for_hook = managed.clone();
3655        let sabotaged = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
3656        let store = store.with_removal_hook(move |phase, quarantine| {
3657            // Sabotage the first attempt only: a racer moves the quarantine back
3658            // to its managed name right after CAR's own rename. The retry below
3659            // then runs against an untouched store.
3660            if phase == RemovalPhase::AfterRename
3661                && !sabotaged.swap(true, std::sync::atomic::Ordering::SeqCst)
3662            {
3663                std::fs::rename(quarantine, &managed_for_hook).unwrap();
3664            }
3665        });
3666        store
3667            .write_receipt(&directory_receipt("org/model", managed.clone()))
3668            .unwrap();
3669
3670        let error = store
3671            .begin_mutation("org/model")
3672            .unwrap()
3673            .remove(2)
3674            .unwrap_err();
3675
3676        // Asserted verbatim: the reason is the operator's only signal that
3677        // retrying is the remedy rather than corruption, so dropping the
3678        // journal-retention half of the sentence must fail this test.
3679        assert_eq!(
3680            unsafe_managed_reason(&error),
3681            "quarantine vanished before deletion; the removal journal is retained, so retrying the removal resumes and completes it"
3682        );
3683        assert!(store.load_removal_journal("org/model").unwrap().is_some());
3684
3685        // And the retry the reason promises does finish the removal.
3686        let retried = store
3687            .begin_mutation("org/model")
3688            .unwrap()
3689            .remove(2)
3690            .unwrap();
3691
3692        assert_eq!(retried.artifact_kind, ManagedArtifactKind::Directory);
3693        assert!(std::fs::symlink_metadata(&managed).is_err());
3694        assert!(no_quarantine_left(&models));
3695        assert!(store.load_removal_journal("org/model").unwrap().is_none());
3696        assert!(store.load_receipt("org/model").unwrap().is_none());
3697    }
3698
3699    #[cfg(any(target_os = "macos", target_os = "linux"))]
3700    #[test]
3701    fn directory_removal_refuses_a_captured_file_replaced_before_unlink() {
3702        let (root, models, store) = supported_platform_store();
3703        let outside = root.path().join("outside");
3704        std::fs::create_dir_all(&outside).unwrap();
3705        let managed = models.join("Managed");
3706        std::fs::create_dir_all(&managed).unwrap();
3707        std::fs::write(managed.join("weights"), b"owned").unwrap();
3708        let parked = outside.join("parked");
3709        let store = store.with_removal_hook(move |phase, quarantine| {
3710            if phase == RemovalPhase::AfterCapture {
3711                std::fs::rename(quarantine.join("weights"), &parked).unwrap();
3712                std::fs::write(quarantine.join("weights"), b"victim").unwrap();
3713            }
3714        });
3715        store
3716            .write_receipt(&directory_receipt("org/model", managed.clone()))
3717            .unwrap();
3718
3719        let error = store
3720            .begin_mutation("org/model")
3721            .unwrap()
3722            .remove(2)
3723            .unwrap_err();
3724
3725        let reason = unsafe_managed_reason(&error);
3726        assert!(
3727            reason.contains("identity changed"),
3728            "unexpected reason: {reason}"
3729        );
3730        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3731        assert_eq!(
3732            std::fs::read(journal.quarantine_path.join("weights")).unwrap(),
3733            b"victim"
3734        );
3735        assert_eq!(std::fs::read(outside.join("parked")).unwrap(), b"owned");
3736    }
3737
3738    #[cfg(any(target_os = "macos", target_os = "linux"))]
3739    #[test]
3740    fn directory_removal_refuses_a_captured_directory_replaced_before_rmdir() {
3741        let (root, models, store) = supported_platform_store();
3742        let outside = root.path().join("outside");
3743        std::fs::create_dir_all(&outside).unwrap();
3744        let managed = models.join("Managed");
3745        std::fs::create_dir_all(managed.join("sub")).unwrap();
3746        std::fs::write(managed.join("sub").join("more"), b"owned").unwrap();
3747        let parked = outside.join("parked-sub");
3748        let parked_for_hook = parked.clone();
3749        let store = store.with_removal_hook(move |phase, quarantine| {
3750            if phase == RemovalPhase::AfterCapture {
3751                std::fs::rename(quarantine.join("sub"), &parked_for_hook).unwrap();
3752                std::fs::create_dir_all(quarantine.join("sub")).unwrap();
3753                std::fs::write(quarantine.join("sub").join("victim"), b"victim").unwrap();
3754            }
3755        });
3756        store
3757            .write_receipt(&directory_receipt("org/model", managed.clone()))
3758            .unwrap();
3759
3760        let error = store
3761            .begin_mutation("org/model")
3762            .unwrap()
3763            .remove(2)
3764            .unwrap_err();
3765
3766        let reason = unsafe_managed_reason(&error);
3767        assert!(
3768            reason.contains("identity changed"),
3769            "unexpected reason: {reason}"
3770        );
3771        let journal = store.load_removal_journal("org/model").unwrap().unwrap();
3772        assert_eq!(
3773            std::fs::read(journal.quarantine_path.join("sub").join("victim")).unwrap(),
3774            b"victim"
3775        );
3776        // The captured subtree moved elsewhere is still CAR's object: its
3777        // captured entry was unlinked there, the directory itself was not.
3778        assert!(std::fs::symlink_metadata(&parked).is_ok());
3779    }
3780
3781    #[cfg(any(target_os = "macos", target_os = "linux"))]
3782    #[test]
3783    fn directory_removal_accepts_a_tree_at_the_depth_cap() {
3784        let (_root, models, store) = supported_platform_store();
3785        let managed = models.join("Managed");
3786        let mut deep = managed.clone();
3787        for level in 0..MAX_REMOVAL_DEPTH_FOR_TESTS {
3788            deep = deep.join(format!("level{level}"));
3789        }
3790        std::fs::create_dir_all(&deep).unwrap();
3791        std::fs::write(deep.join("sentinel"), b"deep").unwrap();
3792        store
3793            .write_receipt(&directory_receipt("org/model", managed.clone()))
3794            .unwrap();
3795
3796        let result = store
3797            .begin_mutation("org/model")
3798            .unwrap()
3799            .remove(2)
3800            .unwrap();
3801
3802        assert_eq!(result.artifact_kind, ManagedArtifactKind::Directory);
3803        assert!(std::fs::symlink_metadata(&managed).is_err());
3804        assert!(no_quarantine_left(&models));
3805    }
3806
3807    #[cfg(any(target_os = "macos", target_os = "linux"))]
3808    #[test]
3809    fn directory_removal_refuses_when_the_quarantine_vanishes_before_the_descriptor_open() {
3810        let (_root, models, store) = supported_platform_store();
3811        let managed = models.join("Managed");
3812        std::fs::create_dir_all(&managed).unwrap();
3813        std::fs::write(managed.join("weights"), b"owned").unwrap();
3814        let managed_for_hook = managed.clone();
3815        let store = store.with_removal_hook(move |phase, quarantine| {
3816            if phase == RemovalPhase::BeforeRootOpen {
3817                // The path-based checks passed; a racer now moves the quarantine
3818                // back to its managed name before the binding descriptor opens.
3819                std::fs::rename(quarantine, &managed_for_hook).unwrap();
3820            }
3821        });
3822        store
3823            .write_receipt(&directory_receipt("org/model", managed.clone()))
3824            .unwrap();
3825
3826        let error = store
3827            .begin_mutation("org/model")
3828            .unwrap()
3829            .remove(2)
3830            .unwrap_err();
3831
3832        // Spelled out rather than compared against QUARANTINE_VANISHED_REASON:
3833        // this window is user-reachable on a first attempt too, and the receipt
3834        // and journal assertions below are exactly the retention the sentence
3835        // promises, so the promise is pinned here as well as at the guard.
3836        assert_eq!(
3837            unsafe_managed_reason(&error),
3838            "quarantine vanished before deletion; the removal journal is retained, so retrying the removal resumes and completes it"
3839        );
3840        assert_eq!(std::fs::read(managed.join("weights")).unwrap(), b"owned");
3841        assert!(store.load_receipt("org/model").unwrap().is_some());
3842        assert!(store.load_removal_journal("org/model").unwrap().is_some());
3843    }
3844
3845    /// Pins — does not endorse — what the retry does when the racer parked the
3846    /// quarantine somewhere other than the managed name. The resume finds
3847    /// neither the artifact nor the quarantine, so it cannot re-detach or
3848    /// delete anything; it unlinks the receipt and journal and reports success
3849    /// while the bytes survive at the parked path. Recorded so a deliberate
3850    /// change to that behaviour has to change this test.
3851    #[cfg(any(target_os = "macos", target_os = "linux"))]
3852    #[test]
3853    fn retry_after_the_quarantine_was_parked_elsewhere_settles_without_deleting_it() {
3854        let (root, models, store) = supported_platform_store();
3855        let outside = root.path().join("outside");
3856        std::fs::create_dir_all(&outside).unwrap();
3857        let managed = models.join("Managed");
3858        std::fs::create_dir_all(&managed).unwrap();
3859        std::fs::write(managed.join("weights"), b"owned").unwrap();
3860        let parked = outside.join("parked");
3861        let parked_for_hook = parked.clone();
3862        let store = store.with_removal_hook(move |phase, quarantine| {
3863            if phase == RemovalPhase::AfterRename {
3864                // Not back to the managed name: out of the models root entirely.
3865                std::fs::rename(quarantine, &parked_for_hook).unwrap();
3866            }
3867        });
3868        store
3869            .write_receipt(&directory_receipt("org/model", managed.clone()))
3870            .unwrap();
3871
3872        let error = store
3873            .begin_mutation("org/model")
3874            .unwrap()
3875            .remove(2)
3876            .unwrap_err();
3877
3878        assert_eq!(
3879            unsafe_managed_reason(&error),
3880            "quarantine vanished before deletion; the removal journal is retained, so retrying the removal resumes and completes it"
3881        );
3882        assert!(store.load_removal_journal("org/model").unwrap().is_some());
3883
3884        let retried = store
3885            .begin_mutation("org/model")
3886            .unwrap()
3887            .remove(2)
3888            .unwrap();
3889
3890        assert_eq!(retried.artifact_kind, ManagedArtifactKind::Directory);
3891        assert!(store.load_removal_journal("org/model").unwrap().is_none());
3892        assert!(store.load_receipt("org/model").unwrap().is_none());
3893        assert!(no_quarantine_left(&models));
3894        // The parked copy is untouched: CAR never followed it out of the root.
3895        assert_eq!(std::fs::read(parked.join("weights")).unwrap(), b"owned");
3896    }
3897}