Skip to main content

a3s_runtime/contract/
process.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6pub struct RuntimeProcessSpec {
7    /// An empty command uses the runnable artifact's declared entrypoint.
8    pub command: Vec<String>,
9    pub args: Vec<String>,
10    pub working_directory: Option<String>,
11    pub environment: BTreeMap<String, String>,
12}
13
14impl RuntimeProcessSpec {
15    pub(crate) fn validate(&self) -> Result<(), String> {
16        if self.command.len() > 64 || self.args.len() > 256 {
17            return Err("process command or argument count exceeds Runtime limits".into());
18        }
19        for value in self.command.iter().chain(self.args.iter()) {
20            validate_process_value(value)?;
21        }
22        if let Some(path) = &self.working_directory {
23            super::validate_absolute_path("working_directory", path)?;
24        }
25        if self.environment.len() > 512 {
26            return Err("process environment exceeds 512 entries".into());
27        }
28        for (name, value) in &self.environment {
29            validate_environment_name(name)?;
30            if value.len() > 32 * 1024 || value.contains('\0') {
31                return Err(format!("environment value for {name:?} is invalid"));
32            }
33        }
34        Ok(())
35    }
36}
37
38fn validate_process_value(value: &str) -> Result<(), String> {
39    if value.is_empty() || value.len() > 32 * 1024 || value.contains('\0') {
40        return Err("process command values must be nonempty bounded non-NUL strings".into());
41    }
42    Ok(())
43}
44
45pub(crate) fn validate_environment_name(value: &str) -> Result<(), String> {
46    let mut bytes = value.bytes();
47    let Some(first) = bytes.next() else {
48        return Err("environment variable name must not be empty".into());
49    };
50    if !(first.is_ascii_alphabetic() || first == b'_')
51        || !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
52        || value.len() > 255
53    {
54        return Err(format!("invalid environment variable name {value:?}"));
55    }
56    Ok(())
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
61pub enum SecretTarget {
62    Environment { variable: String },
63    File { path: String, mode: u32 },
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68pub struct SecretReference {
69    pub name: String,
70    /// Opaque reference resolved by the provider integration. It is never the
71    /// secret value itself.
72    pub reference: String,
73    pub target: SecretTarget,
74}
75
76impl SecretReference {
77    pub(crate) fn validate(&self) -> Result<(), String> {
78        super::validate_name("secret name", &self.name)?;
79        super::validate_nonempty("secret reference", &self.reference, 1024)?;
80        match &self.target {
81            SecretTarget::Environment { variable } => validate_environment_name(variable),
82            SecretTarget::File { path, mode } => {
83                super::validate_absolute_path("secret file path", path)?;
84                if *mode == 0 || *mode > 0o777 {
85                    return Err("secret file mode must be between 0001 and 0777".into());
86                }
87                Ok(())
88            }
89        }
90    }
91}