Skip to main content

a3s_runtime/contract/
mod.rs

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