Skip to main content

soma_infra/
build_context.rs

1use std::path::{Path, PathBuf};
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use soma_fleet::{HostId, HostRecord, TopologyRevision};
6use soma_ops::Timestamp;
7use tokio_util::sync::CancellationToken;
8
9use crate::{FileReadPolicy, InfraError, InfraResult};
10
11const MAX_CONTEXT_FILES: u32 = 100_000;
12const MAX_CONTEXT_BYTES: u64 = 8 * 1024 * 1024 * 1024;
13
14/// Explicit roots and traversal ceilings for Docker build contexts.
15#[derive(Debug, Clone)]
16pub struct BuildContextPolicy {
17    roots: FileReadPolicy,
18    max_files: u32,
19    max_bytes: u64,
20}
21
22impl BuildContextPolicy {
23    /// Creates a build policy from absolute admitted roots.
24    pub fn new<I, P>(roots: I) -> InfraResult<Self>
25    where
26        I: IntoIterator<Item = P>,
27        P: Into<PathBuf>,
28    {
29        Ok(Self {
30            roots: FileReadPolicy::new(roots)?,
31            max_files: 25_000,
32            max_bytes: 2 * 1024 * 1024 * 1024,
33        })
34    }
35
36    /// Sets bounded context file and byte ceilings.
37    pub fn with_limits(mut self, max_files: u32, max_bytes: u64) -> InfraResult<Self> {
38        if max_files == 0 || max_files > MAX_CONTEXT_FILES {
39            return Err(InfraError::InvalidRequest {
40                domain: "build-context",
41                message: format!("file limit must be 1-{MAX_CONTEXT_FILES}"),
42            });
43        }
44        if max_bytes == 0 || max_bytes > MAX_CONTEXT_BYTES {
45            return Err(InfraError::InvalidRequest {
46                domain: "build-context",
47                message: format!("byte limit must be 1-{MAX_CONTEXT_BYTES}"),
48            });
49        }
50        self.max_files = max_files;
51        self.max_bytes = max_bytes;
52        Ok(self)
53    }
54
55    /// Returns admitted roots.
56    pub fn roots(&self) -> impl Iterator<Item = &Path> {
57        self.roots.roots()
58    }
59
60    /// Returns the file ceiling.
61    #[must_use]
62    pub const fn max_files(&self) -> u32 {
63        self.max_files
64    }
65
66    /// Returns the byte ceiling.
67    #[must_use]
68    pub const fn max_bytes(&self) -> u64 {
69        self.max_bytes
70    }
71
72    #[cfg(any(feature = "process-driver", test))]
73    pub(crate) fn resolve(&self, path: &Path) -> InfraResult<(PathBuf, PathBuf)> {
74        self.roots.resolve(path)
75    }
76}
77
78/// Deterministic content fingerprint for one admitted build context.
79#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct BuildContextFingerprint {
81    /// Target host.
82    pub host: HostId,
83    /// Exact topology revision.
84    pub topology_revision: TopologyRevision,
85    /// Absolute build context path.
86    pub path: PathBuf,
87    /// Lowercase SHA-256 over relative paths, modes, sizes, and regular-file content.
88    pub sha256: String,
89    /// Number of regular files hashed.
90    pub file_count: u32,
91    /// Total regular-file bytes hashed.
92    pub byte_count: u64,
93}
94
95impl BuildContextFingerprint {
96    /// Validates the fingerprint wire representation.
97    pub fn validate(&self) -> InfraResult<()> {
98        if self.sha256.len() != 64
99            || !self
100                .sha256
101                .bytes()
102                .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
103        {
104            return Err(InfraError::Parse {
105                domain: "build-context",
106                message: "context fingerprint is not lowercase SHA-256".into(),
107            });
108        }
109        Ok(())
110    }
111}
112
113/// Reads one build context through descriptor-confined traversal.
114#[async_trait]
115pub trait BuildContextInspector: Send + Sync {
116    /// Computes one bounded deterministic context fingerprint.
117    async fn fingerprint(
118        &self,
119        host: &HostRecord,
120        path: &Path,
121        deadline: Timestamp,
122        cancellation: &CancellationToken,
123    ) -> InfraResult<BuildContextFingerprint>;
124}
125
126#[cfg(test)]
127#[path = "build_context_tests.rs"]
128mod tests;