Skip to main content

rustferry_github/
snapshot.rs

1//! Operation-scoped Git snapshot identity and canonical object-graph contracts.
2
3use std::error::Error;
4use std::fmt;
5use std::fs::{self, File, OpenOptions};
6use std::io::{Read as _, Seek as _, Write as _};
7use std::path::{Path, PathBuf};
8use std::str::FromStr as _;
9
10use rustferry_core::{
11    DirectoryFilesystemIdentity, RegularFileFilesystemIdentity, RetainedDirectoryIdentity,
12    RetainedRegularFileIdentity, regular_file_identity_from_file,
13};
14use rustferry_remote::{
15    GIT_SNAPSHOT_ARCHIVE_PATH, GIT_SNAPSHOT_DESCRIPTOR_PATH, GitSnapshotDescriptor,
16    IosDeviceBuildRequest, MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES, SourceArchive,
17    SourceBundleDescriptor, canonical_git_snapshot_descriptor_bytes, git_snapshot_archive_limits,
18    git_snapshot_ref,
19};
20use serde::{Deserialize, Deserializer, Serialize};
21use sha2::{Digest as _, Sha256};
22
23/// Schema version of the compact private staging record.
24pub const GIT_SNAPSHOT_STAGE_SCHEMA_VERSION: u32 = 1;
25/// Schema version of the path-free durable stage locator.
26pub const GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION: u32 = 1;
27/// Schema version of the six-object Git graph identity.
28pub const GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION: u32 = 1;
29/// Private local namespace retaining snapshot objects across retry lineages.
30pub const GIT_SNAPSHOT_KEEPALIVE_REF_PREFIX: &str = "refs/rustferry/goal3/keepalive";
31/// Directory below the private Git isolation root containing operation stages.
32pub const GIT_SNAPSHOT_STAGE_DIRECTORY: &str = "snapshots";
33/// Create-only staged archive filename.
34pub const GIT_SNAPSHOT_STAGE_ARCHIVE_FILE: &str = "source.zip";
35/// Create-only staged descriptor filename.
36pub const GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE: &str = "source.json";
37/// Create-only compact stage metadata filename.
38pub const GIT_SNAPSHOT_STAGE_METADATA_FILE: &str = "stage.json";
39/// Maximum canonical compact stage metadata size.
40pub const MAX_GIT_SNAPSHOT_STAGE_BYTES: u64 = MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES + 64 * 1024;
41/// Maximum complete operation stages inspected by one bounded recovery discovery.
42pub const MAX_GIT_SNAPSHOT_DISCOVERY_STAGES: usize = 16;
43
44const SNAPSHOT_ACTOR_NAME: &str = "ShiroKSH";
45const SNAPSHOT_ACTOR_EMAIL: &str = "kushidashiro@gmail.com";
46
47/// Stable, path-free snapshot contract failure.
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
49pub enum GitSnapshotError {
50    /// Operation cannot derive one exact custom ref.
51    InvalidOperation,
52    /// Full source or keepalive ref differs from its exact namespace contract.
53    InvalidRef,
54    /// Git object ID is not a lowercase SHA-1 object name.
55    InvalidObjectId,
56    /// Tree entry mode or name is not canonical.
57    InvalidTreeEntry,
58    /// Commit timestamp is outside the deterministic supported range.
59    InvalidTimestamp,
60    /// Private Git database does not use GitHub-compatible SHA-1 object IDs.
61    UnsupportedObjectFormat,
62    /// Staging metadata or its descriptor binding is inconsistent.
63    InvalidStage,
64    /// Imported or recomputed objects differ from the staged six-object graph.
65    ObjectGraphMismatch,
66    /// Canonical JSON could not be encoded or decoded.
67    InvalidEncoding,
68    /// Canonical staging metadata exceeds its fixed bound.
69    StageTooLarge,
70    /// Operation stage already exists and cannot be replaced.
71    StageAlreadyExists,
72    /// Snapshot-store membership changed while one bounded recovery scan was in progress.
73    DiscoveryChanged,
74}
75
76impl fmt::Display for GitSnapshotError {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let message = match self {
79            Self::InvalidOperation => "Git snapshot operation is invalid",
80            Self::InvalidRef => "Git snapshot ref is invalid",
81            Self::InvalidObjectId => "Git snapshot object ID is invalid",
82            Self::InvalidTreeEntry => "Git snapshot tree entry is invalid",
83            Self::InvalidTimestamp => "Git snapshot commit timestamp is invalid",
84            Self::UnsupportedObjectFormat => "Git snapshot database does not use SHA-1 object IDs",
85            Self::InvalidStage => "Git snapshot stage is invalid",
86            Self::ObjectGraphMismatch => "Git snapshot object graph does not match",
87            Self::InvalidEncoding => "Git snapshot metadata encoding is invalid",
88            Self::StageTooLarge => "Git snapshot stage metadata exceeds its size limit",
89            Self::StageAlreadyExists => "Git snapshot stage already exists",
90            Self::DiscoveryChanged => "Git snapshot recovery discovery changed during inspection",
91        };
92        formatter.write_str(message)
93    }
94}
95
96impl Error for GitSnapshotError {}
97
98/// Exact source custom ref for one snapshot operation.
99#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
100#[serde(transparent)]
101pub struct GitSnapshotSourceRef(String);
102
103impl GitSnapshotSourceRef {
104    /// Derive the only accepted source ref for `operation_id`.
105    ///
106    /// # Errors
107    ///
108    /// Returns [`GitSnapshotError::InvalidOperation`] for a non-ref-safe operation identifier.
109    pub fn for_operation(operation_id: &str) -> Result<Self, GitSnapshotError> {
110        git_snapshot_ref(operation_id)
111            .map(Self)
112            .map_err(|_| GitSnapshotError::InvalidOperation)
113    }
114
115    /// Return the exact full ref string.
116    pub fn as_str(&self) -> &str {
117        &self.0
118    }
119}
120
121impl<'de> Deserialize<'de> for GitSnapshotSourceRef {
122    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
123    where
124        D: Deserializer<'de>,
125    {
126        let value = String::deserialize(deserializer)?;
127        let operation_id = value
128            .strip_prefix("refs/rustferry/goal3/snapshots/")
129            .ok_or_else(|| serde::de::Error::custom("invalid Git snapshot source ref"))?;
130        let parsed = Self::for_operation(operation_id)
131            .map_err(|_| serde::de::Error::custom("invalid Git snapshot source ref"))?;
132        if parsed.as_str() != value {
133            return Err(serde::de::Error::custom(
134                "non-canonical Git snapshot source ref",
135            ));
136        }
137        Ok(parsed)
138    }
139}
140
141/// Exact local keepalive ref for one snapshot operation.
142#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
143#[serde(transparent)]
144pub struct GitSnapshotKeepaliveRef(String);
145
146impl GitSnapshotKeepaliveRef {
147    /// Derive the only accepted keepalive ref for `operation_id`.
148    ///
149    /// # Errors
150    ///
151    /// Returns [`GitSnapshotError::InvalidOperation`] for a non-ref-safe operation identifier.
152    pub fn for_operation(operation_id: &str) -> Result<Self, GitSnapshotError> {
153        git_snapshot_ref(operation_id).map_err(|_| GitSnapshotError::InvalidOperation)?;
154        Ok(Self(format!(
155            "{GIT_SNAPSHOT_KEEPALIVE_REF_PREFIX}/{operation_id}"
156        )))
157    }
158
159    /// Return the exact full ref string.
160    pub fn as_str(&self) -> &str {
161        &self.0
162    }
163}
164
165impl<'de> Deserialize<'de> for GitSnapshotKeepaliveRef {
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        let value = String::deserialize(deserializer)?;
171        let operation_id = value
172            .strip_prefix(&format!("{GIT_SNAPSHOT_KEEPALIVE_REF_PREFIX}/"))
173            .ok_or_else(|| serde::de::Error::custom("invalid Git snapshot keepalive ref"))?;
174        let parsed = Self::for_operation(operation_id)
175            .map_err(|_| serde::de::Error::custom("invalid Git snapshot keepalive ref"))?;
176        if parsed.as_str() != value {
177            return Err(serde::de::Error::custom(
178                "non-canonical Git snapshot keepalive ref",
179            ));
180        }
181        Ok(parsed)
182    }
183}
184
185/// Exact lowercase 40-hex Git SHA-1 object identifier.
186#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
187#[serde(transparent)]
188pub struct GitSha1ObjectId(String);
189
190impl GitSha1ObjectId {
191    /// Validate and retain one GitHub-compatible SHA-1 object identifier.
192    ///
193    /// # Errors
194    ///
195    /// Returns [`GitSnapshotError::InvalidObjectId`] unless the value is exactly 40 lowercase
196    /// hexadecimal characters.
197    pub fn new(value: impl Into<String>) -> Result<Self, GitSnapshotError> {
198        let value = value.into();
199        if value.len() != 40
200            || !value
201                .bytes()
202                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
203        {
204            return Err(GitSnapshotError::InvalidObjectId);
205        }
206        Ok(Self(value))
207    }
208
209    /// Return the lowercase hexadecimal object name.
210    pub fn as_str(&self) -> &str {
211        &self.0
212    }
213
214    fn raw(&self) -> Result<[u8; 20], GitSnapshotError> {
215        let decoded = hex::decode(&self.0).map_err(|_| GitSnapshotError::InvalidObjectId)?;
216        decoded
217            .try_into()
218            .map_err(|_| GitSnapshotError::InvalidObjectId)
219    }
220}
221
222impl<'de> Deserialize<'de> for GitSha1ObjectId {
223    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224    where
225        D: Deserializer<'de>,
226    {
227        Self::new(String::deserialize(deserializer)?)
228            .map_err(|_| serde::de::Error::custom("invalid Git SHA-1 object ID"))
229    }
230}
231
232/// Exact Git object type accepted by snapshot hash/import callbacks.
233#[derive(Clone, Copy, Debug, Eq, PartialEq)]
234pub enum GitSnapshotObjectKind {
235    /// Raw source archive or canonical descriptor bytes.
236    Blob,
237    /// One of the three canonical nested trees.
238    Tree,
239    /// The canonical parentless commit.
240    Commit,
241}
242
243impl GitSnapshotObjectKind {
244    /// Fixed value for `git hash-object -t` and `git cat-file`.
245    pub const fn git_type(self) -> &'static str {
246        match self {
247            Self::Blob => "blob",
248            Self::Tree => "tree",
249            Self::Commit => "commit",
250        }
251    }
252}
253
254/// Derive one operation's strict stage directory below a private isolation root.
255///
256/// # Errors
257///
258/// Returns a typed failure for a relative root or unsafe operation identifier.
259pub fn git_snapshot_stage_directory(
260    isolation_root: &Path,
261    operation_id: &str,
262) -> Result<PathBuf, GitSnapshotError> {
263    if !isolation_root.is_absolute() {
264        return Err(GitSnapshotError::InvalidStage);
265    }
266    GitSnapshotSourceRef::for_operation(operation_id)?;
267    Ok(isolation_root
268        .join(GIT_SNAPSHOT_STAGE_DIRECTORY)
269        .join(operation_id))
270}
271
272/// Path-free, strictly verified complete stage found by read-only recovery discovery.
273#[derive(Clone, Debug, PartialEq)]
274pub(crate) struct GitSnapshotDiscoveredStageV1 {
275    pub(crate) locator: GitSnapshotStageLocatorV1,
276    pub(crate) stage: GitSnapshotStageV1,
277    pub(crate) descriptor: GitSnapshotDescriptor,
278}
279
280/// Enumerate complete private stages without adopting, deleting, or mutating them.
281///
282/// Enumeration is deterministic and bounded. Any partial stage, invalid operation entry, unknown
283/// store entry, canonical-byte mismatch, rebound identity, or cap overflow fails the whole read so
284/// callers cannot silently skip recovery-required state.
285pub(crate) fn discover_complete_git_snapshot_stages(
286    isolation_root: &Path,
287) -> Result<Vec<GitSnapshotDiscoveredStageV1>, GitSnapshotError> {
288    discover_complete_git_snapshot_stages_with_hook(isolation_root, || {})
289}
290
291fn discover_complete_git_snapshot_stages_with_hook(
292    isolation_root: &Path,
293    after_candidate_validation: impl FnOnce(),
294) -> Result<Vec<GitSnapshotDiscoveredStageV1>, GitSnapshotError> {
295    let isolation = GitSnapshotDirectoryGuard::open(isolation_root)?;
296    let snapshots_path = isolation.path().join(GIT_SNAPSHOT_STAGE_DIRECTORY);
297    match fs::symlink_metadata(&snapshots_path) {
298        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
299            isolation.verify()?;
300            return Ok(Vec::new());
301        }
302        Err(_) => return Err(GitSnapshotError::InvalidStage),
303        Ok(_) => {}
304    }
305    let snapshots = GitSnapshotDirectoryGuard::open(&snapshots_path)?;
306    let operations = read_snapshot_operation_entries(&snapshots_path, false)?;
307    isolation.verify()?;
308    snapshots.verify()?;
309    let mut discovered = Vec::with_capacity(operations.len());
310    for operation_id in operations {
311        discovered.push(GitSnapshotStageDirectory::discover_complete(
312            isolation_root,
313            &operation_id,
314        )?);
315    }
316    after_candidate_validation();
317    let final_operations = read_snapshot_operation_entries(&snapshots_path, true)?;
318    let discovered_operations = discovered
319        .iter()
320        .map(|candidate| candidate.stage.operation_id.as_str())
321        .collect::<Vec<_>>();
322    if final_operations
323        .iter()
324        .map(String::as_str)
325        .collect::<Vec<_>>()
326        != discovered_operations
327    {
328        return Err(GitSnapshotError::DiscoveryChanged);
329    }
330    isolation.verify()?;
331    snapshots.verify()?;
332    Ok(discovered)
333}
334
335fn read_snapshot_operation_entries(
336    snapshots_path: &Path,
337    changed_on_overflow: bool,
338) -> Result<Vec<String>, GitSnapshotError> {
339    let mut operations = Vec::new();
340    for entry in fs::read_dir(snapshots_path).map_err(|_| GitSnapshotError::InvalidStage)? {
341        let entry = entry.map_err(|_| GitSnapshotError::InvalidStage)?;
342        let operation_id = entry
343            .file_name()
344            .into_string()
345            .map_err(|_| GitSnapshotError::InvalidStage)?;
346        GitSnapshotSourceRef::for_operation(&operation_id)?;
347        operations.push(operation_id);
348        if operations.len() > MAX_GIT_SNAPSHOT_DISCOVERY_STAGES {
349            return Err(if changed_on_overflow {
350                GitSnapshotError::DiscoveryChanged
351            } else {
352                GitSnapshotError::InvalidStage
353            });
354        }
355    }
356    operations.sort();
357    Ok(operations)
358}
359
360/// Path-free durable authority for reopening one exact completed stage.
361#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
362#[serde(deny_unknown_fields)]
363pub struct GitSnapshotStageLocatorV1 {
364    /// Locator schema version; currently `1`.
365    pub schema_version: u32,
366    /// Exact operation identifier used to derive the stage path.
367    pub operation_id: String,
368    /// Filesystem identity of the private Git isolation root.
369    pub isolation_root_identity: String,
370    /// Filesystem identity of the fixed `snapshots` store.
371    pub snapshots_store_identity: String,
372    /// Filesystem identity of this operation's create-only stage directory.
373    pub stage_directory_identity: String,
374    /// Filesystem identity of the create-only canonical `stage.json` record.
375    pub metadata_file_identity: String,
376}
377
378impl GitSnapshotStageLocatorV1 {
379    /// Validate the path-free identities and exact operation binding.
380    ///
381    /// # Errors
382    ///
383    /// Returns a typed failure for any schema, operation, or identity mismatch.
384    pub fn validate_for_operation(&self, operation_id: &str) -> Result<(), GitSnapshotError> {
385        if self.schema_version != GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION
386            || self.operation_id != operation_id
387            || GitSnapshotSourceRef::for_operation(operation_id).is_err()
388            || !valid_distinct_directory_identities(
389                &self.isolation_root_identity,
390                &self.snapshots_store_identity,
391                &self.stage_directory_identity,
392            )
393            || RegularFileFilesystemIdentity::from_str(&self.metadata_file_identity).is_err()
394        {
395            return Err(GitSnapshotError::InvalidStage);
396        }
397        Ok(())
398    }
399}
400
401/// Complete identity of the two blobs, three nested trees, and parentless commit.
402#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
403#[serde(deny_unknown_fields)]
404pub struct GitSnapshotObjectGraphV1 {
405    /// Graph schema version; currently `1`.
406    pub schema_version: u32,
407    /// Exact `source.zip` blob.
408    pub archive_blob: GitSha1ObjectId,
409    /// Exact `source.json` blob.
410    pub descriptor_blob: GitSha1ObjectId,
411    /// Leaf tree containing `source.json` and `source.zip`.
412    pub goal3_tree: GitSha1ObjectId,
413    /// Intermediate tree containing only the `goal3` tree.
414    pub rustferry_tree: GitSha1ObjectId,
415    /// Root tree containing only the `.rustferry` tree.
416    pub root_tree: GitSha1ObjectId,
417    /// Exact parentless snapshot commit.
418    pub commit: GitSha1ObjectId,
419}
420
421impl GitSnapshotObjectGraphV1 {
422    /// Require the supported graph schema.
423    ///
424    /// # Errors
425    ///
426    /// Returns [`GitSnapshotError::InvalidStage`] for an unsupported graph schema.
427    pub fn validate(&self) -> Result<(), GitSnapshotError> {
428        if self.schema_version != GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION {
429            return Err(GitSnapshotError::InvalidStage);
430        }
431        Ok(())
432    }
433
434    /// Require both imported blob IDs before recomputing the three trees and commit.
435    ///
436    /// The callback is deliberately process-neutral. Precomputation supplies an offline
437    /// `git hash-object` callback without `-w`; import supplies the same callback with `-w`.
438    /// Both paths therefore share the canonical byte construction while the private Git runner
439    /// retains command, environment, and repository policy. A blob mismatch returns before the
440    /// callback can write any tree or commit.
441    ///
442    /// # Errors
443    ///
444    /// Returns the callback error, a canonical-encoding failure, or
445    /// [`GitSnapshotError::ObjectGraphMismatch`] converted into `E`.
446    pub fn verify_rehashed<E>(
447        &self,
448        archive_blob: GitSha1ObjectId,
449        descriptor_blob: GitSha1ObjectId,
450        operation_id: &str,
451        created_at_ms: u64,
452        hash_object: impl FnMut(GitSnapshotObjectKind, &[u8]) -> Result<GitSha1ObjectId, E>,
453    ) -> Result<(), E>
454    where
455        E: From<GitSnapshotError>,
456    {
457        if archive_blob != self.archive_blob || descriptor_blob != self.descriptor_blob {
458            return Err(E::from(GitSnapshotError::ObjectGraphMismatch));
459        }
460        let actual = complete_git_snapshot_object_graph(
461            archive_blob,
462            descriptor_blob,
463            operation_id,
464            created_at_ms,
465            hash_object,
466        )?;
467        if actual != *self {
468            return Err(E::from(GitSnapshotError::ObjectGraphMismatch));
469        }
470        Ok(())
471    }
472}
473
474/// Complete the exact six-object graph after the two potentially streamed blobs are hashed.
475///
476/// Callers must explicitly prove SHA-1 object format before hashing either blob. The callback is
477/// then invoked exactly three times for trees, leaf to root, and once for the parentless commit.
478///
479/// # Errors
480///
481/// Returns the callback error or a canonical-encoding failure converted into `E`.
482pub fn complete_git_snapshot_object_graph<E>(
483    archive_blob: GitSha1ObjectId,
484    descriptor_blob: GitSha1ObjectId,
485    operation_id: &str,
486    created_at_ms: u64,
487    mut hash_object: impl FnMut(GitSnapshotObjectKind, &[u8]) -> Result<GitSha1ObjectId, E>,
488) -> Result<GitSnapshotObjectGraphV1, E>
489where
490    E: From<GitSnapshotError>,
491{
492    let goal3_bytes =
493        canonical_goal3_tree_bytes(&descriptor_blob, &archive_blob).map_err(E::from)?;
494    let goal3_tree = hash_object(GitSnapshotObjectKind::Tree, &goal3_bytes)?;
495    let rustferry_bytes = canonical_rustferry_tree_bytes(&goal3_tree).map_err(E::from)?;
496    let rustferry_tree = hash_object(GitSnapshotObjectKind::Tree, &rustferry_bytes)?;
497    let root_bytes = canonical_root_tree_bytes(&rustferry_tree).map_err(E::from)?;
498    let root_tree = hash_object(GitSnapshotObjectKind::Tree, &root_bytes)?;
499    let commit_bytes =
500        canonical_parentless_snapshot_commit_bytes(&root_tree, operation_id, created_at_ms)
501            .map_err(E::from)?;
502    let commit = hash_object(GitSnapshotObjectKind::Commit, &commit_bytes)?;
503    Ok(GitSnapshotObjectGraphV1 {
504        schema_version: GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
505        archive_blob,
506        descriptor_blob,
507        goal3_tree,
508        rustferry_tree,
509        root_tree,
510        commit,
511    })
512}
513
514/// Compact create-only metadata that binds a private stage to its canonical graph.
515#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
516#[serde(deny_unknown_fields)]
517pub struct GitSnapshotStageV1 {
518    /// Stage schema version; currently `1`.
519    pub schema_version: u32,
520    /// Exact operation identifier.
521    pub operation_id: String,
522    /// Filesystem identity of the private Git isolation root.
523    pub isolation_root_identity: String,
524    /// Filesystem identity of the fixed `snapshots` store.
525    pub snapshots_store_identity: String,
526    /// Filesystem identity of this operation's create-only stage directory.
527    pub stage_directory_identity: String,
528    /// Exact configured public source repository.
529    pub source_repository: String,
530    /// Exact operation-derived source ref.
531    pub source_ref: GitSnapshotSourceRef,
532    /// Exact operation-derived local keepalive ref.
533    pub keepalive_ref: GitSnapshotKeepaliveRef,
534    /// Millisecond timestamp used to derive deterministic commit seconds.
535    pub source_created_at_ms: u64,
536    /// SHA-256 of the exact zero-write plan accepted by the caller.
537    pub consent_sha256: String,
538    /// Canonical request-template SHA-256 embedded in the descriptor.
539    pub request_template_sha256: String,
540    /// Canonical source-manifest SHA-256.
541    pub manifest_sha256: String,
542    /// Exact staged archive size and SHA-256.
543    pub archive: SourceArchive,
544    /// SHA-256 of the canonical descriptor bytes.
545    pub descriptor_sha256: String,
546    /// Secret-free final request whose revision is the exact parentless snapshot commit.
547    pub final_request: IosDeviceBuildRequest,
548    /// Filesystem identity of the sealed staged archive.
549    pub archive_file_identity: String,
550    /// Filesystem identity of the sealed staged descriptor.
551    pub descriptor_file_identity: String,
552    /// Complete six-object Git graph identity.
553    pub graph: GitSnapshotObjectGraphV1,
554}
555
556impl GitSnapshotStageV1 {
557    /// Validate compact fields and their exact canonical descriptor binding.
558    ///
559    /// # Errors
560    ///
561    /// Returns a typed snapshot failure for any schema, identity, digest, ref, descriptor, or
562    /// object-graph mismatch.
563    pub fn validate_for_descriptor(
564        &self,
565        descriptor: &GitSnapshotDescriptor,
566    ) -> Result<(), GitSnapshotError> {
567        if self.schema_version != GIT_SNAPSHOT_STAGE_SCHEMA_VERSION
568            || self.source_created_at_ms / 1_000 > i64::MAX as u64
569            || !valid_distinct_directory_identities(
570                &self.isolation_root_identity,
571                &self.snapshots_store_identity,
572                &self.stage_directory_identity,
573            )
574            || !is_lower_sha256(&self.consent_sha256)
575            || !is_lower_sha256(&self.request_template_sha256)
576            || !is_lower_sha256(&self.manifest_sha256)
577            || !is_lower_sha256(&self.descriptor_sha256)
578            || self.source_ref != GitSnapshotSourceRef::for_operation(&self.operation_id)?
579            || self.keepalive_ref != GitSnapshotKeepaliveRef::for_operation(&self.operation_id)?
580            || self.source_ref.as_str() != descriptor.snapshot_ref
581            || self.source_repository != descriptor.source_repository
582            || self.request_template_sha256 != descriptor.request_template_sha256
583            || self.manifest_sha256 != descriptor.bundle.manifest.sha256
584            || self.archive != descriptor.bundle.archive
585            || RegularFileFilesystemIdentity::from_str(&self.archive_file_identity).is_err()
586            || RegularFileFilesystemIdentity::from_str(&self.descriptor_file_identity).is_err()
587            || self.archive_file_identity == self.descriptor_file_identity
588        {
589            return Err(GitSnapshotError::InvalidStage);
590        }
591        descriptor
592            .validate(git_snapshot_archive_limits())
593            .map_err(|_| GitSnapshotError::InvalidStage)?;
594        self.graph.validate()?;
595        descriptor
596            .validate_for_request(&self.final_request, git_snapshot_archive_limits())
597            .map_err(|_| GitSnapshotError::InvalidStage)?;
598        if self.final_request.operation_id != self.operation_id
599            || self.final_request.source_revision.as_deref() != Some(self.graph.commit.as_str())
600        {
601            return Err(GitSnapshotError::InvalidStage);
602        }
603        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(descriptor)
604            .map_err(|_| GitSnapshotError::InvalidStage)?;
605        if hex::encode(Sha256::digest(descriptor_bytes)) != self.descriptor_sha256 {
606            return Err(GitSnapshotError::InvalidStage);
607        }
608        Ok(())
609    }
610
611    /// Require exact binding to the already-durable final request and commit revision.
612    ///
613    /// # Errors
614    ///
615    /// Returns a typed failure for any final-request, descriptor, manifest, template, operation,
616    /// repository, ref, or commit mismatch.
617    pub fn validate_for_request(
618        &self,
619        descriptor: &GitSnapshotDescriptor,
620        request: &IosDeviceBuildRequest,
621    ) -> Result<(), GitSnapshotError> {
622        self.validate_for_descriptor(descriptor)?;
623        descriptor
624            .validate_for_request(request, git_snapshot_archive_limits())
625            .map_err(|_| GitSnapshotError::InvalidStage)?;
626        if request != &self.final_request
627            || request.source_revision.as_deref() != Some(self.graph.commit.as_str())
628        {
629            return Err(GitSnapshotError::InvalidStage);
630        }
631        Ok(())
632    }
633
634    /// Encode strict canonical pretty JSON with one trailing newline.
635    ///
636    /// # Errors
637    ///
638    /// Returns a typed snapshot failure when validation, encoding, or size enforcement fails.
639    pub fn canonical_bytes(
640        &self,
641        descriptor: &GitSnapshotDescriptor,
642    ) -> Result<Vec<u8>, GitSnapshotError> {
643        self.validate_for_descriptor(descriptor)?;
644        let mut bytes =
645            serde_json::to_vec_pretty(self).map_err(|_| GitSnapshotError::InvalidEncoding)?;
646        bytes.push(b'\n');
647        if bytes.len() as u64 > MAX_GIT_SNAPSHOT_STAGE_BYTES {
648            return Err(GitSnapshotError::StageTooLarge);
649        }
650        Ok(bytes)
651    }
652
653    /// Decode strict JSON and require full descriptor binding.
654    ///
655    /// # Errors
656    ///
657    /// Returns a typed snapshot failure for oversized, non-canonical, unknown, duplicate, or
658    /// descriptor-mismatched metadata.
659    pub fn decode(
660        bytes: &[u8],
661        descriptor: &GitSnapshotDescriptor,
662    ) -> Result<Self, GitSnapshotError> {
663        if bytes.len() as u64 > MAX_GIT_SNAPSHOT_STAGE_BYTES {
664            return Err(GitSnapshotError::StageTooLarge);
665        }
666        let stage: Self =
667            serde_json::from_slice(bytes).map_err(|_| GitSnapshotError::InvalidEncoding)?;
668        stage.validate_for_descriptor(descriptor)?;
669        if stage.canonical_bytes(descriptor)? != bytes {
670            return Err(GitSnapshotError::InvalidEncoding);
671        }
672        Ok(stage)
673    }
674
675    /// Reopen the exact staged descriptor and archive from this operation's strict private path.
676    ///
677    /// Both files must retain their persisted single-link filesystem identities. The descriptor
678    /// must have its exact canonical bytes and SHA-256; the archive must reproduce its exact size
679    /// and SHA-256. The returned descriptor has already been checked against every stage field.
680    ///
681    /// # Errors
682    ///
683    /// Returns a typed snapshot failure for any path, identity, size, digest, encoding, or binding
684    /// mismatch.
685    pub fn verify_staged_files(
686        &self,
687        isolation_root: &Path,
688    ) -> Result<GitSnapshotDescriptor, GitSnapshotError> {
689        let directories = GitSnapshotStageDirectory::open(isolation_root, &self.operation_id)?;
690        directories.require_stage_binding(self)?;
691        let stage_directory = directories.stage_directory.path();
692        if require_stage_entries(
693            stage_directory,
694            &[
695                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
696                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
697            ],
698        )
699        .is_err()
700            && require_stage_entries(
701                stage_directory,
702                &[
703                    GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
704                    GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
705                    GIT_SNAPSHOT_STAGE_METADATA_FILE,
706                ],
707            )
708            .is_err()
709        {
710            return Err(GitSnapshotError::InvalidStage);
711        }
712        let descriptor_path = stage_directory.join(GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE);
713        let archive_path = stage_directory.join(GIT_SNAPSHOT_STAGE_ARCHIVE_FILE);
714        let descriptor_identity =
715            RegularFileFilesystemIdentity::from_str(&self.descriptor_file_identity)
716                .map_err(|_| GitSnapshotError::InvalidStage)?;
717        let descriptor_bytes = read_bound_file(
718            &descriptor_path,
719            &descriptor_identity,
720            MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES,
721        )?;
722        if hex::encode(Sha256::digest(&descriptor_bytes)) != self.descriptor_sha256 {
723            return Err(GitSnapshotError::InvalidStage);
724        }
725        let descriptor: GitSnapshotDescriptor = serde_json::from_slice(&descriptor_bytes)
726            .map_err(|_| GitSnapshotError::InvalidEncoding)?;
727        let canonical = canonical_git_snapshot_descriptor_bytes(&descriptor)
728            .map_err(|_| GitSnapshotError::InvalidStage)?;
729        if canonical != descriptor_bytes {
730            return Err(GitSnapshotError::InvalidEncoding);
731        }
732        self.validate_for_descriptor(&descriptor)?;
733
734        let archive_identity = RegularFileFilesystemIdentity::from_str(&self.archive_file_identity)
735            .map_err(|_| GitSnapshotError::InvalidStage)?;
736        let archive = describe_bound_file(
737            &archive_path,
738            &archive_identity,
739            git_snapshot_archive_limits().max_archive_size,
740        )?;
741        if archive != self.archive {
742            return Err(GitSnapshotError::InvalidStage);
743        }
744        directories.verify_directory_bindings()?;
745        Ok(descriptor)
746    }
747}
748
749/// Retained create-only directory for assembling one private operation stage.
750pub struct GitSnapshotStageDirectory {
751    operation_id: String,
752    isolation_root: GitSnapshotDirectoryGuard,
753    snapshots_store: GitSnapshotDirectoryGuard,
754    stage_directory: GitSnapshotDirectoryGuard,
755}
756
757#[derive(Clone, Copy, Debug, Eq, PartialEq)]
758enum ExactRetryStageEntries {
759    Empty,
760    Archive,
761    ArchiveAndDescriptor,
762    Complete,
763}
764
765impl fmt::Debug for GitSnapshotStageDirectory {
766    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
767        formatter
768            .debug_struct("GitSnapshotStageDirectory")
769            .field("operation_id", &self.operation_id)
770            .field("isolation_root_identity", self.isolation_root.identity())
771            .field("snapshots_store_identity", self.snapshots_store.identity())
772            .field("stage_directory_identity", self.stage_directory.identity())
773            .finish_non_exhaustive()
774    }
775}
776
777impl GitSnapshotStageDirectory {
778    /// Create one new operation stage below the strict private `snapshots` store.
779    ///
780    /// The fixed store is created once when absent. The operation directory itself is always
781    /// create-only: an existing complete or partial stage is never reused by this path.
782    ///
783    /// # Errors
784    ///
785    /// Returns a typed failure for an unsafe operation, non-private or rebound directory, failed
786    /// durability barrier, or an existing operation stage.
787    pub fn create(isolation_root: &Path, operation_id: &str) -> Result<Self, GitSnapshotError> {
788        GitSnapshotSourceRef::for_operation(operation_id)?;
789        let isolation_root = GitSnapshotDirectoryGuard::open(isolation_root)?;
790        let snapshots_store_path = isolation_root.path().join(GIT_SNAPSHOT_STAGE_DIRECTORY);
791        let snapshots_store = match fs::symlink_metadata(&snapshots_store_path) {
792            Ok(_) => GitSnapshotDirectoryGuard::open(&snapshots_store_path)?,
793            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
794                create_private_directory_create_new(&snapshots_store_path, &isolation_root, true)?;
795                GitSnapshotDirectoryGuard::open(&snapshots_store_path)?
796            }
797            Err(_) => return Err(GitSnapshotError::InvalidStage),
798        };
799        let stage_path = snapshots_store_path.join(operation_id);
800        if fs::symlink_metadata(&stage_path).is_ok() {
801            return Err(GitSnapshotError::StageAlreadyExists);
802        }
803        create_private_directory_create_new(&stage_path, &snapshots_store, false)?;
804        let stage_directory = GitSnapshotDirectoryGuard::open(&stage_path)?;
805        require_stage_entries(stage_directory.path(), &[])?;
806        let result = Self {
807            operation_id: operation_id.to_owned(),
808            isolation_root,
809            snapshots_store,
810            stage_directory,
811        };
812        result.verify_directory_bindings()?;
813        Ok(result)
814    }
815
816    /// Create or reopen one exact-retry stage under durable child-lineage authority.
817    ///
818    /// This narrow recovery path accepts only the four ordered durable prefixes produced by the
819    /// exact-retry writer: empty, archive, archive plus descriptor, or the complete stage. Ordinary
820    /// initial-snapshot creation and adoption remain strictly create-only.
821    pub(crate) fn open_or_create_exact_retry(
822        isolation_root: &Path,
823        operation_id: &str,
824    ) -> Result<Self, GitSnapshotError> {
825        let stage = match Self::create(isolation_root, operation_id) {
826            Ok(stage) => stage,
827            Err(GitSnapshotError::StageAlreadyExists) => Self::open(isolation_root, operation_id)?,
828            Err(error) => return Err(error),
829        };
830        stage.exact_retry_entries()?;
831        Ok(stage)
832    }
833
834    /// Exact create-only archive destination consumed by the deterministic source archiver.
835    pub fn archive_path(&self) -> PathBuf {
836        self.stage_directory
837            .path()
838            .join(GIT_SNAPSHOT_STAGE_ARCHIVE_FILE)
839    }
840
841    /// Create and durably publish an exact already-verified archive byte sequence.
842    ///
843    /// This is used only for exact-source retry after the parent blob was re-read and verified
844    /// from the private Git object database. Ordinary workspace capture continues to use the
845    /// deterministic streaming source archiver.
846    ///
847    /// # Errors
848    ///
849    /// Returns a typed failure for size/SHA mismatch, archive-limit overflow, an existing file,
850    /// unsafe filesystem identity, or failed file/directory durability.
851    pub fn write_archive_bytes_create_new(
852        &self,
853        bytes: &[u8],
854        expected: &SourceArchive,
855    ) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
856        let limits = git_snapshot_archive_limits();
857        if bytes.len() as u64 != expected.size
858            || expected.size > limits.max_archive_size
859            || hex::encode(Sha256::digest(bytes)) != expected.sha256
860        {
861            return Err(GitSnapshotError::InvalidStage);
862        }
863        self.verify_directory_bindings()?;
864        let path = self.archive_path();
865        let identity = write_private_file_create_new(&path, bytes, &self.stage_directory)?;
866        let described = describe_bound_file(&path, &identity, limits.max_archive_size)?;
867        if described != *expected {
868            return Err(GitSnapshotError::InvalidStage);
869        }
870        self.verify_directory_bindings()?;
871        Ok(identity)
872    }
873
874    /// Create or reproduce the exact retained archive at the retry stage frontier.
875    pub(crate) fn write_or_verify_archive_bytes_exact_retry(
876        &self,
877        bytes: &[u8],
878        expected: &SourceArchive,
879    ) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
880        let limits = git_snapshot_archive_limits();
881        if bytes.len() as u64 != expected.size
882            || expected.size > limits.max_archive_size
883            || hex::encode(Sha256::digest(bytes)) != expected.sha256
884        {
885            return Err(GitSnapshotError::InvalidStage);
886        }
887        let entries = self.exact_retry_entries()?;
888        let recover_frontier = match entries {
889            ExactRetryStageEntries::Empty | ExactRetryStageEntries::Archive => true,
890            ExactRetryStageEntries::ArchiveAndDescriptor | ExactRetryStageEntries::Complete => {
891                false
892            }
893        };
894        reconcile_exact_retry_file(
895            &self.stage_directory,
896            GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
897            bytes,
898            limits.max_archive_size,
899            recover_frontier,
900        )
901    }
902
903    /// Exact create-only descriptor destination.
904    pub fn descriptor_path(&self) -> PathBuf {
905        self.stage_directory
906            .path()
907            .join(GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE)
908    }
909
910    /// Root identity persisted into canonical stage bytes and the durable locator.
911    pub fn isolation_root_identity(&self) -> &str {
912        self.isolation_root.identity().as_str()
913    }
914
915    /// Snapshot-store identity persisted into canonical stage bytes and the durable locator.
916    pub fn snapshots_store_identity(&self) -> &str {
917        self.snapshots_store.identity().as_str()
918    }
919
920    /// Operation-stage identity persisted into canonical stage bytes and the durable locator.
921    pub fn stage_directory_identity(&self) -> &str {
922        self.stage_directory.identity().as_str()
923    }
924
925    /// Create and durably publish the exact canonical descriptor bytes.
926    ///
927    /// # Errors
928    ///
929    /// Returns a typed failure if the descriptor cannot be encoded, the destination already
930    /// exists, the private stage changed, or file/directory synchronization fails.
931    pub fn write_descriptor_create_new(
932        &self,
933        descriptor: &GitSnapshotDescriptor,
934    ) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
935        self.verify_directory_bindings()?;
936        let bytes = canonical_git_snapshot_descriptor_bytes(descriptor)
937            .map_err(|_| GitSnapshotError::InvalidStage)?;
938        let path = self.descriptor_path();
939        let identity = write_private_file_create_new(&path, &bytes, &self.stage_directory)?;
940        self.verify_directory_bindings()?;
941        Ok(identity)
942    }
943
944    /// Create or reproduce the exact canonical descriptor at the retry stage frontier.
945    pub(crate) fn write_or_verify_descriptor_exact_retry(
946        &self,
947        descriptor: &GitSnapshotDescriptor,
948    ) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
949        let bytes = canonical_git_snapshot_descriptor_bytes(descriptor)
950            .map_err(|_| GitSnapshotError::InvalidStage)?;
951        let entries = self.exact_retry_entries()?;
952        let recover_frontier = match entries {
953            ExactRetryStageEntries::Archive | ExactRetryStageEntries::ArchiveAndDescriptor => true,
954            ExactRetryStageEntries::Complete => false,
955            ExactRetryStageEntries::Empty => return Err(GitSnapshotError::InvalidStage),
956        };
957        reconcile_exact_retry_file(
958            &self.stage_directory,
959            GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
960            &bytes,
961            MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES,
962            recover_frontier,
963        )
964    }
965
966    /// Synchronize and bind the already create-only deterministic archive.
967    ///
968    /// The archive creator remains responsible for create-only ZIP construction. This barrier
969    /// reopens its exact single-link identity, synchronizes it, and reproduces its size/SHA-256
970    /// while the identity guard remains live.
971    ///
972    /// # Errors
973    ///
974    /// Returns a typed failure for a missing, rebound, oversized, unsynchronized, or digest-
975    /// mismatched archive.
976    pub fn seal_archive(
977        &self,
978        expected: &SourceArchive,
979    ) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
980        self.verify_directory_bindings()?;
981        let path = self.archive_path();
982        let sync_file = OpenOptions::new()
983            .read(true)
984            .write(true)
985            .open(&path)
986            .map_err(|_| GitSnapshotError::InvalidStage)?;
987        let synced_identity = regular_file_identity_from_file(&sync_file)
988            .map_err(|_| GitSnapshotError::InvalidStage)?;
989        sync_file
990            .sync_all()
991            .map_err(|_| GitSnapshotError::InvalidStage)?;
992        drop(sync_file);
993        let retained =
994            RetainedRegularFileIdentity::open(&path).map_err(|_| GitSnapshotError::InvalidStage)?;
995        let identity = retained.identity().clone();
996        if identity != synced_identity {
997            return Err(GitSnapshotError::InvalidStage);
998        }
999        let mut file = File::open(&path).map_err(|_| GitSnapshotError::InvalidStage)?;
1000        if regular_file_identity_from_file(&file).map_err(|_| GitSnapshotError::InvalidStage)?
1001            != identity
1002        {
1003            return Err(GitSnapshotError::InvalidStage);
1004        }
1005        let actual = describe_open_file(&mut file, git_snapshot_archive_limits().max_archive_size)?;
1006        retained
1007            .verify_path(&path)
1008            .map_err(|_| GitSnapshotError::InvalidStage)?;
1009        if &actual != expected {
1010            return Err(GitSnapshotError::InvalidStage);
1011        }
1012        self.stage_directory.sync_metadata()?;
1013        self.verify_directory_bindings()?;
1014        Ok(identity)
1015    }
1016
1017    /// Reopen the exact archive and descriptor as retained no-write precompute inputs.
1018    ///
1019    /// Callers must probe the fixed private Git database for exact SHA-1 object format before
1020    /// hashing either returned input. The archive is supplied as an already-open reader; the
1021    /// descriptor is supplied as exact canonical bytes. No file, Git object, ref, or network state
1022    /// is mutated by this method.
1023    ///
1024    /// # Errors
1025    ///
1026    /// Returns a typed failure for any directory, entry-set, file identity, digest, or canonical
1027    /// descriptor mismatch.
1028    pub fn precompute_inputs(
1029        &self,
1030        archive_file_identity: &RegularFileFilesystemIdentity,
1031        descriptor_file_identity: &RegularFileFilesystemIdentity,
1032        archive: &SourceArchive,
1033        descriptor: &GitSnapshotDescriptor,
1034    ) -> Result<GitSnapshotPrecomputeInputs, GitSnapshotError> {
1035        self.verify_directory_bindings()?;
1036        require_stage_entries(
1037            self.stage_directory.path(),
1038            &[
1039                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1040                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1041            ],
1042        )?;
1043        let directories = Self::open(self.isolation_root.path(), &self.operation_id)?;
1044        if directories.isolation_root_identity() != self.isolation_root_identity()
1045            || directories.snapshots_store_identity() != self.snapshots_store_identity()
1046            || directories.stage_directory_identity() != self.stage_directory_identity()
1047        {
1048            return Err(GitSnapshotError::InvalidStage);
1049        }
1050        GitSnapshotPrecomputeInputs::open(
1051            directories,
1052            archive_file_identity,
1053            descriptor_file_identity,
1054            archive,
1055            descriptor,
1056        )
1057    }
1058
1059    /// Create and durably publish canonical `stage.json`, returning its path-free locator.
1060    ///
1061    /// The metadata destination is create-only. Success requires exact request/descriptor/stage
1062    /// binding and reproduces both staged file identities and digests before the final directory
1063    /// metadata barrier.
1064    ///
1065    /// # Errors
1066    ///
1067    /// Returns a typed failure for any stage, request, identity, digest, encoding, unexpected
1068    /// entry, create-only, or durability mismatch.
1069    pub fn publish_metadata_create_new(
1070        &self,
1071        stage: &GitSnapshotStageV1,
1072        descriptor: &GitSnapshotDescriptor,
1073        final_request: &IosDeviceBuildRequest,
1074    ) -> Result<GitSnapshotStageLocatorV1, GitSnapshotError> {
1075        self.verify_directory_bindings()?;
1076        self.require_stage_binding(stage)?;
1077        stage.validate_for_request(descriptor, final_request)?;
1078        require_stage_entries(
1079            self.stage_directory.path(),
1080            &[
1081                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1082                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1083            ],
1084        )?;
1085        let archive_identity =
1086            RegularFileFilesystemIdentity::from_str(&stage.archive_file_identity)
1087                .map_err(|_| GitSnapshotError::InvalidStage)?;
1088        let descriptor_identity =
1089            RegularFileFilesystemIdentity::from_str(&stage.descriptor_file_identity)
1090                .map_err(|_| GitSnapshotError::InvalidStage)?;
1091        let mut payloads = self.precompute_inputs(
1092            &archive_identity,
1093            &descriptor_identity,
1094            &stage.archive,
1095            descriptor,
1096        )?;
1097        payloads.verify_contents()?;
1098
1099        let bytes = stage.canonical_bytes(descriptor)?;
1100        let metadata_path = self
1101            .stage_directory
1102            .path()
1103            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1104        let metadata_identity =
1105            write_private_file_create_new(&metadata_path, &bytes, &self.stage_directory)?;
1106        require_stage_entries(
1107            self.stage_directory.path(),
1108            &[
1109                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1110                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1111                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1112            ],
1113        )?;
1114        payloads.verify_payload_files()?;
1115        self.verify_directory_bindings()?;
1116        let locator = GitSnapshotStageLocatorV1 {
1117            schema_version: GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION,
1118            operation_id: self.operation_id.clone(),
1119            isolation_root_identity: self.isolation_root_identity().to_owned(),
1120            snapshots_store_identity: self.snapshots_store_identity().to_owned(),
1121            stage_directory_identity: self.stage_directory_identity().to_owned(),
1122            metadata_file_identity: metadata_identity.to_string(),
1123        };
1124        locator.validate_for_operation(&self.operation_id)?;
1125        Ok(locator)
1126    }
1127
1128    /// Create or exactly adopt canonical retry metadata after both payloads are durable.
1129    pub(crate) fn publish_or_verify_metadata_exact_retry(
1130        &self,
1131        stage: &GitSnapshotStageV1,
1132        descriptor: &GitSnapshotDescriptor,
1133        final_request: &IosDeviceBuildRequest,
1134    ) -> Result<GitSnapshotStageLocatorV1, GitSnapshotError> {
1135        self.verify_directory_bindings()?;
1136        self.require_stage_binding(stage)?;
1137        stage.validate_for_request(descriptor, final_request)?;
1138        match self.exact_retry_entries()? {
1139            ExactRetryStageEntries::ArchiveAndDescriptor => {
1140                return self.publish_metadata_create_new(stage, descriptor, final_request);
1141            }
1142            ExactRetryStageEntries::Complete => {}
1143            ExactRetryStageEntries::Empty | ExactRetryStageEntries::Archive => {
1144                return Err(GitSnapshotError::InvalidStage);
1145            }
1146        }
1147        let expected = stage.canonical_bytes(descriptor)?;
1148        match reconcile_exact_retry_file(
1149            &self.stage_directory,
1150            GIT_SNAPSHOT_STAGE_METADATA_FILE,
1151            &expected,
1152            MAX_GIT_SNAPSHOT_STAGE_BYTES,
1153            true,
1154        ) {
1155            Ok(metadata_identity) => {
1156                let locator = GitSnapshotStageLocatorV1 {
1157                    schema_version: GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION,
1158                    operation_id: self.operation_id.clone(),
1159                    isolation_root_identity: self.isolation_root_identity().to_owned(),
1160                    snapshots_store_identity: self.snapshots_store_identity().to_owned(),
1161                    stage_directory_identity: self.stage_directory_identity().to_owned(),
1162                    metadata_file_identity: metadata_identity.to_string(),
1163                };
1164                let inputs = GitSnapshotImportInputs::load(
1165                    self.isolation_root.path(),
1166                    &locator,
1167                    final_request,
1168                )?;
1169                if inputs.stage() != stage || inputs.descriptor() != descriptor {
1170                    return Err(GitSnapshotError::InvalidStage);
1171                }
1172                drop(inputs);
1173                self.verify_directory_bindings()?;
1174                require_stage_entries(
1175                    self.stage_directory.path(),
1176                    &[
1177                        GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1178                        GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1179                        GIT_SNAPSHOT_STAGE_METADATA_FILE,
1180                    ],
1181                )?;
1182                Ok(locator)
1183            }
1184            Err(error) => Err(error),
1185        }
1186    }
1187
1188    fn open(isolation_root: &Path, operation_id: &str) -> Result<Self, GitSnapshotError> {
1189        GitSnapshotSourceRef::for_operation(operation_id)?;
1190        let isolation_root = GitSnapshotDirectoryGuard::open(isolation_root)?;
1191        let snapshots_store = GitSnapshotDirectoryGuard::open(
1192            &isolation_root.path().join(GIT_SNAPSHOT_STAGE_DIRECTORY),
1193        )?;
1194        let stage_directory =
1195            GitSnapshotDirectoryGuard::open(&snapshots_store.path().join(operation_id))?;
1196        let result = Self {
1197            operation_id: operation_id.to_owned(),
1198            isolation_root,
1199            snapshots_store,
1200            stage_directory,
1201        };
1202        result.verify_directory_bindings()?;
1203        Ok(result)
1204    }
1205
1206    fn discover_complete(
1207        isolation_root: &Path,
1208        operation_id: &str,
1209    ) -> Result<GitSnapshotDiscoveredStageV1, GitSnapshotError> {
1210        let directories = Self::open(isolation_root, operation_id)?;
1211        require_stage_entries(
1212            directories.stage_directory.path(),
1213            &[
1214                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1215                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1216                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1217            ],
1218        )?;
1219        let metadata_path = directories
1220            .stage_directory
1221            .path()
1222            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1223        let (metadata_guard, mut metadata_file) = open_unbound_file(&metadata_path)?;
1224        let metadata_bytes = read_open_file(&mut metadata_file, MAX_GIT_SNAPSHOT_STAGE_BYTES)?;
1225        metadata_guard
1226            .verify_path(&metadata_path)
1227            .map_err(|_| GitSnapshotError::InvalidStage)?;
1228        let unverified_stage: GitSnapshotStageV1 = serde_json::from_slice(&metadata_bytes)
1229            .map_err(|_| GitSnapshotError::InvalidEncoding)?;
1230        directories.require_stage_binding(&unverified_stage)?;
1231        let descriptor_identity =
1232            RegularFileFilesystemIdentity::from_str(&unverified_stage.descriptor_file_identity)
1233                .map_err(|_| GitSnapshotError::InvalidStage)?;
1234        let descriptor_bytes = read_bound_file(
1235            &directories.descriptor_path(),
1236            &descriptor_identity,
1237            MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES,
1238        )?;
1239        let descriptor: GitSnapshotDescriptor = serde_json::from_slice(&descriptor_bytes)
1240            .map_err(|_| GitSnapshotError::InvalidEncoding)?;
1241        if canonical_git_snapshot_descriptor_bytes(&descriptor)
1242            .map_err(|_| GitSnapshotError::InvalidStage)?
1243            != descriptor_bytes
1244        {
1245            return Err(GitSnapshotError::InvalidEncoding);
1246        }
1247        let stage = GitSnapshotStageV1::decode(&metadata_bytes, &descriptor)?;
1248        if stage != unverified_stage
1249            || metadata_guard.identity().as_str() == stage.archive_file_identity
1250            || metadata_guard.identity().as_str() == stage.descriptor_file_identity
1251        {
1252            return Err(GitSnapshotError::InvalidStage);
1253        }
1254        let locator = GitSnapshotStageLocatorV1 {
1255            schema_version: GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION,
1256            operation_id: operation_id.to_owned(),
1257            isolation_root_identity: directories.isolation_root_identity().to_owned(),
1258            snapshots_store_identity: directories.snapshots_store_identity().to_owned(),
1259            stage_directory_identity: directories.stage_directory_identity().to_owned(),
1260            metadata_file_identity: metadata_guard.identity().to_string(),
1261        };
1262        locator.validate_for_operation(operation_id)?;
1263        directories.verify_directory_bindings()?;
1264        metadata_guard
1265            .verify_path(&metadata_path)
1266            .map_err(|_| GitSnapshotError::InvalidStage)?;
1267        drop(metadata_file);
1268        drop(metadata_guard);
1269        drop(directories);
1270        let inputs = GitSnapshotImportInputs::load(isolation_root, &locator, &stage.final_request)?;
1271        if inputs.stage() != &stage || inputs.descriptor() != &descriptor {
1272            return Err(GitSnapshotError::InvalidStage);
1273        }
1274        drop(inputs);
1275        Ok(GitSnapshotDiscoveredStageV1 {
1276            locator,
1277            stage,
1278            descriptor,
1279        })
1280    }
1281
1282    fn exact_retry_entries(&self) -> Result<ExactRetryStageEntries, GitSnapshotError> {
1283        self.verify_directory_bindings()?;
1284        let entries = if require_stage_entries(self.stage_directory.path(), &[]).is_ok() {
1285            ExactRetryStageEntries::Empty
1286        } else if require_stage_entries(
1287            self.stage_directory.path(),
1288            &[GIT_SNAPSHOT_STAGE_ARCHIVE_FILE],
1289        )
1290        .is_ok()
1291        {
1292            ExactRetryStageEntries::Archive
1293        } else if require_stage_entries(
1294            self.stage_directory.path(),
1295            &[
1296                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1297                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1298            ],
1299        )
1300        .is_ok()
1301        {
1302            ExactRetryStageEntries::ArchiveAndDescriptor
1303        } else if require_stage_entries(
1304            self.stage_directory.path(),
1305            &[
1306                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1307                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1308                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1309            ],
1310        )
1311        .is_ok()
1312        {
1313            ExactRetryStageEntries::Complete
1314        } else {
1315            return Err(GitSnapshotError::InvalidStage);
1316        };
1317        self.verify_directory_bindings()?;
1318        Ok(entries)
1319    }
1320
1321    fn verify_directory_bindings(&self) -> Result<(), GitSnapshotError> {
1322        self.isolation_root.verify()?;
1323        self.snapshots_store.verify()?;
1324        self.stage_directory.verify()?;
1325        if self.snapshots_store.path()
1326            != self
1327                .isolation_root
1328                .path()
1329                .join(GIT_SNAPSHOT_STAGE_DIRECTORY)
1330            || self.stage_directory.path() != self.snapshots_store.path().join(&self.operation_id)
1331        {
1332            return Err(GitSnapshotError::InvalidStage);
1333        }
1334        Ok(())
1335    }
1336
1337    fn require_stage_binding(&self, stage: &GitSnapshotStageV1) -> Result<(), GitSnapshotError> {
1338        if stage.operation_id != self.operation_id
1339            || stage.isolation_root_identity != self.isolation_root_identity()
1340            || stage.snapshots_store_identity != self.snapshots_store_identity()
1341            || stage.stage_directory_identity != self.stage_directory_identity()
1342        {
1343            return Err(GitSnapshotError::InvalidStage);
1344        }
1345        Ok(())
1346    }
1347}
1348
1349/// Retained exact staged payloads for zero-write Git object precomputation.
1350pub struct GitSnapshotPrecomputeInputs {
1351    descriptor_bytes: Vec<u8>,
1352    expected_archive: SourceArchive,
1353    directories: GitSnapshotStageDirectory,
1354    descriptor_guard: RetainedRegularFileIdentity,
1355    descriptor_file: File,
1356    archive_guard: RetainedRegularFileIdentity,
1357    archive_file: File,
1358}
1359
1360impl fmt::Debug for GitSnapshotPrecomputeInputs {
1361    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1362        formatter
1363            .debug_struct("GitSnapshotPrecomputeInputs")
1364            .field("operation_id", &self.directories.operation_id)
1365            .field("expected_archive", &self.expected_archive)
1366            .finish_non_exhaustive()
1367    }
1368}
1369
1370impl GitSnapshotPrecomputeInputs {
1371    /// Exact archive descriptor reproduced while the retained stage remains open.
1372    pub const fn expected_archive(&self) -> &SourceArchive {
1373        &self.expected_archive
1374    }
1375
1376    /// Exact canonical descriptor bytes from the retained staged descriptor.
1377    pub fn descriptor_bytes(&self) -> &[u8] {
1378        &self.descriptor_bytes
1379    }
1380
1381    /// Revalidate both retained payload contents, rewind, and borrow the exact archive reader.
1382    ///
1383    /// # Errors
1384    ///
1385    /// Returns a typed failure for any path, identity, entry-set, content, digest, or rewind
1386    /// mismatch.
1387    pub fn archive_reader(&mut self) -> Result<&mut File, GitSnapshotError> {
1388        self.verify_contents()?;
1389        self.archive_file
1390            .seek(std::io::SeekFrom::Start(0))
1391            .map_err(|_| GitSnapshotError::InvalidStage)?;
1392        Ok(&mut self.archive_file)
1393    }
1394
1395    /// Reproduce both retained payloads and exact stage entry bindings.
1396    ///
1397    /// Call this immediately after each synchronous no-write Git hash operation, especially on
1398    /// Unix where retained descriptors cannot prevent in-place mutation or namespace rebinding.
1399    ///
1400    /// # Errors
1401    ///
1402    /// Returns a typed failure for any directory, entry-set, identity, canonical byte, size, or
1403    /// digest mismatch.
1404    pub fn verify_contents(&mut self) -> Result<(), GitSnapshotError> {
1405        self.directories.verify_directory_bindings()?;
1406        require_stage_entries(
1407            self.directories.stage_directory.path(),
1408            &[
1409                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1410                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1411            ],
1412        )?;
1413        self.verify_payload_files()?;
1414        self.directories.verify_directory_bindings()
1415    }
1416
1417    fn open(
1418        directories: GitSnapshotStageDirectory,
1419        archive_file_identity: &RegularFileFilesystemIdentity,
1420        descriptor_file_identity: &RegularFileFilesystemIdentity,
1421        expected_archive: &SourceArchive,
1422        descriptor: &GitSnapshotDescriptor,
1423    ) -> Result<Self, GitSnapshotError> {
1424        if archive_file_identity == descriptor_file_identity {
1425            return Err(GitSnapshotError::InvalidStage);
1426        }
1427        descriptor
1428            .validate(git_snapshot_archive_limits())
1429            .map_err(|_| GitSnapshotError::InvalidStage)?;
1430        if descriptor.bundle.archive != *expected_archive {
1431            return Err(GitSnapshotError::InvalidStage);
1432        }
1433        let expected_descriptor = canonical_git_snapshot_descriptor_bytes(descriptor)
1434            .map_err(|_| GitSnapshotError::InvalidStage)?;
1435        let descriptor_path = directories.descriptor_path();
1436        let (descriptor_guard, mut descriptor_file) =
1437            open_bound_file(&descriptor_path, descriptor_file_identity)?;
1438        let descriptor_bytes =
1439            read_open_file(&mut descriptor_file, MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES)?;
1440        descriptor_guard
1441            .verify_path(&descriptor_path)
1442            .map_err(|_| GitSnapshotError::InvalidStage)?;
1443        if descriptor_bytes != expected_descriptor {
1444            return Err(GitSnapshotError::InvalidStage);
1445        }
1446
1447        let archive_path = directories.archive_path();
1448        let (archive_guard, mut archive_file) =
1449            open_bound_file(&archive_path, archive_file_identity)?;
1450        let archive = describe_open_file(
1451            &mut archive_file,
1452            git_snapshot_archive_limits().max_archive_size,
1453        )?;
1454        archive_guard
1455            .verify_path(&archive_path)
1456            .map_err(|_| GitSnapshotError::InvalidStage)?;
1457        if archive != *expected_archive {
1458            return Err(GitSnapshotError::InvalidStage);
1459        }
1460        let mut result = Self {
1461            descriptor_bytes,
1462            expected_archive: expected_archive.clone(),
1463            directories,
1464            descriptor_guard,
1465            descriptor_file,
1466            archive_guard,
1467            archive_file,
1468        };
1469        result.verify_contents()?;
1470        Ok(result)
1471    }
1472
1473    fn verify_payload_files(&mut self) -> Result<(), GitSnapshotError> {
1474        let descriptor_path = self.directories.descriptor_path();
1475        if regular_file_identity_from_file(&self.descriptor_file)
1476            .map_err(|_| GitSnapshotError::InvalidStage)?
1477            != *self.descriptor_guard.identity()
1478        {
1479            return Err(GitSnapshotError::InvalidStage);
1480        }
1481        let actual_descriptor =
1482            read_open_file(&mut self.descriptor_file, MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES)?;
1483        self.descriptor_guard
1484            .verify_path(&descriptor_path)
1485            .map_err(|_| GitSnapshotError::InvalidStage)?;
1486        if actual_descriptor != self.descriptor_bytes {
1487            return Err(GitSnapshotError::InvalidStage);
1488        }
1489
1490        let archive_path = self.directories.archive_path();
1491        if regular_file_identity_from_file(&self.archive_file)
1492            .map_err(|_| GitSnapshotError::InvalidStage)?
1493            != *self.archive_guard.identity()
1494        {
1495            return Err(GitSnapshotError::InvalidStage);
1496        }
1497        let actual_archive = describe_open_file(
1498            &mut self.archive_file,
1499            git_snapshot_archive_limits().max_archive_size,
1500        )?;
1501        self.archive_file
1502            .seek(std::io::SeekFrom::Start(0))
1503            .map_err(|_| GitSnapshotError::InvalidStage)?;
1504        self.archive_guard
1505            .verify_path(&archive_path)
1506            .map_err(|_| GitSnapshotError::InvalidStage)?;
1507        if actual_archive != self.expected_archive {
1508            return Err(GitSnapshotError::InvalidStage);
1509        }
1510        Ok(())
1511    }
1512}
1513
1514/// Exact retained files and directories authorized for one snapshot object import.
1515///
1516/// The archive reader and canonical descriptor bytes originate from already-open identity-bound
1517/// files. All guards remain alive until this value is dropped. Call [`Self::verify_bindings`]
1518/// immediately after the fixed Git runner consumes either input, especially on Unix.
1519pub struct GitSnapshotImportInputs {
1520    locator: GitSnapshotStageLocatorV1,
1521    stage: GitSnapshotStageV1,
1522    descriptor: GitSnapshotDescriptor,
1523    descriptor_bytes: Vec<u8>,
1524    directories: GitSnapshotStageDirectory,
1525    metadata_guard: RetainedRegularFileIdentity,
1526    metadata_file: File,
1527    descriptor_guard: RetainedRegularFileIdentity,
1528    descriptor_file: File,
1529    archive_guard: RetainedRegularFileIdentity,
1530    archive_file: File,
1531}
1532
1533struct GitSnapshotDirectoryGuard {
1534    path: PathBuf,
1535    retained: RetainedDirectoryIdentity,
1536    #[cfg(windows)]
1537    _private: File,
1538}
1539
1540impl GitSnapshotDirectoryGuard {
1541    fn open(path: &Path) -> Result<Self, GitSnapshotError> {
1542        if !path.is_absolute() {
1543            return Err(GitSnapshotError::InvalidStage);
1544        }
1545        let retained =
1546            RetainedDirectoryIdentity::open(path).map_err(|_| GitSnapshotError::InvalidStage)?;
1547        #[cfg(windows)]
1548        let private =
1549            rustferry_core::windows_private_directory::open_private_directory_read_guard(path)
1550                .map_err(|_| GitSnapshotError::InvalidStage)?;
1551        #[cfg(windows)]
1552        if rustferry_core::directory_identity_from_file(&private)
1553            .map_err(|_| GitSnapshotError::InvalidStage)?
1554            != *retained.identity()
1555        {
1556            return Err(GitSnapshotError::InvalidStage);
1557        }
1558        #[cfg(unix)]
1559        {
1560            use std::os::unix::fs::MetadataExt as _;
1561
1562            let metadata = retained
1563                .as_file()
1564                .metadata()
1565                .map_err(|_| GitSnapshotError::InvalidStage)?;
1566            if metadata.mode() & 0o077 != 0 {
1567                return Err(GitSnapshotError::InvalidStage);
1568            }
1569        }
1570        let result = Self {
1571            path: path.to_owned(),
1572            retained,
1573            #[cfg(windows)]
1574            _private: private,
1575        };
1576        result.verify()?;
1577        Ok(result)
1578    }
1579
1580    fn path(&self) -> &Path {
1581        &self.path
1582    }
1583
1584    fn identity(&self) -> &DirectoryFilesystemIdentity {
1585        self.retained.identity()
1586    }
1587
1588    fn verify(&self) -> Result<(), GitSnapshotError> {
1589        self.retained
1590            .verify_path(&self.path)
1591            .map_err(|_| GitSnapshotError::InvalidStage)
1592    }
1593
1594    fn sync_metadata(&self) -> Result<(), GitSnapshotError> {
1595        self.verify()?;
1596        #[cfg(windows)]
1597        self.retained
1598            .sync_metadata(&self.path)
1599            .map_err(|_| GitSnapshotError::InvalidStage)?;
1600        #[cfg(unix)]
1601        self.retained
1602            .as_file()
1603            .sync_all()
1604            .map_err(|_| GitSnapshotError::InvalidStage)?;
1605        self.verify()
1606    }
1607}
1608
1609impl fmt::Debug for GitSnapshotImportInputs {
1610    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1611        formatter
1612            .debug_struct("GitSnapshotImportInputs")
1613            .field("locator", &self.locator)
1614            .field("stage", &self.stage)
1615            .field("descriptor", &self.descriptor)
1616            .finish_non_exhaustive()
1617    }
1618}
1619
1620impl GitSnapshotImportInputs {
1621    /// One-time, no-mutation adoption of a complete stage before its first locator checkpoint.
1622    ///
1623    /// Callers may use this only for a durable job whose provider resume is still absent. The
1624    /// already-durable final request is mandatory and is checked against the operation, exact
1625    /// graph commit revision, descriptor, manifest, request template, repository, and ref. An
1626    /// orphan stage without such a matching job cannot be adopted through this API.
1627    ///
1628    /// # Errors
1629    ///
1630    /// Returns a typed failure for any absent, partial, rebound, non-canonical, or request-
1631    /// mismatched stage. This method never creates, writes, imports, or deletes anything.
1632    pub(crate) fn adopt_without_locator(
1633        isolation_root: &Path,
1634        final_request: &IosDeviceBuildRequest,
1635    ) -> Result<(GitSnapshotStageLocatorV1, Self), GitSnapshotError> {
1636        let directories =
1637            GitSnapshotStageDirectory::open(isolation_root, &final_request.operation_id)?;
1638        let metadata_path = directories
1639            .stage_directory
1640            .path()
1641            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1642        let (metadata_guard, metadata_file) = open_unbound_file(&metadata_path)?;
1643        let locator = GitSnapshotStageLocatorV1 {
1644            schema_version: GIT_SNAPSHOT_STAGE_LOCATOR_SCHEMA_VERSION,
1645            operation_id: final_request.operation_id.clone(),
1646            isolation_root_identity: directories.isolation_root_identity().to_owned(),
1647            snapshots_store_identity: directories.snapshots_store_identity().to_owned(),
1648            stage_directory_identity: directories.stage_directory_identity().to_owned(),
1649            metadata_file_identity: metadata_guard.identity().to_string(),
1650        };
1651        locator.validate_for_operation(&final_request.operation_id)?;
1652        let inputs = Self::open_retained(
1653            directories,
1654            locator.clone(),
1655            metadata_guard,
1656            metadata_file,
1657            final_request,
1658        )?;
1659        Ok((locator, inputs))
1660    }
1661
1662    /// Delete one exact bounded stage after its keepalive is durably confirmed.
1663    ///
1664    /// The durable locator, immutable stage, and final request are independently cross-bound
1665    /// before any removal. Replay accepts a wholly absent operation directory. A partial stage is
1666    /// removed only when every remaining entry is one of the three fixed files and retains its
1667    /// persisted filesystem identity; an unexpected, rebound, linked, or non-regular entry fails
1668    /// closed. The snapshots-store directory is synchronized after every namespace mutation.
1669    ///
1670    /// # Errors
1671    ///
1672    /// Returns a typed stage failure for any identity, request, entry-set, exact-removal, or
1673    /// directory-durability failure.
1674    pub fn delete_stage_exact(
1675        isolation_root: &Path,
1676        locator: &GitSnapshotStageLocatorV1,
1677        expected_stage: &GitSnapshotStageV1,
1678        final_request: &IosDeviceBuildRequest,
1679    ) -> Result<(), GitSnapshotError> {
1680        locator.validate_for_operation(&final_request.operation_id)?;
1681        let descriptor = GitSnapshotDescriptor::from_request(
1682            final_request,
1683            SourceBundleDescriptor::new(
1684                expected_stage.archive.clone(),
1685                final_request.source.clone(),
1686            ),
1687        )
1688        .map_err(|_| GitSnapshotError::InvalidStage)?;
1689        expected_stage.validate_for_request(&descriptor, final_request)?;
1690        if locator.operation_id != expected_stage.operation_id
1691            || locator.isolation_root_identity != expected_stage.isolation_root_identity
1692            || locator.snapshots_store_identity != expected_stage.snapshots_store_identity
1693            || locator.stage_directory_identity != expected_stage.stage_directory_identity
1694        {
1695            return Err(GitSnapshotError::InvalidStage);
1696        }
1697
1698        let isolation_root_guard = GitSnapshotDirectoryGuard::open(isolation_root)?;
1699        let snapshots_store = GitSnapshotDirectoryGuard::open(
1700            &isolation_root_guard
1701                .path()
1702                .join(GIT_SNAPSHOT_STAGE_DIRECTORY),
1703        )?;
1704        if isolation_root_guard.identity().as_str() != locator.isolation_root_identity
1705            || snapshots_store.identity().as_str() != locator.snapshots_store_identity
1706        {
1707            return Err(GitSnapshotError::InvalidStage);
1708        }
1709        let stage_path = snapshots_store.path().join(&locator.operation_id);
1710        match fs::symlink_metadata(&stage_path) {
1711            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
1712                snapshots_store.sync_metadata()?;
1713                isolation_root_guard.verify()?;
1714                snapshots_store.verify()?;
1715                return Ok(());
1716            }
1717            Err(_) => return Err(GitSnapshotError::InvalidStage),
1718            Ok(_) => {}
1719        }
1720        let stage_directory = GitSnapshotDirectoryGuard::open(&stage_path)?;
1721        if stage_directory.identity().as_str() != locator.stage_directory_identity {
1722            return Err(GitSnapshotError::InvalidStage);
1723        }
1724        let expected_files = [
1725            (
1726                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1727                &expected_stage.archive_file_identity,
1728            ),
1729            (
1730                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1731                &expected_stage.descriptor_file_identity,
1732            ),
1733            (
1734                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1735                &locator.metadata_file_identity,
1736            ),
1737        ];
1738        require_stage_entry_subset(
1739            &stage_path,
1740            &[
1741                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1742                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1743                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1744            ],
1745        )?;
1746        for (name, encoded_identity) in &expected_files {
1747            validate_exact_stage_file_if_present(&stage_directory, name, encoded_identity)?;
1748        }
1749        for (name, encoded_identity) in expected_files {
1750            remove_exact_stage_file_if_present(&stage_directory, name, encoded_identity)?;
1751        }
1752        require_stage_entries(&stage_path, &[])?;
1753        remove_exact_empty_stage_directory(stage_directory)?;
1754        snapshots_store.sync_metadata()?;
1755        require_confirmed_absent(&stage_path)?;
1756        isolation_root_guard.verify()?;
1757        snapshots_store.verify()
1758    }
1759
1760    /// Reopen a complete stage using only its exact durable path-free locator.
1761    ///
1762    /// # Errors
1763    ///
1764    /// Returns a typed failure for a locator, pathname, identity, canonical-byte, digest, graph,
1765    /// or already-durable final-request mismatch. This method performs no mutation.
1766    pub fn load(
1767        isolation_root: &Path,
1768        locator: &GitSnapshotStageLocatorV1,
1769        final_request: &IosDeviceBuildRequest,
1770    ) -> Result<Self, GitSnapshotError> {
1771        locator.validate_for_operation(&final_request.operation_id)?;
1772        let directories = GitSnapshotStageDirectory::open(isolation_root, &locator.operation_id)?;
1773        if locator.isolation_root_identity != directories.isolation_root_identity()
1774            || locator.snapshots_store_identity != directories.snapshots_store_identity()
1775            || locator.stage_directory_identity != directories.stage_directory_identity()
1776        {
1777            return Err(GitSnapshotError::InvalidStage);
1778        }
1779        let metadata_identity =
1780            RegularFileFilesystemIdentity::from_str(&locator.metadata_file_identity)
1781                .map_err(|_| GitSnapshotError::InvalidStage)?;
1782        let metadata_path = directories
1783            .stage_directory
1784            .path()
1785            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1786        let (metadata_guard, metadata_file) = open_bound_file(&metadata_path, &metadata_identity)?;
1787        Self::open_retained(
1788            directories,
1789            locator.clone(),
1790            metadata_guard,
1791            metadata_file,
1792            final_request,
1793        )
1794    }
1795
1796    /// Exact path-free locator used for this reopen.
1797    pub const fn locator(&self) -> &GitSnapshotStageLocatorV1 {
1798        &self.locator
1799    }
1800
1801    /// Canonical stage metadata bound to the final request.
1802    pub const fn stage(&self) -> &GitSnapshotStageV1 {
1803        &self.stage
1804    }
1805
1806    /// Canonical descriptor bound to the final request.
1807    pub const fn descriptor(&self) -> &GitSnapshotDescriptor {
1808        &self.descriptor
1809    }
1810
1811    /// Exact canonical descriptor bytes from the retained staged file.
1812    pub fn descriptor_bytes(&self) -> &[u8] {
1813        &self.descriptor_bytes
1814    }
1815
1816    /// Rewind and borrow the already-open exact archive for streaming to fixed Git.
1817    ///
1818    /// # Errors
1819    ///
1820    /// Returns a typed failure when a directory or file binding changed or the retained reader
1821    /// cannot be rewound.
1822    pub fn archive_reader(&mut self) -> Result<&mut File, GitSnapshotError> {
1823        self.verify_bindings()?;
1824        self.archive_file
1825            .seek(std::io::SeekFrom::Start(0))
1826            .map_err(|_| GitSnapshotError::InvalidStage)?;
1827        Ok(&mut self.archive_file)
1828    }
1829
1830    /// Require the actual retained archive import ID and then reproduce all staged contents.
1831    ///
1832    /// Call this synchronously after `git hash-object -w --stdin` consumes [`Self::archive_reader`]
1833    /// and before importing another object or mutating any ref.
1834    ///
1835    /// # Errors
1836    ///
1837    /// Returns a graph mismatch for a different blob ID, or a stage failure for any post-import
1838    /// content/identity/path drift.
1839    pub fn verify_imported_archive_blob(
1840        &mut self,
1841        actual: &GitSha1ObjectId,
1842    ) -> Result<(), GitSnapshotError> {
1843        if actual != &self.stage.graph.archive_blob {
1844            return Err(GitSnapshotError::ObjectGraphMismatch);
1845        }
1846        self.verify_bindings()
1847    }
1848
1849    /// Require the actual canonical descriptor import ID and reproduce all staged contents.
1850    ///
1851    /// Call this synchronously after `git hash-object -w --stdin` consumes
1852    /// [`Self::descriptor_bytes`] and before importing trees, the commit, or mutating any ref.
1853    ///
1854    /// # Errors
1855    ///
1856    /// Returns a graph mismatch for a different blob ID, or a stage failure for any post-import
1857    /// content/identity/path drift.
1858    pub fn verify_imported_descriptor_blob(
1859        &mut self,
1860        actual: &GitSha1ObjectId,
1861    ) -> Result<(), GitSnapshotError> {
1862        if actual != &self.stage.graph.descriptor_blob {
1863            return Err(GitSnapshotError::ObjectGraphMismatch);
1864        }
1865        self.verify_bindings()
1866    }
1867
1868    /// Revalidate every retained pathname and reproduce all three staged file contents.
1869    ///
1870    /// # Errors
1871    ///
1872    /// Returns a typed failure for any rebound directory, stage entry, file identity, canonical
1873    /// metadata/descriptor byte, or archive size/digest mismatch. The archive is rewound on
1874    /// success. Call this immediately after synchronous Git consumption, especially on Unix.
1875    pub fn verify_bindings(&mut self) -> Result<(), GitSnapshotError> {
1876        self.directories.verify_directory_bindings()?;
1877        self.directories.require_stage_binding(&self.stage)?;
1878        require_stage_entries(
1879            self.directories.stage_directory.path(),
1880            &[
1881                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1882                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1883                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1884            ],
1885        )?;
1886        let metadata_path = self
1887            .directories
1888            .stage_directory
1889            .path()
1890            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1891        let descriptor_path = self.directories.descriptor_path();
1892        let archive_path = self.directories.archive_path();
1893        verify_open_bound_file(&self.metadata_guard, &self.metadata_file, &metadata_path)?;
1894        verify_open_bound_file(
1895            &self.descriptor_guard,
1896            &self.descriptor_file,
1897            &descriptor_path,
1898        )?;
1899        verify_open_bound_file(&self.archive_guard, &self.archive_file, &archive_path)?;
1900
1901        let metadata_bytes = read_open_file(&mut self.metadata_file, MAX_GIT_SNAPSHOT_STAGE_BYTES)?;
1902        if metadata_bytes != self.stage.canonical_bytes(&self.descriptor)? {
1903            return Err(GitSnapshotError::InvalidStage);
1904        }
1905        let descriptor_bytes =
1906            read_open_file(&mut self.descriptor_file, MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES)?;
1907        if descriptor_bytes != self.descriptor_bytes {
1908            return Err(GitSnapshotError::InvalidStage);
1909        }
1910        let archive = describe_open_file(
1911            &mut self.archive_file,
1912            git_snapshot_archive_limits().max_archive_size,
1913        )?;
1914        self.archive_file
1915            .seek(std::io::SeekFrom::Start(0))
1916            .map_err(|_| GitSnapshotError::InvalidStage)?;
1917        if archive != self.stage.archive {
1918            return Err(GitSnapshotError::InvalidStage);
1919        }
1920        verify_open_bound_file(&self.metadata_guard, &self.metadata_file, &metadata_path)?;
1921        verify_open_bound_file(
1922            &self.descriptor_guard,
1923            &self.descriptor_file,
1924            &descriptor_path,
1925        )?;
1926        verify_open_bound_file(&self.archive_guard, &self.archive_file, &archive_path)?;
1927        require_stage_entries(
1928            self.directories.stage_directory.path(),
1929            &[
1930                GIT_SNAPSHOT_STAGE_ARCHIVE_FILE,
1931                GIT_SNAPSHOT_STAGE_DESCRIPTOR_FILE,
1932                GIT_SNAPSHOT_STAGE_METADATA_FILE,
1933            ],
1934        )?;
1935        self.directories.verify_directory_bindings()
1936    }
1937
1938    fn open_retained(
1939        directories: GitSnapshotStageDirectory,
1940        locator: GitSnapshotStageLocatorV1,
1941        metadata_guard: RetainedRegularFileIdentity,
1942        mut metadata_file: File,
1943        final_request: &IosDeviceBuildRequest,
1944    ) -> Result<Self, GitSnapshotError> {
1945        let metadata_path = directories
1946            .stage_directory
1947            .path()
1948            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
1949        let metadata_bytes = read_open_file(&mut metadata_file, MAX_GIT_SNAPSHOT_STAGE_BYTES)?;
1950        metadata_guard
1951            .verify_path(&metadata_path)
1952            .map_err(|_| GitSnapshotError::InvalidStage)?;
1953        let stage: GitSnapshotStageV1 = serde_json::from_slice(&metadata_bytes)
1954            .map_err(|_| GitSnapshotError::InvalidEncoding)?;
1955        directories.require_stage_binding(&stage)?;
1956        if locator.operation_id != stage.operation_id
1957            || locator.isolation_root_identity != stage.isolation_root_identity
1958            || locator.snapshots_store_identity != stage.snapshots_store_identity
1959            || locator.stage_directory_identity != stage.stage_directory_identity
1960            || locator.metadata_file_identity != metadata_guard.identity().as_str()
1961        {
1962            return Err(GitSnapshotError::InvalidStage);
1963        }
1964
1965        let descriptor_identity =
1966            RegularFileFilesystemIdentity::from_str(&stage.descriptor_file_identity)
1967                .map_err(|_| GitSnapshotError::InvalidStage)?;
1968        let descriptor_path = directories.descriptor_path();
1969        let (descriptor_guard, mut descriptor_file) =
1970            open_bound_file(&descriptor_path, &descriptor_identity)?;
1971        let descriptor_bytes =
1972            read_open_file(&mut descriptor_file, MAX_GIT_SNAPSHOT_DESCRIPTOR_BYTES)?;
1973        descriptor_guard
1974            .verify_path(&descriptor_path)
1975            .map_err(|_| GitSnapshotError::InvalidStage)?;
1976        let descriptor: GitSnapshotDescriptor = serde_json::from_slice(&descriptor_bytes)
1977            .map_err(|_| GitSnapshotError::InvalidEncoding)?;
1978        if canonical_git_snapshot_descriptor_bytes(&descriptor)
1979            .map_err(|_| GitSnapshotError::InvalidStage)?
1980            != descriptor_bytes
1981            || stage.canonical_bytes(&descriptor)? != metadata_bytes
1982        {
1983            return Err(GitSnapshotError::InvalidEncoding);
1984        }
1985        stage.validate_for_request(&descriptor, final_request)?;
1986
1987        let archive_identity =
1988            RegularFileFilesystemIdentity::from_str(&stage.archive_file_identity)
1989                .map_err(|_| GitSnapshotError::InvalidStage)?;
1990        let archive_path = directories.archive_path();
1991        let (archive_guard, mut archive_file) = open_bound_file(&archive_path, &archive_identity)?;
1992        let archive = describe_open_file(
1993            &mut archive_file,
1994            git_snapshot_archive_limits().max_archive_size,
1995        )?;
1996        archive_file
1997            .seek(std::io::SeekFrom::Start(0))
1998            .map_err(|_| GitSnapshotError::InvalidStage)?;
1999        archive_guard
2000            .verify_path(&archive_path)
2001            .map_err(|_| GitSnapshotError::InvalidStage)?;
2002        if archive != stage.archive
2003            || locator.metadata_file_identity == stage.archive_file_identity
2004            || locator.metadata_file_identity == stage.descriptor_file_identity
2005        {
2006            return Err(GitSnapshotError::InvalidStage);
2007        }
2008        let mut inputs = Self {
2009            locator,
2010            stage,
2011            descriptor,
2012            descriptor_bytes,
2013            directories,
2014            metadata_guard,
2015            metadata_file,
2016            descriptor_guard,
2017            descriptor_file,
2018            archive_guard,
2019            archive_file,
2020        };
2021        inputs.verify_bindings()?;
2022        Ok(inputs)
2023    }
2024}
2025
2026/// Construct the valid leaf-tree bytes containing the two fixed snapshot files.
2027///
2028/// # Errors
2029///
2030/// Returns [`GitSnapshotError::InvalidTreeEntry`] if a fixed path or object ID cannot form the
2031/// canonical Git tree encoding.
2032pub fn canonical_goal3_tree_bytes(
2033    descriptor_blob: &GitSha1ObjectId,
2034    archive_blob: &GitSha1ObjectId,
2035) -> Result<Vec<u8>, GitSnapshotError> {
2036    let descriptor_name = GIT_SNAPSHOT_DESCRIPTOR_PATH
2037        .rsplit_once('/')
2038        .map(|(_, name)| name)
2039        .ok_or(GitSnapshotError::InvalidTreeEntry)?;
2040    let archive_name = GIT_SNAPSHOT_ARCHIVE_PATH
2041        .rsplit_once('/')
2042        .map(|(_, name)| name)
2043        .ok_or(GitSnapshotError::InvalidTreeEntry)?;
2044    let mut entries = [
2045        (descriptor_name, descriptor_blob),
2046        (archive_name, archive_blob),
2047    ];
2048    entries.sort_by(|left, right| left.0.as_bytes().cmp(right.0.as_bytes()));
2049    let mut tree = Vec::with_capacity(128);
2050    for (name, object) in entries {
2051        append_tree_entry(&mut tree, "100644", name, object)?;
2052    }
2053    Ok(tree)
2054}
2055
2056/// Construct the valid `.rustferry` tree containing only `goal3`.
2057///
2058/// # Errors
2059///
2060/// Returns [`GitSnapshotError::InvalidTreeEntry`] if the object cannot form a canonical entry.
2061pub fn canonical_rustferry_tree_bytes(
2062    goal3_tree: &GitSha1ObjectId,
2063) -> Result<Vec<u8>, GitSnapshotError> {
2064    let mut tree = Vec::with_capacity(40);
2065    append_tree_entry(&mut tree, "40000", "goal3", goal3_tree)?;
2066    Ok(tree)
2067}
2068
2069/// Construct the valid root tree containing only `.rustferry`.
2070///
2071/// # Errors
2072///
2073/// Returns [`GitSnapshotError::InvalidTreeEntry`] if the object cannot form a canonical entry.
2074pub fn canonical_root_tree_bytes(
2075    rustferry_tree: &GitSha1ObjectId,
2076) -> Result<Vec<u8>, GitSnapshotError> {
2077    let mut tree = Vec::with_capacity(48);
2078    append_tree_entry(&mut tree, "40000", ".rustferry", rustferry_tree)?;
2079    Ok(tree)
2080}
2081
2082/// Construct the exact parentless commit bytes for one snapshot graph.
2083///
2084/// # Errors
2085///
2086/// Returns a typed snapshot failure for an unsafe operation identifier or unsupported timestamp.
2087pub fn canonical_parentless_snapshot_commit_bytes(
2088    root_tree: &GitSha1ObjectId,
2089    operation_id: &str,
2090    created_at_ms: u64,
2091) -> Result<Vec<u8>, GitSnapshotError> {
2092    git_snapshot_ref(operation_id).map_err(|_| GitSnapshotError::InvalidOperation)?;
2093    let seconds = created_at_ms / 1_000;
2094    if seconds > i64::MAX as u64 {
2095        return Err(GitSnapshotError::InvalidTimestamp);
2096    }
2097    Ok(format!(
2098        "tree {}\nauthor {SNAPSHOT_ACTOR_NAME} <{SNAPSHOT_ACTOR_EMAIL}> {seconds} +0000\ncommitter {SNAPSHOT_ACTOR_NAME} <{SNAPSHOT_ACTOR_EMAIL}> {seconds} +0000\n\nRustFerry Git snapshot {operation_id}\n",
2099        root_tree.as_str()
2100    )
2101    .into_bytes())
2102}
2103
2104/// Require exact `sha1` output from the private repository object-format probe.
2105///
2106/// Callers must obtain `output` from `git rev-parse --show-object-format` through the fixed
2107/// isolated Git runner before hashing or importing any snapshot object.
2108///
2109/// # Errors
2110///
2111/// Returns [`GitSnapshotError::UnsupportedObjectFormat`] for anything other than exact `sha1`.
2112pub fn require_sha1_object_format(output: &[u8]) -> Result<(), GitSnapshotError> {
2113    if output == b"sha1\n" {
2114        Ok(())
2115    } else {
2116        Err(GitSnapshotError::UnsupportedObjectFormat)
2117    }
2118}
2119
2120fn append_tree_entry(
2121    output: &mut Vec<u8>,
2122    mode: &str,
2123    name: &str,
2124    object: &GitSha1ObjectId,
2125) -> Result<(), GitSnapshotError> {
2126    if !matches!(mode, "100644" | "40000")
2127        || name.is_empty()
2128        || name.len() > 255
2129        || name.as_bytes().contains(&b'/')
2130        || name.as_bytes().contains(&0)
2131        || matches!(name, "." | "..")
2132    {
2133        return Err(GitSnapshotError::InvalidTreeEntry);
2134    }
2135    output.extend_from_slice(mode.as_bytes());
2136    output.push(b' ');
2137    output.extend_from_slice(name.as_bytes());
2138    output.push(0);
2139    output.extend_from_slice(&object.raw()?);
2140    Ok(())
2141}
2142
2143fn is_lower_sha256(value: &str) -> bool {
2144    value.len() == 64
2145        && value
2146            .bytes()
2147            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
2148}
2149
2150fn valid_distinct_directory_identities(root: &str, store: &str, stage: &str) -> bool {
2151    DirectoryFilesystemIdentity::from_str(root).is_ok()
2152        && DirectoryFilesystemIdentity::from_str(store).is_ok()
2153        && DirectoryFilesystemIdentity::from_str(stage).is_ok()
2154        && root != store
2155        && root != stage
2156        && store != stage
2157}
2158
2159fn create_private_directory_create_new(
2160    path: &Path,
2161    parent: &GitSnapshotDirectoryGuard,
2162    allow_existing_race: bool,
2163) -> Result<(), GitSnapshotError> {
2164    parent.verify()?;
2165    #[cfg(windows)]
2166    {
2167        use std::os::windows::io::AsHandle as _;
2168
2169        match rustferry_core::windows_private_directory::create_private_directory(path) {
2170            Ok(directory) => {
2171                rustferry_core::windows_private_directory::sync_private_directory_handle(
2172                    directory.as_handle(),
2173                )
2174                .map_err(|_| GitSnapshotError::InvalidStage)?;
2175            }
2176            Err(error)
2177                if allow_existing_race
2178                    && error.kind()
2179                        == rustferry_core::windows_private_directory::PrivateDirectoryErrorKind::AlreadyExists =>
2180            {
2181                parent.verify()?;
2182                return Ok(());
2183            }
2184            Err(error)
2185                if error.kind()
2186                    == rustferry_core::windows_private_directory::PrivateDirectoryErrorKind::AlreadyExists =>
2187            {
2188                return Err(GitSnapshotError::StageAlreadyExists);
2189            }
2190            Err(_) => return Err(GitSnapshotError::InvalidStage),
2191        }
2192    }
2193    #[cfg(unix)]
2194    {
2195        use std::os::unix::fs::DirBuilderExt as _;
2196
2197        let mut builder = fs::DirBuilder::new();
2198        builder.mode(0o700);
2199        match builder.create(path) {
2200            Ok(()) => File::open(path)
2201                .and_then(|directory| directory.sync_all())
2202                .map_err(|_| GitSnapshotError::InvalidStage)?,
2203            Err(error)
2204                if allow_existing_race && error.kind() == std::io::ErrorKind::AlreadyExists =>
2205            {
2206                parent.verify()?;
2207                return Ok(());
2208            }
2209            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
2210                return Err(GitSnapshotError::StageAlreadyExists);
2211            }
2212            Err(_) => return Err(GitSnapshotError::InvalidStage),
2213        }
2214    }
2215    #[cfg(not(any(windows, unix)))]
2216    {
2217        let _ = allow_existing_race;
2218        fs::create_dir(path).map_err(|error| {
2219            if error.kind() == std::io::ErrorKind::AlreadyExists {
2220                GitSnapshotError::StageAlreadyExists
2221            } else {
2222                GitSnapshotError::InvalidStage
2223            }
2224        })?;
2225        File::open(path)
2226            .and_then(|directory| directory.sync_all())
2227            .map_err(|_| GitSnapshotError::InvalidStage)?;
2228    }
2229    parent.sync_metadata()
2230}
2231
2232fn write_private_file_create_new(
2233    path: &Path,
2234    bytes: &[u8],
2235    parent: &GitSnapshotDirectoryGuard,
2236) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
2237    parent.verify()?;
2238    #[cfg(windows)]
2239    let mut file = rustferry_core::windows_private_directory::create_private_file(path).map_err(
2240        |error| {
2241            if error.kind()
2242                == rustferry_core::windows_private_directory::PrivateDirectoryErrorKind::AlreadyExists
2243            {
2244                GitSnapshotError::StageAlreadyExists
2245            } else {
2246                GitSnapshotError::InvalidStage
2247            }
2248        },
2249    )?;
2250    #[cfg(unix)]
2251    let mut file = {
2252        use std::os::unix::fs::OpenOptionsExt as _;
2253
2254        let mut options = OpenOptions::new();
2255        options.write(true).create_new(true).mode(0o600);
2256        options.open(path).map_err(|error| {
2257            if error.kind() == std::io::ErrorKind::AlreadyExists {
2258                GitSnapshotError::StageAlreadyExists
2259            } else {
2260                GitSnapshotError::InvalidStage
2261            }
2262        })?
2263    };
2264    #[cfg(not(any(windows, unix)))]
2265    let mut file = OpenOptions::new()
2266        .write(true)
2267        .create_new(true)
2268        .open(path)
2269        .map_err(|error| {
2270            if error.kind() == std::io::ErrorKind::AlreadyExists {
2271                GitSnapshotError::StageAlreadyExists
2272            } else {
2273                GitSnapshotError::InvalidStage
2274            }
2275        })?;
2276    file.write_all(bytes)
2277        .and_then(|()| file.sync_all())
2278        .map_err(|_| GitSnapshotError::InvalidStage)?;
2279    let identity =
2280        regular_file_identity_from_file(&file).map_err(|_| GitSnapshotError::InvalidStage)?;
2281    parent.sync_metadata()?;
2282    if RegularFileFilesystemIdentity::capture(path).map_err(|_| GitSnapshotError::InvalidStage)?
2283        != identity
2284    {
2285        return Err(GitSnapshotError::InvalidStage);
2286    }
2287    Ok(identity)
2288}
2289
2290fn reconcile_exact_retry_file(
2291    parent: &GitSnapshotDirectoryGuard,
2292    name: &str,
2293    expected_bytes: &[u8],
2294    max_bytes: u64,
2295    recover_frontier: bool,
2296) -> Result<RegularFileFilesystemIdentity, GitSnapshotError> {
2297    parent.verify()?;
2298    let path = parent.path().join(name);
2299    match fs::symlink_metadata(&path) {
2300        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
2301            return write_private_file_create_new(&path, expected_bytes, parent);
2302        }
2303        Err(_) => return Err(GitSnapshotError::InvalidStage),
2304        Ok(_) => {}
2305    }
2306    let retained =
2307        RetainedRegularFileIdentity::open(&path).map_err(|_| GitSnapshotError::InvalidStage)?;
2308    let identity = retained.identity().clone();
2309    let actual = read_bound_file(&path, &identity, max_bytes);
2310    retained
2311        .verify_path(&path)
2312        .map_err(|_| GitSnapshotError::InvalidStage)?;
2313    parent.verify()?;
2314    if matches!(actual.as_deref(), Ok(bytes) if bytes == expected_bytes) {
2315        drop(retained);
2316        let sync_file = OpenOptions::new()
2317            .read(true)
2318            .write(true)
2319            .open(&path)
2320            .map_err(|_| GitSnapshotError::InvalidStage)?;
2321        if regular_file_identity_from_file(&sync_file)
2322            .map_err(|_| GitSnapshotError::InvalidStage)?
2323            != identity
2324        {
2325            return Err(GitSnapshotError::InvalidStage);
2326        }
2327        sync_file
2328            .sync_all()
2329            .map_err(|_| GitSnapshotError::InvalidStage)?;
2330        drop(sync_file);
2331        parent.sync_metadata()?;
2332        if read_bound_file(&path, &identity, max_bytes)?.as_slice() != expected_bytes {
2333            return Err(GitSnapshotError::InvalidStage);
2334        }
2335        parent.verify()?;
2336        return Ok(identity);
2337    }
2338    let recoverable_prefix = actual
2339        .as_ref()
2340        .is_ok_and(|bytes| bytes.len() < expected_bytes.len() && expected_bytes.starts_with(bytes));
2341    if !recover_frontier || !recoverable_prefix {
2342        return Err(GitSnapshotError::InvalidStage);
2343    }
2344    drop(retained);
2345    remove_exact_stage_file_if_present(parent, name, identity.as_str())?;
2346    write_private_file_create_new(&path, expected_bytes, parent)
2347}
2348
2349fn require_stage_entries(path: &Path, expected: &[&str]) -> Result<(), GitSnapshotError> {
2350    let mut actual = fs::read_dir(path)
2351        .map_err(|_| GitSnapshotError::InvalidStage)?
2352        .map(|entry| {
2353            let entry = entry.map_err(|_| GitSnapshotError::InvalidStage)?;
2354            entry
2355                .file_name()
2356                .into_string()
2357                .map_err(|_| GitSnapshotError::InvalidStage)
2358        })
2359        .collect::<Result<Vec<_>, _>>()?;
2360    actual.sort();
2361    let mut expected = expected.iter().map(ToString::to_string).collect::<Vec<_>>();
2362    expected.sort();
2363    if actual != expected {
2364        return Err(GitSnapshotError::InvalidStage);
2365    }
2366    Ok(())
2367}
2368
2369fn require_stage_entry_subset(path: &Path, allowed: &[&str]) -> Result<(), GitSnapshotError> {
2370    for entry in fs::read_dir(path).map_err(|_| GitSnapshotError::InvalidStage)? {
2371        let entry = entry.map_err(|_| GitSnapshotError::InvalidStage)?;
2372        let name = entry
2373            .file_name()
2374            .into_string()
2375            .map_err(|_| GitSnapshotError::InvalidStage)?;
2376        if !allowed.contains(&name.as_str()) {
2377            return Err(GitSnapshotError::InvalidStage);
2378        }
2379    }
2380    Ok(())
2381}
2382
2383fn validate_exact_stage_file_if_present(
2384    parent: &GitSnapshotDirectoryGuard,
2385    name: &str,
2386    encoded_identity: &str,
2387) -> Result<(), GitSnapshotError> {
2388    let expected = RegularFileFilesystemIdentity::from_str(encoded_identity)
2389        .map_err(|_| GitSnapshotError::InvalidStage)?;
2390    let path = parent.path().join(name);
2391    match fs::symlink_metadata(&path) {
2392        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2393        Err(_) => return Err(GitSnapshotError::InvalidStage),
2394        Ok(_) => {}
2395    }
2396    parent.verify()?;
2397    if RegularFileFilesystemIdentity::capture(&path).map_err(|_| GitSnapshotError::InvalidStage)?
2398        != expected
2399    {
2400        return Err(GitSnapshotError::InvalidStage);
2401    }
2402    parent.verify()
2403}
2404
2405fn remove_exact_stage_file_if_present(
2406    parent: &GitSnapshotDirectoryGuard,
2407    name: &str,
2408    encoded_identity: &str,
2409) -> Result<(), GitSnapshotError> {
2410    let expected = RegularFileFilesystemIdentity::from_str(encoded_identity)
2411        .map_err(|_| GitSnapshotError::InvalidStage)?;
2412    let path = parent.path().join(name);
2413    match fs::symlink_metadata(&path) {
2414        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
2415        Err(_) => return Err(GitSnapshotError::InvalidStage),
2416        Ok(_) => {}
2417    }
2418    parent.verify()?;
2419    #[cfg(windows)]
2420    {
2421        let removal = rustferry_core::open_regular_file_for_exact_removal(&path)
2422            .map_err(|_| GitSnapshotError::InvalidStage)?;
2423        if removal.identity() != &expected {
2424            return Err(GitSnapshotError::InvalidStage);
2425        }
2426        removal
2427            .remove()
2428            .map_err(|_| GitSnapshotError::InvalidStage)?;
2429    }
2430    #[cfg(not(windows))]
2431    {
2432        let retained =
2433            RetainedRegularFileIdentity::open(&path).map_err(|_| GitSnapshotError::InvalidStage)?;
2434        if retained.identity() != &expected {
2435            return Err(GitSnapshotError::InvalidStage);
2436        }
2437        retained
2438            .verify_path(&path)
2439            .map_err(|_| GitSnapshotError::InvalidStage)?;
2440        fs::remove_file(&path).map_err(|_| GitSnapshotError::InvalidStage)?;
2441        drop(retained);
2442    }
2443    require_confirmed_absent(&path)?;
2444    parent.sync_metadata()
2445}
2446
2447fn require_confirmed_absent(path: &Path) -> Result<(), GitSnapshotError> {
2448    match fs::symlink_metadata(path) {
2449        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
2450        Ok(_) | Err(_) => Err(GitSnapshotError::InvalidStage),
2451    }
2452}
2453
2454fn remove_exact_empty_stage_directory(
2455    directory: GitSnapshotDirectoryGuard,
2456) -> Result<(), GitSnapshotError> {
2457    directory.verify()?;
2458    #[cfg(windows)]
2459    {
2460        let path = directory.path().to_owned();
2461        let expected = directory.identity().clone();
2462        drop(directory);
2463        let removal = rustferry_core::windows_private_directory::open_private_directory(&path)
2464            .map_err(|_| GitSnapshotError::InvalidStage)?;
2465        if rustferry_core::directory_identity_from_file(&removal)
2466            .map_err(|_| GitSnapshotError::InvalidStage)?
2467            != expected
2468        {
2469            return Err(GitSnapshotError::InvalidStage);
2470        }
2471        rustferry_core::windows_private_directory::remove_private_directory_handle(removal)
2472            .map_err(|_| GitSnapshotError::InvalidStage)
2473    }
2474    #[cfg(not(windows))]
2475    {
2476        fs::remove_dir(directory.path()).map_err(|_| GitSnapshotError::InvalidStage)?;
2477        drop(directory);
2478        Ok(())
2479    }
2480}
2481
2482fn open_unbound_file(path: &Path) -> Result<(RetainedRegularFileIdentity, File), GitSnapshotError> {
2483    let retained =
2484        RetainedRegularFileIdentity::open(path).map_err(|_| GitSnapshotError::InvalidStage)?;
2485    let expected = retained.identity().clone();
2486    open_bound_file_with_retained(path, &expected, retained)
2487}
2488
2489fn verify_open_bound_file(
2490    retained: &RetainedRegularFileIdentity,
2491    file: &File,
2492    path: &Path,
2493) -> Result<(), GitSnapshotError> {
2494    if regular_file_identity_from_file(file).map_err(|_| GitSnapshotError::InvalidStage)?
2495        != *retained.identity()
2496    {
2497        return Err(GitSnapshotError::InvalidStage);
2498    }
2499    retained
2500        .verify_path(path)
2501        .map_err(|_| GitSnapshotError::InvalidStage)
2502}
2503
2504fn open_bound_file_with_retained(
2505    path: &Path,
2506    expected: &RegularFileFilesystemIdentity,
2507    retained: RetainedRegularFileIdentity,
2508) -> Result<(RetainedRegularFileIdentity, File), GitSnapshotError> {
2509    if retained.identity() != expected {
2510        return Err(GitSnapshotError::InvalidStage);
2511    }
2512    retained
2513        .verify_path(path)
2514        .map_err(|_| GitSnapshotError::InvalidStage)?;
2515    let file = File::open(path).map_err(|_| GitSnapshotError::InvalidStage)?;
2516    if regular_file_identity_from_file(&file).map_err(|_| GitSnapshotError::InvalidStage)?
2517        != *expected
2518    {
2519        return Err(GitSnapshotError::InvalidStage);
2520    }
2521    retained
2522        .verify_path(path)
2523        .map_err(|_| GitSnapshotError::InvalidStage)?;
2524    Ok((retained, file))
2525}
2526
2527fn read_open_file(file: &mut File, maximum: u64) -> Result<Vec<u8>, GitSnapshotError> {
2528    if file
2529        .metadata()
2530        .map_err(|_| GitSnapshotError::InvalidStage)?
2531        .len()
2532        > maximum
2533    {
2534        return Err(GitSnapshotError::StageTooLarge);
2535    }
2536    file.seek(std::io::SeekFrom::Start(0))
2537        .map_err(|_| GitSnapshotError::InvalidStage)?;
2538    let capacity = usize::try_from(maximum).map_err(|_| GitSnapshotError::StageTooLarge)?;
2539    let mut bytes = Vec::with_capacity(capacity.min(64 * 1024));
2540    std::io::Read::take(file, maximum.saturating_add(1))
2541        .read_to_end(&mut bytes)
2542        .map_err(|_| GitSnapshotError::InvalidStage)?;
2543    if bytes.len() as u64 > maximum {
2544        return Err(GitSnapshotError::StageTooLarge);
2545    }
2546    Ok(bytes)
2547}
2548
2549fn describe_open_file(file: &mut File, maximum: u64) -> Result<SourceArchive, GitSnapshotError> {
2550    if file
2551        .metadata()
2552        .map_err(|_| GitSnapshotError::InvalidStage)?
2553        .len()
2554        > maximum
2555    {
2556        return Err(GitSnapshotError::StageTooLarge);
2557    }
2558    file.seek(std::io::SeekFrom::Start(0))
2559        .map_err(|_| GitSnapshotError::InvalidStage)?;
2560    let mut size = 0_u64;
2561    let mut digest = Sha256::new();
2562    let mut buffer = vec![0_u8; 64 * 1024];
2563    loop {
2564        let read = file
2565            .read(&mut buffer)
2566            .map_err(|_| GitSnapshotError::InvalidStage)?;
2567        if read == 0 {
2568            break;
2569        }
2570        size = size
2571            .checked_add(u64::try_from(read).map_err(|_| GitSnapshotError::StageTooLarge)?)
2572            .ok_or(GitSnapshotError::StageTooLarge)?;
2573        if size > maximum {
2574            return Err(GitSnapshotError::StageTooLarge);
2575        }
2576        digest.update(&buffer[..read]);
2577    }
2578    Ok(SourceArchive {
2579        size,
2580        sha256: hex::encode(digest.finalize()),
2581    })
2582}
2583
2584fn open_bound_file(
2585    path: &Path,
2586    expected: &RegularFileFilesystemIdentity,
2587) -> Result<(RetainedRegularFileIdentity, File), GitSnapshotError> {
2588    let retained =
2589        RetainedRegularFileIdentity::open(path).map_err(|_| GitSnapshotError::InvalidStage)?;
2590    if retained.identity() != expected {
2591        return Err(GitSnapshotError::InvalidStage);
2592    }
2593    retained
2594        .verify_path(path)
2595        .map_err(|_| GitSnapshotError::InvalidStage)?;
2596    let file = File::open(path).map_err(|_| GitSnapshotError::InvalidStage)?;
2597    if regular_file_identity_from_file(&file).map_err(|_| GitSnapshotError::InvalidStage)?
2598        != *expected
2599    {
2600        return Err(GitSnapshotError::InvalidStage);
2601    }
2602    retained
2603        .verify_path(path)
2604        .map_err(|_| GitSnapshotError::InvalidStage)?;
2605    Ok((retained, file))
2606}
2607
2608fn read_bound_file(
2609    path: &Path,
2610    expected: &RegularFileFilesystemIdentity,
2611    maximum: u64,
2612) -> Result<Vec<u8>, GitSnapshotError> {
2613    let (retained, mut file) = open_bound_file(path, expected)?;
2614    if file
2615        .metadata()
2616        .map_err(|_| GitSnapshotError::InvalidStage)?
2617        .len()
2618        > maximum
2619    {
2620        return Err(GitSnapshotError::StageTooLarge);
2621    }
2622    let capacity = usize::try_from(maximum).map_err(|_| GitSnapshotError::StageTooLarge)?;
2623    let mut bytes = Vec::with_capacity(capacity.min(64 * 1024));
2624    std::io::Read::take(&mut file, maximum.saturating_add(1))
2625        .read_to_end(&mut bytes)
2626        .map_err(|_| GitSnapshotError::InvalidStage)?;
2627    if bytes.len() as u64 > maximum {
2628        return Err(GitSnapshotError::StageTooLarge);
2629    }
2630    retained
2631        .verify_path(path)
2632        .map_err(|_| GitSnapshotError::InvalidStage)?;
2633    Ok(bytes)
2634}
2635
2636fn describe_bound_file(
2637    path: &Path,
2638    expected: &RegularFileFilesystemIdentity,
2639    maximum: u64,
2640) -> Result<SourceArchive, GitSnapshotError> {
2641    let (retained, mut file) = open_bound_file(path, expected)?;
2642    if file
2643        .metadata()
2644        .map_err(|_| GitSnapshotError::InvalidStage)?
2645        .len()
2646        > maximum
2647    {
2648        return Err(GitSnapshotError::StageTooLarge);
2649    }
2650    let mut size = 0_u64;
2651    let mut digest = Sha256::new();
2652    let mut buffer = vec![0_u8; 64 * 1024];
2653    loop {
2654        let read = file
2655            .read(&mut buffer)
2656            .map_err(|_| GitSnapshotError::InvalidStage)?;
2657        if read == 0 {
2658            break;
2659        }
2660        size = size
2661            .checked_add(u64::try_from(read).map_err(|_| GitSnapshotError::StageTooLarge)?)
2662            .ok_or(GitSnapshotError::StageTooLarge)?;
2663        if size > maximum {
2664            return Err(GitSnapshotError::StageTooLarge);
2665        }
2666        digest.update(&buffer[..read]);
2667    }
2668    retained
2669        .verify_path(path)
2670        .map_err(|_| GitSnapshotError::InvalidStage)?;
2671    Ok(SourceArchive {
2672        size,
2673        sha256: hex::encode(digest.finalize()),
2674    })
2675}
2676
2677#[cfg(test)]
2678mod tests {
2679    use std::collections::BTreeSet;
2680    use std::fs;
2681    use std::io::Write as _;
2682    use std::path::Path;
2683    use std::process::{Command, Stdio};
2684
2685    use camino::Utf8PathBuf;
2686    use rustferry_remote::{
2687        BuildProfile, BundleIdentifier, CURRENT_PROTOCOL_VERSION, IosArtifactType,
2688        IosDeviceBuildRequest, IosDeviceProductExpectation, SigningMode, SigningPlan,
2689        SigningTarget, SigningTargetKind, SourceBundleDescriptor, SourceBundleRequest, SourceMode,
2690        create_source_bundle_archive, plan_source_bundle,
2691    };
2692
2693    use super::*;
2694
2695    struct StageFixture {
2696        _temporary: tempfile::TempDir,
2697        isolation_root: PathBuf,
2698        archive_path: PathBuf,
2699        descriptor_path: PathBuf,
2700        metadata_path: PathBuf,
2701        descriptor: GitSnapshotDescriptor,
2702        request: IosDeviceBuildRequest,
2703        stage: GitSnapshotStageV1,
2704        locator: GitSnapshotStageLocatorV1,
2705    }
2706
2707    #[derive(Clone, Copy, Debug)]
2708    enum ExactRetryFrontier {
2709        Empty,
2710        Archive,
2711        ArchiveAndDescriptor,
2712        PartialArchive,
2713        PartialDescriptor,
2714        PartialMetadata,
2715        Complete,
2716    }
2717
2718    struct ExactRetryFixture {
2719        operation_id: String,
2720        archive_path: PathBuf,
2721        descriptor_path: PathBuf,
2722        metadata_path: PathBuf,
2723        descriptor: GitSnapshotDescriptor,
2724        request: IosDeviceBuildRequest,
2725        stage: GitSnapshotStageV1,
2726        locator: GitSnapshotStageLocatorV1,
2727        preexisting_identities: Vec<RegularFileFilesystemIdentity>,
2728    }
2729
2730    fn object(byte: char) -> GitSha1ObjectId {
2731        GitSha1ObjectId::new(byte.to_string().repeat(40)).expect("object ID")
2732    }
2733
2734    fn hash_git_object(
2735        git_directory: &Path,
2736        kind: GitSnapshotObjectKind,
2737        write: bool,
2738        bytes: &[u8],
2739    ) -> GitSha1ObjectId {
2740        let mut command = Command::new("git");
2741        command
2742            .arg("--git-dir")
2743            .arg(git_directory)
2744            .arg("hash-object");
2745        if write {
2746            command.arg("-w");
2747        }
2748        if kind != GitSnapshotObjectKind::Blob {
2749            command.args(["-t", kind.git_type()]);
2750        }
2751        let mut child = command
2752            .arg("--stdin")
2753            .stdin(Stdio::piped())
2754            .stdout(Stdio::piped())
2755            .stderr(Stdio::piped())
2756            .spawn()
2757            .expect("spawn Git hash-object");
2758        child
2759            .stdin
2760            .take()
2761            .unwrap()
2762            .write_all(bytes)
2763            .expect("write Git object");
2764        let output = child.wait_with_output().expect("wait for Git hash-object");
2765        assert!(output.status.success(), "Git hash-object failed");
2766        GitSha1ObjectId::new(String::from_utf8(output.stdout).unwrap().trim().to_owned()).unwrap()
2767    }
2768
2769    fn hash_git_object_file(
2770        git_directory: &Path,
2771        kind: GitSnapshotObjectKind,
2772        write: bool,
2773        file: &File,
2774    ) -> GitSha1ObjectId {
2775        let mut command = Command::new("git");
2776        command
2777            .arg("--git-dir")
2778            .arg(git_directory)
2779            .arg("hash-object");
2780        if write {
2781            command.arg("-w");
2782        }
2783        if kind != GitSnapshotObjectKind::Blob {
2784            command.args(["-t", kind.git_type()]);
2785        }
2786        let output = command
2787            .arg("--stdin")
2788            .stdin(Stdio::from(file.try_clone().expect("clone retained input")))
2789            .stdout(Stdio::piped())
2790            .stderr(Stdio::piped())
2791            .output()
2792            .expect("run Git hash-object with retained input");
2793        assert!(output.status.success(), "Git hash-object failed");
2794        GitSha1ObjectId::new(String::from_utf8(output.stdout).unwrap().trim().to_owned()).unwrap()
2795    }
2796
2797    fn create_private_test_root(path: &Path) {
2798        #[cfg(windows)]
2799        drop(
2800            rustferry_core::windows_private_directory::create_private_directory(path)
2801                .expect("private test root"),
2802        );
2803        #[cfg(unix)]
2804        {
2805            use std::os::unix::fs::DirBuilderExt as _;
2806
2807            let mut builder = fs::DirBuilder::new();
2808            builder.mode(0o700).create(path).expect("private test root");
2809        }
2810    }
2811
2812    #[allow(clippy::too_many_lines)]
2813    fn stage_fixture() -> StageFixture {
2814        let temporary = tempfile::tempdir().expect("temporary directory");
2815        let utf8 = |path| Utf8PathBuf::from_path_buf(path).expect("UTF-8 temporary path");
2816        let workspace = utf8(temporary.path().join("workspace"));
2817        let project = workspace.join("app");
2818        let isolation_root = temporary.path().join("private");
2819        let operation_id = "operation-snapshot-stage-1";
2820        fs::create_dir_all(project.join("src")).unwrap();
2821        create_private_test_root(&isolation_root);
2822        let stage_directory =
2823            GitSnapshotStageDirectory::create(&isolation_root, operation_id).unwrap();
2824        fs::write(
2825            workspace.join("Cargo.toml"),
2826            b"[workspace]\nmembers = [\"app\"]\n",
2827        )
2828        .unwrap();
2829        fs::write(workspace.join("Cargo.lock"), b"").unwrap();
2830        fs::write(
2831            project.join("Cargo.toml"),
2832            b"[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
2833        )
2834        .unwrap();
2835        fs::write(project.join("src/main.rs"), b"fn main() {}\n").unwrap();
2836
2837        let limits = git_snapshot_archive_limits();
2838        let plan = plan_source_bundle(
2839            &SourceBundleRequest::new(workspace, project).with_limits(limits.source),
2840        )
2841        .unwrap();
2842        let archive_path = utf8(stage_directory.archive_path());
2843        let archive = create_source_bundle_archive(&plan, &archive_path, limits).unwrap();
2844        let archive_identity = stage_directory.seal_archive(&archive).unwrap();
2845        let signing = SigningPlan {
2846            mode: SigningMode::UnsignedCompileOnly,
2847            signing: None,
2848            team: None,
2849            device: None,
2850            targets: vec![SigningTarget {
2851                name: "App".to_owned(),
2852                bundle_identifier: BundleIdentifier::new("com.example.app").unwrap(),
2853                kind: SigningTargetKind::Application,
2854            }],
2855            provisioning: Vec::new(),
2856            entitlements: Vec::new(),
2857            allow_provisioning_updates: false,
2858        };
2859        let mut request = IosDeviceBuildRequest {
2860            protocol_version: CURRENT_PROTOCOL_VERSION,
2861            operation_id: operation_id.to_owned(),
2862            product_name: "App".to_owned(),
2863            bundle_identifier: "com.example.app".to_owned(),
2864            minimum_ios_version: "16.0".to_owned(),
2865            product: IosDeviceProductExpectation {
2866                app_directory_name: "App.app".to_owned(),
2867                executable: "App".to_owned(),
2868                app_version: "1.0.0".to_owned(),
2869                build_number: "1".to_owned(),
2870                nested_bundles: Vec::new(),
2871            },
2872            profile: BuildProfile::Release,
2873            source_mode: SourceMode::GitSnapshot,
2874            source_repository: Some("https://github.com/example/project".to_owned()),
2875            source_revision: None,
2876            source: plan.manifest().clone(),
2877            signing,
2878            requested_artifacts: BTreeSet::from([IosArtifactType::Xcarchive]),
2879        };
2880        let descriptor = GitSnapshotDescriptor::from_request(
2881            &request,
2882            SourceBundleDescriptor::new(archive.clone(), plan.manifest().clone()),
2883        )
2884        .unwrap();
2885        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(&descriptor).unwrap();
2886        let descriptor_path = stage_directory.descriptor_path();
2887        let descriptor_identity = stage_directory
2888            .write_descriptor_create_new(&descriptor)
2889            .unwrap();
2890        let graph = GitSnapshotObjectGraphV1 {
2891            schema_version: GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
2892            archive_blob: object('1'),
2893            descriptor_blob: object('2'),
2894            goal3_tree: object('3'),
2895            rustferry_tree: object('4'),
2896            root_tree: object('5'),
2897            commit: object('6'),
2898        };
2899        request.source_revision = Some(graph.commit.as_str().to_owned());
2900        let stage = GitSnapshotStageV1 {
2901            schema_version: GIT_SNAPSHOT_STAGE_SCHEMA_VERSION,
2902            operation_id: operation_id.to_owned(),
2903            isolation_root_identity: stage_directory.isolation_root_identity().to_owned(),
2904            snapshots_store_identity: stage_directory.snapshots_store_identity().to_owned(),
2905            stage_directory_identity: stage_directory.stage_directory_identity().to_owned(),
2906            source_repository: descriptor.source_repository.clone(),
2907            source_ref: GitSnapshotSourceRef::for_operation(operation_id).unwrap(),
2908            keepalive_ref: GitSnapshotKeepaliveRef::for_operation(operation_id).unwrap(),
2909            source_created_at_ms: 1_234_567,
2910            consent_sha256: hex::encode(Sha256::digest(b"accepted-plan-v1")),
2911            request_template_sha256: descriptor.request_template_sha256.clone(),
2912            manifest_sha256: descriptor.bundle.manifest.sha256.clone(),
2913            archive,
2914            descriptor_sha256: hex::encode(Sha256::digest(&descriptor_bytes)),
2915            final_request: request.clone(),
2916            archive_file_identity: archive_identity.to_string(),
2917            descriptor_file_identity: descriptor_identity.to_string(),
2918            graph,
2919        };
2920        let locator = stage_directory
2921            .publish_metadata_create_new(&stage, &descriptor, &request)
2922            .unwrap();
2923        let metadata_path = git_snapshot_stage_directory(&isolation_root, operation_id)
2924            .unwrap()
2925            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
2926        StageFixture {
2927            _temporary: temporary,
2928            isolation_root,
2929            archive_path: archive_path.into_std_path_buf(),
2930            descriptor_path,
2931            metadata_path,
2932            descriptor,
2933            request,
2934            stage,
2935            locator,
2936        }
2937    }
2938
2939    #[allow(clippy::too_many_lines)]
2940    fn reconcile_exact_retry_fixture(
2941        parent: &StageFixture,
2942        operation_id: &str,
2943        frontier: ExactRetryFrontier,
2944    ) -> ExactRetryFixture {
2945        let archive_bytes = fs::read(&parent.archive_path).unwrap();
2946        let directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
2947            &parent.isolation_root,
2948            operation_id,
2949        )
2950        .unwrap();
2951        let archive_path = directory.archive_path();
2952        let descriptor_path = directory.descriptor_path();
2953        let metadata_path = git_snapshot_stage_directory(&parent.isolation_root, operation_id)
2954            .unwrap()
2955            .join(GIT_SNAPSHOT_STAGE_METADATA_FILE);
2956        let mut request = parent.request.clone();
2957        request.operation_id = operation_id.to_owned();
2958        request.source_revision = None;
2959        let descriptor = GitSnapshotDescriptor::from_request(
2960            &request,
2961            SourceBundleDescriptor::new(
2962                parent.stage.archive.clone(),
2963                parent.descriptor.bundle.manifest.clone(),
2964            ),
2965        )
2966        .unwrap();
2967        let descriptor_bytes = canonical_git_snapshot_descriptor_bytes(&descriptor).unwrap();
2968
2969        match frontier {
2970            ExactRetryFrontier::Archive | ExactRetryFrontier::ArchiveAndDescriptor => {
2971                write_private_file_create_new(
2972                    &archive_path,
2973                    &archive_bytes,
2974                    &directory.stage_directory,
2975                )
2976                .unwrap();
2977            }
2978            ExactRetryFrontier::PartialArchive => {
2979                write_private_file_create_new(
2980                    &archive_path,
2981                    &archive_bytes[..archive_bytes.len() / 2],
2982                    &directory.stage_directory,
2983                )
2984                .unwrap();
2985            }
2986            ExactRetryFrontier::PartialDescriptor
2987            | ExactRetryFrontier::PartialMetadata
2988            | ExactRetryFrontier::Complete => {
2989                write_private_file_create_new(
2990                    &archive_path,
2991                    &archive_bytes,
2992                    &directory.stage_directory,
2993                )
2994                .unwrap();
2995                let descriptor_payload =
2996                    if matches!(frontier, ExactRetryFrontier::PartialDescriptor) {
2997                        &descriptor_bytes[..descriptor_bytes.len() / 2]
2998                    } else {
2999                        &descriptor_bytes
3000                    };
3001                write_private_file_create_new(
3002                    &descriptor_path,
3003                    descriptor_payload,
3004                    &directory.stage_directory,
3005                )
3006                .unwrap();
3007            }
3008            ExactRetryFrontier::Empty => {}
3009        }
3010        if matches!(frontier, ExactRetryFrontier::ArchiveAndDescriptor) {
3011            write_private_file_create_new(
3012                &descriptor_path,
3013                &descriptor_bytes,
3014                &directory.stage_directory,
3015            )
3016            .unwrap();
3017        }
3018        let mut preexisting_identities = [
3019            archive_path.as_path(),
3020            descriptor_path.as_path(),
3021            metadata_path.as_path(),
3022        ]
3023        .into_iter()
3024        .filter_map(|path| RegularFileFilesystemIdentity::capture(path).ok())
3025        .collect::<Vec<_>>();
3026
3027        let archive_identity = directory
3028            .write_or_verify_archive_bytes_exact_retry(&archive_bytes, &parent.stage.archive)
3029            .unwrap();
3030        let descriptor_identity = directory
3031            .write_or_verify_descriptor_exact_retry(&descriptor)
3032            .unwrap();
3033        let graph = GitSnapshotObjectGraphV1 {
3034            schema_version: GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
3035            archive_blob: parent.stage.graph.archive_blob.clone(),
3036            descriptor_blob: object('a'),
3037            goal3_tree: object('b'),
3038            rustferry_tree: object('c'),
3039            root_tree: object('d'),
3040            commit: object('e'),
3041        };
3042        request.source_revision = Some(graph.commit.as_str().to_owned());
3043        let stage = GitSnapshotStageV1 {
3044            schema_version: GIT_SNAPSHOT_STAGE_SCHEMA_VERSION,
3045            operation_id: operation_id.to_owned(),
3046            isolation_root_identity: directory.isolation_root_identity().to_owned(),
3047            snapshots_store_identity: directory.snapshots_store_identity().to_owned(),
3048            stage_directory_identity: directory.stage_directory_identity().to_owned(),
3049            source_repository: descriptor.source_repository.clone(),
3050            source_ref: GitSnapshotSourceRef::for_operation(operation_id).unwrap(),
3051            keepalive_ref: GitSnapshotKeepaliveRef::for_operation(operation_id).unwrap(),
3052            source_created_at_ms: 9_876_543,
3053            consent_sha256: hex::encode(Sha256::digest(b"retry-lineage-authorization-v1")),
3054            request_template_sha256: descriptor.request_template_sha256.clone(),
3055            manifest_sha256: descriptor.bundle.manifest.sha256.clone(),
3056            archive: parent.stage.archive.clone(),
3057            descriptor_sha256: hex::encode(Sha256::digest(&descriptor_bytes)),
3058            final_request: request.clone(),
3059            archive_file_identity: archive_identity.to_string(),
3060            descriptor_file_identity: descriptor_identity.to_string(),
3061            graph,
3062        };
3063        if matches!(
3064            frontier,
3065            ExactRetryFrontier::PartialMetadata | ExactRetryFrontier::Complete
3066        ) {
3067            let metadata_bytes = stage.canonical_bytes(&descriptor).unwrap();
3068            let metadata_payload = if matches!(frontier, ExactRetryFrontier::PartialMetadata) {
3069                &metadata_bytes[..metadata_bytes.len() / 2]
3070            } else {
3071                &metadata_bytes
3072            };
3073            write_private_file_create_new(
3074                &metadata_path,
3075                metadata_payload,
3076                &directory.stage_directory,
3077            )
3078            .unwrap();
3079            preexisting_identities
3080                .push(RegularFileFilesystemIdentity::capture(&metadata_path).unwrap());
3081        }
3082        let locator = directory
3083            .publish_or_verify_metadata_exact_retry(&stage, &descriptor, &request)
3084            .unwrap();
3085
3086        let replay_directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3087            &parent.isolation_root,
3088            operation_id,
3089        )
3090        .unwrap();
3091        assert_eq!(
3092            replay_directory
3093                .write_or_verify_archive_bytes_exact_retry(&archive_bytes, &parent.stage.archive)
3094                .unwrap(),
3095            archive_identity
3096        );
3097        assert_eq!(
3098            replay_directory
3099                .write_or_verify_descriptor_exact_retry(&descriptor)
3100                .unwrap(),
3101            descriptor_identity
3102        );
3103        assert_eq!(
3104            replay_directory
3105                .publish_or_verify_metadata_exact_retry(&stage, &descriptor, &request)
3106                .unwrap(),
3107            locator
3108        );
3109
3110        ExactRetryFixture {
3111            operation_id: operation_id.to_owned(),
3112            archive_path,
3113            descriptor_path,
3114            metadata_path,
3115            descriptor,
3116            request,
3117            stage,
3118            locator,
3119            preexisting_identities,
3120        }
3121    }
3122
3123    #[test]
3124    fn exact_retry_recovers_every_ordered_stage_frontier_and_replays_complete_stage() {
3125        let parent = stage_fixture();
3126        for (index, frontier) in [
3127            ExactRetryFrontier::Empty,
3128            ExactRetryFrontier::Archive,
3129            ExactRetryFrontier::ArchiveAndDescriptor,
3130            ExactRetryFrontier::PartialArchive,
3131            ExactRetryFrontier::PartialDescriptor,
3132            ExactRetryFrontier::PartialMetadata,
3133            ExactRetryFrontier::Complete,
3134        ]
3135        .into_iter()
3136        .enumerate()
3137        {
3138            let recovered = reconcile_exact_retry_fixture(
3139                &parent,
3140                &format!("operation-exact-retry-frontier-{index}"),
3141                frontier,
3142            );
3143            let loaded = GitSnapshotImportInputs::load(
3144                &parent.isolation_root,
3145                &recovered.locator,
3146                &recovered.request,
3147            )
3148            .unwrap();
3149            assert_eq!(loaded.stage(), &recovered.stage);
3150            assert_eq!(loaded.descriptor(), &recovered.descriptor);
3151            assert_eq!(
3152                fs::read(&recovered.archive_path).unwrap(),
3153                fs::read(&parent.archive_path).unwrap()
3154            );
3155            assert_eq!(
3156                fs::read(&recovered.descriptor_path).unwrap(),
3157                canonical_git_snapshot_descriptor_bytes(&recovered.descriptor).unwrap()
3158            );
3159            assert_eq!(
3160                fs::read(&recovered.metadata_path).unwrap(),
3161                recovered
3162                    .stage
3163                    .canonical_bytes(&recovered.descriptor)
3164                    .unwrap()
3165            );
3166            if matches!(
3167                frontier,
3168                ExactRetryFrontier::Archive
3169                    | ExactRetryFrontier::ArchiveAndDescriptor
3170                    | ExactRetryFrontier::Complete
3171            ) {
3172                assert!(recovered.preexisting_identities.contains(
3173                    &RegularFileFilesystemIdentity::capture(&recovered.archive_path).unwrap()
3174                ));
3175            }
3176            if matches!(
3177                frontier,
3178                ExactRetryFrontier::ArchiveAndDescriptor | ExactRetryFrontier::Complete
3179            ) {
3180                assert!(recovered.preexisting_identities.contains(
3181                    &RegularFileFilesystemIdentity::capture(&recovered.descriptor_path).unwrap()
3182                ));
3183            }
3184            if matches!(frontier, ExactRetryFrontier::Complete) {
3185                assert!(recovered.preexisting_identities.contains(
3186                    &RegularFileFilesystemIdentity::capture(&recovered.metadata_path).unwrap()
3187                ));
3188            }
3189        }
3190    }
3191
3192    #[test]
3193    fn exact_retry_rejects_nonprefix_out_of_order_extra_and_complete_mismatch_without_mutation() {
3194        let parent = stage_fixture();
3195        let archive = fs::read(&parent.archive_path).unwrap();
3196        let operation_id = "operation-exact-retry-invalid-archive";
3197        let directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3198            &parent.isolation_root,
3199            operation_id,
3200        )
3201        .unwrap();
3202        write_private_file_create_new(
3203            &directory.archive_path(),
3204            b"not-an-archive-prefix",
3205            &directory.stage_directory,
3206        )
3207        .unwrap();
3208        let before = fs::read(directory.archive_path()).unwrap();
3209        assert_eq!(
3210            directory.write_or_verify_archive_bytes_exact_retry(&archive, &parent.stage.archive),
3211            Err(GitSnapshotError::InvalidStage)
3212        );
3213        assert_eq!(fs::read(directory.archive_path()).unwrap(), before);
3214
3215        let operation_id = "operation-exact-retry-descriptor-only";
3216        let directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3217            &parent.isolation_root,
3218            operation_id,
3219        )
3220        .unwrap();
3221        write_private_file_create_new(
3222            &directory.descriptor_path(),
3223            b"descriptor-without-archive",
3224            &directory.stage_directory,
3225        )
3226        .unwrap();
3227        assert_eq!(
3228            GitSnapshotStageDirectory::open_or_create_exact_retry(
3229                &parent.isolation_root,
3230                operation_id,
3231            )
3232            .unwrap_err(),
3233            GitSnapshotError::InvalidStage
3234        );
3235
3236        let operation_id = "operation-exact-retry-extra-entry";
3237        let directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3238            &parent.isolation_root,
3239            operation_id,
3240        )
3241        .unwrap();
3242        write_private_file_create_new(
3243            &directory.stage_directory.path().join("unexpected"),
3244            b"unexpected",
3245            &directory.stage_directory,
3246        )
3247        .unwrap();
3248        assert_eq!(
3249            GitSnapshotStageDirectory::open_or_create_exact_retry(
3250                &parent.isolation_root,
3251                operation_id,
3252            )
3253            .unwrap_err(),
3254            GitSnapshotError::InvalidStage
3255        );
3256
3257        let recovered = reconcile_exact_retry_fixture(
3258            &parent,
3259            "operation-exact-retry-complete-mismatch",
3260            ExactRetryFrontier::Complete,
3261        );
3262        let before = [
3263            fs::read(&recovered.archive_path).unwrap(),
3264            fs::read(&recovered.descriptor_path).unwrap(),
3265            fs::read(&recovered.metadata_path).unwrap(),
3266        ];
3267        let directory = GitSnapshotStageDirectory::open_or_create_exact_retry(
3268            &parent.isolation_root,
3269            &recovered.operation_id,
3270        )
3271        .unwrap();
3272        let mut changed = recovered.stage.clone();
3273        changed.consent_sha256 = hex::encode(Sha256::digest(b"different-lineage-authority"));
3274        assert_eq!(
3275            directory.publish_or_verify_metadata_exact_retry(
3276                &changed,
3277                &recovered.descriptor,
3278                &recovered.request,
3279            ),
3280            Err(GitSnapshotError::InvalidStage)
3281        );
3282        assert_eq!(
3283            [
3284                fs::read(&recovered.archive_path).unwrap(),
3285                fs::read(&recovered.descriptor_path).unwrap(),
3286                fs::read(&recovered.metadata_path).unwrap(),
3287            ],
3288            before
3289        );
3290    }
3291
3292    #[test]
3293    fn complete_stage_discovery_is_bounded_strict_path_free_and_read_only() {
3294        let empty = tempfile::tempdir().unwrap();
3295        let empty_root = empty.path().join("private");
3296        create_private_test_root(&empty_root);
3297        assert!(
3298            discover_complete_git_snapshot_stages(&empty_root)
3299                .unwrap()
3300                .is_empty()
3301        );
3302        assert!(fs::read_dir(&empty_root).unwrap().next().is_none());
3303
3304        let fixture = stage_fixture();
3305        let before = [
3306            fs::read(&fixture.archive_path).unwrap(),
3307            fs::read(&fixture.descriptor_path).unwrap(),
3308            fs::read(&fixture.metadata_path).unwrap(),
3309        ];
3310        let discovered = discover_complete_git_snapshot_stages(&fixture.isolation_root).unwrap();
3311        assert_eq!(discovered.len(), 1);
3312        assert_eq!(discovered[0].locator, fixture.locator);
3313        assert_eq!(discovered[0].stage, fixture.stage);
3314        assert_eq!(discovered[0].descriptor, fixture.descriptor);
3315        assert_eq!(discovered[0].stage.final_request, fixture.request);
3316        assert_eq!(
3317            [
3318                fs::read(&fixture.archive_path).unwrap(),
3319                fs::read(&fixture.descriptor_path).unwrap(),
3320                fs::read(&fixture.metadata_path).unwrap(),
3321            ],
3322            before
3323        );
3324    }
3325
3326    #[test]
3327    fn discovery_fails_the_whole_read_for_partial_unknown_overflow_and_rebound_state() {
3328        let changed = stage_fixture();
3329        assert_eq!(
3330            discover_complete_git_snapshot_stages_with_hook(&changed.isolation_root, || {
3331                GitSnapshotStageDirectory::create(
3332                    &changed.isolation_root,
3333                    "operation-discovery-concurrent-partial",
3334                )
3335                .unwrap();
3336            }),
3337            Err(GitSnapshotError::DiscoveryChanged)
3338        );
3339
3340        let partial = stage_fixture();
3341        GitSnapshotStageDirectory::create(&partial.isolation_root, "operation-discovery-partial")
3342            .unwrap();
3343        assert_eq!(
3344            discover_complete_git_snapshot_stages(&partial.isolation_root),
3345            Err(GitSnapshotError::InvalidStage)
3346        );
3347
3348        let unknown = stage_fixture();
3349        fs::create_dir(
3350            unknown
3351                .isolation_root
3352                .join(GIT_SNAPSHOT_STAGE_DIRECTORY)
3353                .join(".not-an-operation"),
3354        )
3355        .unwrap();
3356        assert_eq!(
3357            discover_complete_git_snapshot_stages(&unknown.isolation_root),
3358            Err(GitSnapshotError::InvalidOperation)
3359        );
3360
3361        let overflow = tempfile::tempdir().unwrap();
3362        let overflow_root = overflow.path().join("private");
3363        create_private_test_root(&overflow_root);
3364        for index in 0..=MAX_GIT_SNAPSHOT_DISCOVERY_STAGES {
3365            GitSnapshotStageDirectory::create(
3366                &overflow_root,
3367                &format!("operation-discovery-overflow-{index:02}"),
3368            )
3369            .unwrap();
3370        }
3371        assert_eq!(
3372            discover_complete_git_snapshot_stages(&overflow_root),
3373            Err(GitSnapshotError::InvalidStage)
3374        );
3375
3376        let rebound = stage_fixture();
3377        let hardlink = rebound
3378            .isolation_root
3379            .join("descriptor-hardlink-outside-stage");
3380        fs::hard_link(&rebound.descriptor_path, &hardlink).unwrap();
3381        assert_eq!(
3382            discover_complete_git_snapshot_stages(&rebound.isolation_root),
3383            Err(GitSnapshotError::InvalidStage)
3384        );
3385    }
3386
3387    #[test]
3388    fn full_refs_are_typed_and_operation_derived() {
3389        let operation = "operation-snapshot-1";
3390        let source = GitSnapshotSourceRef::for_operation(operation).unwrap();
3391        let keepalive = GitSnapshotKeepaliveRef::for_operation(operation).unwrap();
3392        assert_eq!(
3393            source.as_str(),
3394            "refs/rustferry/goal3/snapshots/operation-snapshot-1"
3395        );
3396        assert_eq!(
3397            keepalive.as_str(),
3398            "refs/rustferry/goal3/keepalive/operation-snapshot-1"
3399        );
3400        assert!(GitSnapshotSourceRef::for_operation("../unsafe").is_err());
3401        assert!(GitSnapshotKeepaliveRef::for_operation("name.lock").is_err());
3402        assert!(
3403            serde_json::from_str::<GitSnapshotSourceRef>(
3404                "\"refs/heads/rustferry/goal3/snapshots/operation-snapshot-1\""
3405            )
3406            .is_err()
3407        );
3408    }
3409
3410    #[test]
3411    fn strict_stage_round_trips_and_reopens_exact_bound_files() {
3412        let fixture = stage_fixture();
3413        let bytes = fixture.stage.canonical_bytes(&fixture.descriptor).unwrap();
3414        let decoded = GitSnapshotStageV1::decode(&bytes, &fixture.descriptor).unwrap();
3415        assert_eq!(decoded, fixture.stage);
3416        assert_eq!(
3417            decoded
3418                .verify_staged_files(&fixture.isolation_root)
3419                .unwrap(),
3420            fixture.descriptor
3421        );
3422
3423        let mut noncanonical = bytes;
3424        noncanonical.pop();
3425        assert_eq!(
3426            GitSnapshotStageV1::decode(&noncanonical, &fixture.descriptor),
3427            Err(GitSnapshotError::InvalidEncoding)
3428        );
3429    }
3430
3431    #[test]
3432    fn complete_stage_load_and_no_mutation_adoption_retain_exact_import_inputs() {
3433        let fixture = stage_fixture();
3434        let before = [
3435            fs::read(&fixture.archive_path).unwrap(),
3436            fs::read(&fixture.descriptor_path).unwrap(),
3437            fs::read(&fixture.metadata_path).unwrap(),
3438        ];
3439        let mut loaded = GitSnapshotImportInputs::load(
3440            &fixture.isolation_root,
3441            &fixture.locator,
3442            &fixture.request,
3443        )
3444        .unwrap();
3445        assert_eq!(loaded.stage(), &fixture.stage);
3446        assert_eq!(loaded.descriptor(), &fixture.descriptor);
3447        assert_eq!(
3448            loaded.descriptor_bytes(),
3449            canonical_git_snapshot_descriptor_bytes(&fixture.descriptor).unwrap()
3450        );
3451        let mut archive = Vec::new();
3452        loaded
3453            .archive_reader()
3454            .unwrap()
3455            .read_to_end(&mut archive)
3456            .unwrap();
3457        assert_eq!(archive, before[0]);
3458        loaded.verify_bindings().unwrap();
3459        drop(loaded);
3460
3461        let (adopted_locator, mut adopted) = GitSnapshotImportInputs::adopt_without_locator(
3462            &fixture.isolation_root,
3463            &fixture.request,
3464        )
3465        .unwrap();
3466        assert_eq!(adopted_locator, fixture.locator);
3467        assert_eq!(adopted.locator(), &fixture.locator);
3468        adopted.verify_bindings().unwrap();
3469        drop(adopted);
3470        assert_eq!(
3471            [
3472                fs::read(&fixture.archive_path).unwrap(),
3473                fs::read(&fixture.descriptor_path).unwrap(),
3474                fs::read(&fixture.metadata_path).unwrap(),
3475            ],
3476            before
3477        );
3478        assert_eq!(
3479            GitSnapshotStageDirectory::create(
3480                &fixture.isolation_root,
3481                &fixture.request.operation_id,
3482            )
3483            .unwrap_err(),
3484            GitSnapshotError::StageAlreadyExists
3485        );
3486    }
3487
3488    #[test]
3489    fn locator_and_durable_request_bind_root_store_operation_and_commit() {
3490        let fixture = stage_fixture();
3491        let mut locator = fixture.locator.clone();
3492        locator.isolation_root_identity = locator.snapshots_store_identity.clone();
3493        assert!(
3494            GitSnapshotImportInputs::load(&fixture.isolation_root, &locator, &fixture.request,)
3495                .is_err()
3496        );
3497
3498        let mut request = fixture.request.clone();
3499        request.source_revision = Some("a".repeat(40));
3500        assert!(
3501            GitSnapshotImportInputs::load(&fixture.isolation_root, &fixture.locator, &request,)
3502                .is_err()
3503        );
3504
3505        let mut request = fixture.request.clone();
3506        request.operation_id = "operation-snapshot-stage-other".to_owned();
3507        assert!(
3508            GitSnapshotImportInputs::adopt_without_locator(&fixture.isolation_root, &request)
3509                .is_err()
3510        );
3511    }
3512
3513    #[test]
3514    fn partial_crash_stage_without_metadata_is_cleanup_only() {
3515        let fixture = stage_fixture();
3516        fs::remove_file(&fixture.metadata_path).unwrap();
3517        assert!(
3518            GitSnapshotImportInputs::adopt_without_locator(
3519                &fixture.isolation_root,
3520                &fixture.request,
3521            )
3522            .is_err()
3523        );
3524        assert_eq!(
3525            GitSnapshotStageDirectory::create(
3526                &fixture.isolation_root,
3527                &fixture.request.operation_id,
3528            )
3529            .unwrap_err(),
3530            GitSnapshotError::StageAlreadyExists
3531        );
3532        assert_eq!(
3533            fs::read(&fixture.archive_path).unwrap().len() as u64,
3534            fixture.stage.archive.size
3535        );
3536    }
3537
3538    #[test]
3539    fn exact_stage_deletion_is_durable_and_replayable() {
3540        let fixture = stage_fixture();
3541        let stage_path = fixture.metadata_path.parent().unwrap().to_path_buf();
3542        GitSnapshotImportInputs::delete_stage_exact(
3543            &fixture.isolation_root,
3544            &fixture.locator,
3545            &fixture.stage,
3546            &fixture.request,
3547        )
3548        .unwrap();
3549        assert!(!stage_path.exists());
3550        assert!(
3551            fixture
3552                .isolation_root
3553                .join(GIT_SNAPSHOT_STAGE_DIRECTORY)
3554                .is_dir()
3555        );
3556
3557        GitSnapshotImportInputs::delete_stage_exact(
3558            &fixture.isolation_root,
3559            &fixture.locator,
3560            &fixture.stage,
3561            &fixture.request,
3562        )
3563        .unwrap();
3564    }
3565
3566    #[test]
3567    fn exact_stage_deletion_recovers_identity_bound_partial_stage() {
3568        let fixture = stage_fixture();
3569        let stage_path = fixture.metadata_path.parent().unwrap().to_path_buf();
3570        fs::remove_file(&fixture.archive_path).unwrap();
3571        GitSnapshotImportInputs::delete_stage_exact(
3572            &fixture.isolation_root,
3573            &fixture.locator,
3574            &fixture.stage,
3575            &fixture.request,
3576        )
3577        .unwrap();
3578        assert!(!stage_path.exists());
3579    }
3580
3581    #[test]
3582    fn exact_stage_deletion_rejects_unexpected_or_rebound_entries_before_mutation() {
3583        let fixture = stage_fixture();
3584        let unexpected = fixture.metadata_path.parent().unwrap().join("unexpected");
3585        fs::write(&unexpected, b"unexpected").unwrap();
3586        assert_eq!(
3587            GitSnapshotImportInputs::delete_stage_exact(
3588                &fixture.isolation_root,
3589                &fixture.locator,
3590                &fixture.stage,
3591                &fixture.request,
3592            ),
3593            Err(GitSnapshotError::InvalidStage)
3594        );
3595        assert!(fixture.archive_path.exists());
3596        assert!(fixture.descriptor_path.exists());
3597        assert!(fixture.metadata_path.exists());
3598
3599        let fixture = stage_fixture();
3600        let displaced_metadata = fixture.isolation_root.join("displaced-metadata");
3601        fs::rename(&fixture.metadata_path, &displaced_metadata).unwrap();
3602        fs::write(&fixture.metadata_path, b"replacement").unwrap();
3603        assert_eq!(
3604            GitSnapshotImportInputs::delete_stage_exact(
3605                &fixture.isolation_root,
3606                &fixture.locator,
3607                &fixture.stage,
3608                &fixture.request,
3609            ),
3610            Err(GitSnapshotError::InvalidStage)
3611        );
3612        assert!(fixture.archive_path.exists());
3613        assert!(fixture.descriptor_path.exists());
3614        assert!(fixture.metadata_path.exists());
3615    }
3616
3617    #[test]
3618    fn completed_stage_rejects_metadata_archive_and_entry_tampering() {
3619        let fixture = stage_fixture();
3620        let mut bytes = fs::read(&fixture.metadata_path).unwrap();
3621        *bytes.last_mut().unwrap() = b' ';
3622        fs::write(&fixture.metadata_path, bytes).unwrap();
3623        assert!(
3624            GitSnapshotImportInputs::load(
3625                &fixture.isolation_root,
3626                &fixture.locator,
3627                &fixture.request,
3628            )
3629            .is_err()
3630        );
3631
3632        let fixture = stage_fixture();
3633        fs::write(&fixture.archive_path, b"tampered archive").unwrap();
3634        assert!(
3635            GitSnapshotImportInputs::load(
3636                &fixture.isolation_root,
3637                &fixture.locator,
3638                &fixture.request,
3639            )
3640            .is_err()
3641        );
3642
3643        let fixture = stage_fixture();
3644        let unexpected = fixture.metadata_path.parent().unwrap().join("unexpected");
3645        fs::write(unexpected, b"unexpected").unwrap();
3646        assert!(
3647            GitSnapshotImportInputs::load(
3648                &fixture.isolation_root,
3649                &fixture.locator,
3650                &fixture.request,
3651            )
3652            .is_err()
3653        );
3654    }
3655
3656    #[cfg(unix)]
3657    #[test]
3658    fn post_git_verification_rejects_unix_in_place_mutation_after_archive_eof() {
3659        use std::fs::OpenOptions;
3660        use std::io::Seek as _;
3661
3662        let fixture = stage_fixture();
3663        let mut inputs = GitSnapshotImportInputs::load(
3664            &fixture.isolation_root,
3665            &fixture.locator,
3666            &fixture.request,
3667        )
3668        .unwrap();
3669        let mut consumed = Vec::new();
3670        inputs
3671            .archive_reader()
3672            .unwrap()
3673            .read_to_end(&mut consumed)
3674            .unwrap();
3675        assert_eq!(consumed.len() as u64, fixture.stage.archive.size);
3676
3677        let original_identity = RegularFileFilesystemIdentity::capture(&fixture.archive_path)
3678            .unwrap()
3679            .to_string();
3680        let mut writer = OpenOptions::new()
3681            .write(true)
3682            .open(&fixture.archive_path)
3683            .unwrap();
3684        writer.seek(std::io::SeekFrom::Start(0)).unwrap();
3685        writer.write_all(&[0]).unwrap();
3686        writer.sync_all().unwrap();
3687        drop(writer);
3688        assert_eq!(
3689            RegularFileFilesystemIdentity::capture(&fixture.archive_path)
3690                .unwrap()
3691                .to_string(),
3692            original_identity
3693        );
3694        assert_eq!(
3695            inputs.verify_bindings(),
3696            Err(GitSnapshotError::InvalidStage)
3697        );
3698    }
3699
3700    #[test]
3701    fn stage_reopen_rejects_identity_or_descriptor_byte_drift() {
3702        let mut fixture = stage_fixture();
3703        fixture.stage.descriptor_file_identity = fixture.stage.archive_file_identity.clone();
3704        assert_eq!(
3705            fixture.stage.verify_staged_files(&fixture.isolation_root),
3706            Err(GitSnapshotError::InvalidStage)
3707        );
3708
3709        let fixture = stage_fixture();
3710        let mut bytes = fs::read(&fixture.descriptor_path).unwrap();
3711        let last = bytes.last_mut().unwrap();
3712        *last = b' ';
3713        fs::write(&fixture.descriptor_path, bytes).unwrap();
3714        assert_eq!(
3715            fixture.stage.verify_staged_files(&fixture.isolation_root),
3716            Err(GitSnapshotError::InvalidStage)
3717        );
3718    }
3719
3720    #[test]
3721    fn six_object_tree_hierarchy_is_canonical_and_never_uses_full_path_entries() {
3722        let leaf = canonical_goal3_tree_bytes(&object('1'), &object('2')).unwrap();
3723        assert!(leaf.starts_with(b"100644 source.json\0"));
3724        assert!(
3725            leaf.windows(b"100644 source.zip\0".len())
3726                .any(|window| window == b"100644 source.zip\0")
3727        );
3728        assert!(!leaf.windows(10).any(|window| window.contains(&b'/')));
3729
3730        let middle = canonical_rustferry_tree_bytes(&object('3')).unwrap();
3731        assert!(middle.starts_with(b"40000 goal3\0"));
3732        let root = canonical_root_tree_bytes(&object('4')).unwrap();
3733        assert!(root.starts_with(b"40000 .rustferry\0"));
3734
3735        let mut malformed = Vec::new();
3736        assert_eq!(
3737            append_tree_entry(
3738                &mut malformed,
3739                "100644",
3740                ".rustferry/goal3/source.json",
3741                &object('1')
3742            ),
3743            Err(GitSnapshotError::InvalidTreeEntry)
3744        );
3745    }
3746
3747    #[test]
3748    #[allow(clippy::too_many_lines)]
3749    fn no_write_precompute_and_import_rehash_the_same_parentless_six_object_graph() {
3750        let temporary = tempfile::tempdir().unwrap();
3751        let git_directory = temporary.path().join("repository.git");
3752        let initialized = Command::new("git")
3753            .args(["init", "--bare", "--quiet"])
3754            .arg(&git_directory)
3755            .output()
3756            .expect("initialize bare Git repository");
3757        assert!(initialized.status.success());
3758        let format = Command::new("git")
3759            .arg("--git-dir")
3760            .arg(&git_directory)
3761            .args(["rev-parse", "--show-object-format"])
3762            .output()
3763            .unwrap();
3764        assert!(format.status.success());
3765        require_sha1_object_format(&format.stdout).unwrap();
3766
3767        let archive_bytes = b"deterministic source archive fixture\n";
3768        let descriptor_bytes = b"canonical descriptor fixture\n";
3769        let archive_blob = hash_git_object(
3770            &git_directory,
3771            GitSnapshotObjectKind::Blob,
3772            false,
3773            archive_bytes,
3774        );
3775        let descriptor_blob = hash_git_object(
3776            &git_directory,
3777            GitSnapshotObjectKind::Blob,
3778            false,
3779            descriptor_bytes,
3780        );
3781        let graph = complete_git_snapshot_object_graph::<GitSnapshotError>(
3782            archive_blob.clone(),
3783            descriptor_blob.clone(),
3784            "operation-snapshot-graph-1",
3785            1_234_567,
3786            |kind, bytes| Ok(hash_git_object(&git_directory, kind, false, bytes)),
3787        )
3788        .unwrap();
3789        let absent = Command::new("git")
3790            .arg("--git-dir")
3791            .arg(&git_directory)
3792            .args([
3793                "cat-file",
3794                "-e",
3795                &format!("{}^{{commit}}", graph.commit.as_str()),
3796            ])
3797            .stdout(Stdio::null())
3798            .stderr(Stdio::null())
3799            .status()
3800            .unwrap();
3801        assert!(!absent.success());
3802
3803        assert_eq!(
3804            hash_git_object(
3805                &git_directory,
3806                GitSnapshotObjectKind::Blob,
3807                true,
3808                archive_bytes,
3809            ),
3810            archive_blob
3811        );
3812        assert_eq!(
3813            hash_git_object(
3814                &git_directory,
3815                GitSnapshotObjectKind::Blob,
3816                true,
3817                descriptor_bytes,
3818            ),
3819            descriptor_blob
3820        );
3821        let tampered_archive_blob = hash_git_object(
3822            &git_directory,
3823            GitSnapshotObjectKind::Blob,
3824            true,
3825            b"different staged archive bytes\n",
3826        );
3827        assert_ne!(tampered_archive_blob, graph.archive_blob);
3828        assert_eq!(
3829            graph.verify_rehashed::<GitSnapshotError>(
3830                tampered_archive_blob,
3831                descriptor_blob.clone(),
3832                "operation-snapshot-graph-1",
3833                1_234_567,
3834                |kind, bytes| Ok(hash_git_object(&git_directory, kind, true, bytes)),
3835            ),
3836            Err(GitSnapshotError::ObjectGraphMismatch)
3837        );
3838        graph
3839            .verify_rehashed::<GitSnapshotError>(
3840                archive_blob,
3841                descriptor_blob,
3842                "operation-snapshot-graph-1",
3843                1_234_567,
3844                |kind, bytes| Ok(hash_git_object(&git_directory, kind, true, bytes)),
3845            )
3846            .unwrap();
3847
3848        let names = Command::new("git")
3849            .arg("--git-dir")
3850            .arg(&git_directory)
3851            .args(["ls-tree", "-r", "--name-only", "-z", graph.commit.as_str()])
3852            .output()
3853            .unwrap();
3854        assert!(names.status.success());
3855        assert_eq!(
3856            names.stdout,
3857            b".rustferry/goal3/source.json\0.rustferry/goal3/source.zip\0"
3858        );
3859        let parents = Command::new("git")
3860            .arg("--git-dir")
3861            .arg(&git_directory)
3862            .args(["rev-list", "--parents", "-n", "1", graph.commit.as_str()])
3863            .output()
3864            .unwrap();
3865        assert!(parents.status.success());
3866        assert_eq!(
3867            String::from_utf8(parents.stdout).unwrap(),
3868            format!("{}\n", graph.commit.as_str())
3869        );
3870    }
3871
3872    #[test]
3873    #[allow(clippy::too_many_lines)]
3874    fn retained_stage_inputs_are_the_blob_authority_for_import_rehash() {
3875        let fixture = stage_fixture();
3876        let git_temporary = tempfile::tempdir().unwrap();
3877        let git_directory = git_temporary.path().join("repository.git");
3878        let initialized = Command::new("git")
3879            .args(["init", "--bare", "--quiet"])
3880            .arg(&git_directory)
3881            .output()
3882            .expect("initialize bare Git repository");
3883        assert!(initialized.status.success());
3884        let format = Command::new("git")
3885            .arg("--git-dir")
3886            .arg(&git_directory)
3887            .args(["rev-parse", "--show-object-format"])
3888            .output()
3889            .unwrap();
3890        assert!(format.status.success());
3891        require_sha1_object_format(&format.stdout).unwrap();
3892
3893        fs::remove_file(&fixture.metadata_path).unwrap();
3894        let directories =
3895            GitSnapshotStageDirectory::open(&fixture.isolation_root, &fixture.request.operation_id)
3896                .unwrap();
3897        let archive_identity =
3898            RegularFileFilesystemIdentity::from_str(&fixture.stage.archive_file_identity).unwrap();
3899        let descriptor_identity =
3900            RegularFileFilesystemIdentity::from_str(&fixture.stage.descriptor_file_identity)
3901                .unwrap();
3902        let mut precompute = directories
3903            .precompute_inputs(
3904                &archive_identity,
3905                &descriptor_identity,
3906                &fixture.stage.archive,
3907                &fixture.descriptor,
3908            )
3909            .unwrap();
3910        let archive_blob = {
3911            let archive = precompute.archive_reader().unwrap();
3912            hash_git_object_file(&git_directory, GitSnapshotObjectKind::Blob, false, archive)
3913        };
3914        precompute.verify_contents().unwrap();
3915        let descriptor_blob = hash_git_object(
3916            &git_directory,
3917            GitSnapshotObjectKind::Blob,
3918            false,
3919            precompute.descriptor_bytes(),
3920        );
3921        precompute.verify_contents().unwrap();
3922        let graph = complete_git_snapshot_object_graph::<GitSnapshotError>(
3923            archive_blob,
3924            descriptor_blob,
3925            &fixture.request.operation_id,
3926            fixture.stage.source_created_at_ms,
3927            |kind, bytes| Ok(hash_git_object(&git_directory, kind, false, bytes)),
3928        )
3929        .unwrap();
3930        drop(precompute);
3931
3932        let mut request = fixture.request.clone();
3933        request.source_revision = Some(graph.commit.as_str().to_owned());
3934        let mut stage = fixture.stage.clone();
3935        stage.graph = graph.clone();
3936        stage.final_request = request.clone();
3937        let locator = directories
3938            .publish_metadata_create_new(&stage, &fixture.descriptor, &request)
3939            .unwrap();
3940        drop(directories);
3941
3942        let mut inputs =
3943            GitSnapshotImportInputs::load(&fixture.isolation_root, &locator, &request).unwrap();
3944        // The format gate is deliberately above both blob imports.
3945        let imported_archive = {
3946            let archive = inputs.archive_reader().unwrap();
3947            hash_git_object_file(&git_directory, GitSnapshotObjectKind::Blob, true, archive)
3948        };
3949        inputs
3950            .verify_imported_archive_blob(&imported_archive)
3951            .unwrap();
3952        let imported_descriptor = hash_git_object(
3953            &git_directory,
3954            GitSnapshotObjectKind::Blob,
3955            true,
3956            inputs.descriptor_bytes(),
3957        );
3958        inputs
3959            .verify_imported_descriptor_blob(&imported_descriptor)
3960            .unwrap();
3961        assert_eq!(imported_archive, stage.graph.archive_blob);
3962        assert_eq!(imported_descriptor, stage.graph.descriptor_blob);
3963        stage
3964            .graph
3965            .verify_rehashed::<GitSnapshotError>(
3966                imported_archive,
3967                imported_descriptor,
3968                &stage.operation_id,
3969                stage.source_created_at_ms,
3970                |kind, bytes| Ok(hash_git_object(&git_directory, kind, true, bytes)),
3971            )
3972            .unwrap();
3973        inputs.verify_bindings().unwrap();
3974    }
3975
3976    #[test]
3977    fn parentless_commit_bytes_bind_actor_time_root_and_operation() {
3978        let bytes = canonical_parentless_snapshot_commit_bytes(
3979            &object('5'),
3980            "operation-snapshot-1",
3981            1_234_567,
3982        )
3983        .unwrap();
3984        assert_eq!(
3985            String::from_utf8(bytes).unwrap(),
3986            concat!(
3987                "tree 5555555555555555555555555555555555555555\n",
3988                "author ShiroKSH <kushidashiro@gmail.com> 1234 +0000\n",
3989                "committer ShiroKSH <kushidashiro@gmail.com> 1234 +0000\n",
3990                "\n",
3991                "RustFerry Git snapshot operation-snapshot-1\n",
3992            )
3993        );
3994    }
3995
3996    #[test]
3997    fn object_ids_are_exact_lowercase_sha1_names() {
3998        assert!(GitSha1ObjectId::new("a".repeat(40)).is_ok());
3999        for invalid in ["a".repeat(39), "A".repeat(40), "g".repeat(40)] {
4000            assert!(GitSha1ObjectId::new(invalid).is_err());
4001        }
4002    }
4003
4004    #[test]
4005    fn imported_blob_mismatch_stops_before_any_tree_or_commit_write() {
4006        let graph = GitSnapshotObjectGraphV1 {
4007            schema_version: GIT_SNAPSHOT_GRAPH_SCHEMA_VERSION,
4008            archive_blob: object('1'),
4009            descriptor_blob: object('2'),
4010            goal3_tree: object('3'),
4011            rustferry_tree: object('4'),
4012            root_tree: object('5'),
4013            commit: object('6'),
4014        };
4015        let mut calls = 0_u8;
4016        assert_eq!(
4017            graph.verify_rehashed::<GitSnapshotError>(
4018                object('a'),
4019                object('2'),
4020                "operation-snapshot-1",
4021                1_234_567,
4022                |_, _| {
4023                    calls = calls.saturating_add(1);
4024                    Ok(object('f'))
4025                },
4026            ),
4027            Err(GitSnapshotError::ObjectGraphMismatch)
4028        );
4029        assert_eq!(calls, 0);
4030    }
4031
4032    #[test]
4033    fn object_format_gate_accepts_only_exact_sha1_probe_output() {
4034        assert_eq!(require_sha1_object_format(b"sha1\n"), Ok(()));
4035        for invalid in [
4036            b"sha256\n".as_slice(),
4037            b"sha1 sha256\n".as_slice(),
4038            b"sha1".as_slice(),
4039            b"SHA1\n".as_slice(),
4040        ] {
4041            assert_eq!(
4042                require_sha1_object_format(invalid),
4043                Err(GitSnapshotError::UnsupportedObjectFormat)
4044            );
4045        }
4046    }
4047
4048    #[test]
4049    fn git_rejects_the_malformed_flat_full_path_tree_shape() {
4050        let mut malformed = Vec::new();
4051        malformed.extend_from_slice(b"100644 .rustferry/goal3/source.json\0");
4052        malformed.extend_from_slice(&object('1').raw().unwrap());
4053        malformed.extend_from_slice(b"100644 .rustferry/goal3/source.zip\0");
4054        malformed.extend_from_slice(&object('2').raw().unwrap());
4055
4056        let mut child = Command::new("git")
4057            .args(["hash-object", "-t", "tree", "--stdin"])
4058            .stdin(Stdio::piped())
4059            .stdout(Stdio::piped())
4060            .stderr(Stdio::piped())
4061            .spawn()
4062            .expect("spawn Git tree parser");
4063        child
4064            .stdin
4065            .take()
4066            .expect("Git stdin")
4067            .write_all(&malformed)
4068            .expect("write malformed tree");
4069        let output = child.wait_with_output().expect("wait for Git");
4070        assert!(!output.status.success());
4071    }
4072}