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, 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}