soma_infra/
build_context.rs1use 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#[derive(Debug, Clone)]
16pub struct BuildContextPolicy {
17 roots: FileReadPolicy,
18 max_files: u32,
19 max_bytes: u64,
20}
21
22impl BuildContextPolicy {
23 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 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 pub fn roots(&self) -> impl Iterator<Item = &Path> {
57 self.roots.roots()
58 }
59
60 #[must_use]
62 pub const fn max_files(&self) -> u32 {
63 self.max_files
64 }
65
66 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct BuildContextFingerprint {
81 pub host: HostId,
83 pub topology_revision: TopologyRevision,
85 pub path: PathBuf,
87 pub sha256: String,
89 pub file_count: u32,
91 pub byte_count: u64,
93}
94
95impl BuildContextFingerprint {
96 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#[async_trait]
115pub trait BuildContextInspector: Send + Sync {
116 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;