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::{
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, RuntimeUnitClass, RuntimeUnitSpec,
29};
30
31pub(crate) fn validate_digest(value: &str) -> Result<(), String> {
32    let Some(hex) = value.strip_prefix("sha256:") else {
33        return Err("digest must use sha256".into());
34    };
35    if hex.len() != 64 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
36        return Err("digest must contain exactly 64 hexadecimal characters".into());
37    }
38    Ok(())
39}
40
41pub(crate) fn validate_nonempty(label: &str, value: &str, max: usize) -> Result<(), String> {
42    if value.is_empty() || value.len() > max || value.contains('\0') || value.contains(['\r', '\n'])
43    {
44        return Err(format!(
45            "{label} must be a bounded nonempty single-line value"
46        ));
47    }
48    Ok(())
49}
50
51pub(crate) fn validate_id(label: &str, value: &str, max: usize) -> Result<(), String> {
52    validate_nonempty(label, value, max)?;
53    if value
54        .bytes()
55        .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_.:/".contains(&byte)))
56    {
57        return Err(format!("{label} contains unsupported characters"));
58    }
59    Ok(())
60}
61
62pub(crate) fn validate_name(label: &str, value: &str) -> Result<(), String> {
63    validate_nonempty(label, value, 255)?;
64    if value
65        .bytes()
66        .any(|byte| !(byte.is_ascii_alphanumeric() || b"-_ .".contains(&byte)))
67        || value.starts_with(['-', '_', '.', ' '])
68        || value.ends_with(['-', '_', '.', ' '])
69    {
70        return Err(format!("{label} contains unsupported characters"));
71    }
72    Ok(())
73}
74
75pub(crate) fn validate_absolute_path(label: &str, value: &str) -> Result<(), String> {
76    if !value.starts_with('/')
77        || value.len() > 4096
78        || value.contains('\0')
79        || value.split('/').any(|segment| segment == "..")
80    {
81        return Err(format!(
82            "{label} must be a bounded absolute path without '..'"
83        ));
84    }
85    Ok(())
86}
87
88pub(crate) fn validate_uri(label: &str, value: &str) -> Result<(), String> {
89    validate_nonempty(label, value, 4096)?;
90    let Some((scheme, rest)) = value.split_once("://") else {
91        return Err(format!("{label} must contain a URI scheme"));
92    };
93    if scheme.is_empty()
94        || rest.is_empty()
95        || !scheme
96            .bytes()
97            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
98    {
99        return Err(format!("{label} is invalid"));
100    }
101    Ok(())
102}