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    RegistryCredential,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[serde(deny_unknown_fields)]
69pub struct SecretReference {
70    pub name: String,
71    /// Opaque reference resolved by the provider integration. It is never the
72    /// secret value itself.
73    pub reference: String,
74    pub target: SecretTarget,
75}
76
77impl SecretReference {
78    pub(crate) fn validate(&self) -> Result<(), String> {
79        super::validate_name("secret name", &self.name)?;
80        super::validate_nonempty("secret reference", &self.reference, 1024)?;
81        match &self.target {
82            SecretTarget::Environment { variable } => validate_environment_name(variable),
83            SecretTarget::File { path, mode } => {
84                super::validate_absolute_path("secret file path", path)?;
85                if *mode == 0 || *mode > 0o777 {
86                    return Err("secret file mode must be between 0001 and 0777".into());
87                }
88                Ok(())
89            }
90            SecretTarget::RegistryCredential => Ok(()),
91        }
92    }
93}