a3s_runtime/contract/
mod.rs1mod artifact;
2mod capabilities;
3mod network;
4mod observation;
5mod process;
6mod protocol;
7mod resource;
8mod unit;
9
10pub use artifact::{ArtifactRef, RuntimeOutputArtifact};
11pub use capabilities::{ResourceControl, RuntimeCapabilities, RuntimeFeature};
12pub use network::{
13 NetworkMode, RuntimeNetworkSpec, RuntimePort, RuntimeServiceEndpoint, TransportProtocol,
14};
15pub use observation::{
16 RuntimeEvidence, RuntimeFailure, RuntimeHealthObservation, RuntimeHealthState,
17 RuntimeInspection, RuntimeObservation, RuntimeUnitState, RuntimeUsage,
18};
19pub use process::{RuntimeProcessSpec, SecretReference, SecretTarget};
20pub use protocol::{
21 RuntimeActionRequest, RuntimeApplyRequest, RuntimeExecRequest, RuntimeExecResult,
22 RuntimeLogChunk, RuntimeLogDiscontinuityReason, RuntimeLogQuery, RuntimeLogStream,
23 RuntimeRemoval,
24};
25pub use resource::{IsolationLevel, ResourceLimits};
26pub use unit::{
27 HealthCheckKind, HealthProbe, MountKind, RestartPolicy, RuntimeHealthCheck, RuntimeMount,
28 RuntimeMountSource, RuntimeOutputSpec, RuntimeServiceLifecycle, RuntimeUnitClass,
29 RuntimeUnitSpec,
30};
31
32pub(crate) fn validate_digest(value: &str) -> Result<(), String> {
33 let Some(hex) = value.strip_prefix("sha256:") else {
34 return Err("digest must use sha256".into());
35 };
36 if hex.len() != 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
37 return Err("digest must contain exactly 64 hexadecimal characters".into());
38 }
39 Ok(())
40}
41
42pub(crate) fn validate_nonempty(label: &str, value: &str, max: usize) -> Result<(), String> {
43 if value.is_empty() || value.len() > max || value.contains('\0') || value.contains(['\r', '\n'])
44 {
45 return Err(format!(
46 "{label} must be a bounded nonempty single-line value"
47 ));
48 }
49 Ok(())
50}
51
52pub(crate) fn validate_id(label: &str, value: &str, max: usize) -> Result<(), String> {
53 validate_nonempty(label, value, max)?;
54 if value
55 .bytes()
56 .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_.:/".contains(&byte)))
57 {
58 return Err(format!("{label} contains unsupported characters"));
59 }
60 Ok(())
61}
62
63pub(crate) fn validate_name(label: &str, value: &str) -> Result<(), String> {
64 validate_nonempty(label, value, 255)?;
65 if value
66 .bytes()
67 .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_ .".contains(&byte)))
68 || value.starts_with(['-', '_', '.', ' '])
69 || value.ends_with(['-', '_', '.', ' '])
70 {
71 return Err(format!("{label} contains unsupported characters"));
72 }
73 Ok(())
74}
75
76pub(crate) fn validate_absolute_path(label: &str, value: &str) -> Result<(), String> {
77 if !value.starts_with('/')
78 || value.len() > 4096
79 || value.contains('\0')
80 || value.split('/').any(|segment| segment == "..")
81 {
82 return Err(format!(
83 "{label} must be a bounded absolute path without '..'"
84 ));
85 }
86 Ok(())
87}
88
89pub(crate) fn validate_uri(label: &str, value: &str) -> Result<(), String> {
90 validate_nonempty(label, value, 4096)?;
91 let Some((scheme, rest)) = value.split_once("://") else {
92 return Err(format!("{label} must contain a URI scheme"));
93 };
94 if scheme.is_empty()
95 || rest.is_empty()
96 || !scheme
97 .bytes()
98 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
99 {
100 return Err(format!("{label} is invalid"));
101 }
102 Ok(())
103}