Skip to main content

phoxal_bundle/
artifact.rs

1//! Indexed executable artifact records.
2
3use std::fmt;
4use std::io::{Seek, SeekFrom};
5use std::path::{Path, PathBuf};
6
7use phoxal_runtime_contract::identity::ParticipantArtifactId;
8use phoxal_runtime_contract::metadata::ParticipantContract;
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    BIN_DIR, BundleError, BundlePath, DocumentError, Sha256Digest, open_executable_source,
13};
14
15/// One opened executable source pinned to the exact file descriptor the
16/// writer will hash and copy.
17///
18/// Opening is no-follow on supported platforms. Keeping the descriptor owned
19/// closes the check/open race that a path-only writer would otherwise have.
20pub struct BinarySource {
21    file: std::fs::File,
22    path: PathBuf,
23}
24
25impl fmt::Debug for BinarySource {
26    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27        formatter
28            .debug_struct("BinarySource")
29            .field("path", &self.path)
30            .finish_non_exhaustive()
31    }
32}
33
34impl BinarySource {
35    /// Open one executable source through a descriptor-pinned parent walk,
36    /// without following a substituted leaf.
37    pub fn open(path: impl AsRef<Path>) -> Result<Self, BundleError> {
38        let path = path.as_ref().to_path_buf();
39        let file = open_executable_source(&path)?;
40        Ok(Self { file, path })
41    }
42
43    /// The source pathname retained only for diagnostics.
44    #[must_use]
45    pub fn path(&self) -> &Path {
46        &self.path
47    }
48
49    pub(crate) fn reader(&self) -> Result<std::fs::File, BundleError> {
50        let mut file = self
51            .file
52            .try_clone()
53            .map_err(|source| BundleError::ReadFile {
54                path: self.path.clone(),
55                source,
56            })?;
57        file.seek(SeekFrom::Start(0))
58            .map_err(|source| BundleError::ReadFile {
59                path: self.path.clone(),
60                source,
61            })?;
62        Ok(file)
63    }
64}
65
66/// A staged reusable executable and its canonical artifact contract.
67#[derive(Clone, Debug, Deserialize, Serialize)]
68#[serde(deny_unknown_fields)]
69pub struct BinaryReference {
70    pub(crate) path: BundlePath,
71    pub(crate) digest: Sha256Digest,
72    pub(crate) size_bytes: u64,
73    pub(crate) contract: ParticipantContract,
74}
75
76impl BinaryReference {
77    /// Construct a reference for an already-built executable source.
78    pub fn from_source(
79        path: BundlePath,
80        contract: ParticipantContract,
81        source: &BinarySource,
82    ) -> Result<Self, BundleError> {
83        let file = source.reader()?;
84        let size_bytes = file
85            .metadata()
86            .map_err(|source_error| BundleError::ReadFile {
87                path: source.path.clone(),
88                source: source_error,
89            })?
90            .len();
91        Ok(Self {
92            path,
93            digest: Sha256Digest::from_reader(file).map_err(|source_error| {
94                BundleError::ReadFile {
95                    path: source.path.clone(),
96                    source: source_error,
97                }
98            })?,
99            size_bytes,
100            contract,
101        })
102    }
103
104    #[must_use]
105    pub const fn path(&self) -> &BundlePath {
106        &self.path
107    }
108    #[must_use]
109    pub const fn digest(&self) -> Sha256Digest {
110        self.digest
111    }
112    #[must_use]
113    pub const fn size_bytes(&self) -> u64 {
114        self.size_bytes
115    }
116    #[must_use]
117    pub const fn contract(&self) -> &ParticipantContract {
118        &self.contract
119    }
120
121    pub(crate) fn validate(&self, id: &ParticipantArtifactId) -> Result<(), DocumentError> {
122        if self.contract.id != *id {
123            return Err(DocumentError::ArtifactContractMismatch {
124                artifact: id.clone(),
125                contract: self.contract.id.clone(),
126            });
127        }
128        if !self.path.starts_with_directory(BIN_DIR) {
129            return Err(DocumentError::ArtifactOutsideBin {
130                artifact: id.clone(),
131                path: self.path.clone(),
132            });
133        }
134        Ok(())
135    }
136}