1use thiserror::Error;
2
3pub const CODEX_WS_APT_PACKAGES_ENV: &str = "CODEX_WS_APT_PACKAGES";
5
6pub const CODEX_WS_SETUP_COMMANDS_ENV: &str = "CODEX_WS_SETUP_COMMANDS";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct RuntimeEnvironmentVariable {
12 name: &'static str,
13 value: String,
14}
15
16impl RuntimeEnvironmentVariable {
17 #[must_use]
28 pub fn new(name: &'static str, value: String) -> Self {
29 Self { name, value }
30 }
31
32 #[must_use]
38 pub const fn name(&self) -> &'static str {
39 self.name
40 }
41
42 #[must_use]
48 pub fn value(&self) -> &str {
49 &self.value
50 }
51
52 #[must_use]
58 pub fn docker_assignment(&self) -> String {
59 format!("{}={}", self.name, self.value)
60 }
61}
62
63pub fn validate_apt_packages(packages: Vec<String>) -> Result<Vec<String>, RuntimeSpecError> {
78 let mut validated_packages = Vec::with_capacity(packages.len());
79 for package in packages {
80 let package = package.trim().to_owned();
81 if package.is_empty() {
82 return Err(RuntimeSpecError::EmptyAptPackage);
83 }
84 if !is_valid_apt_package(&package) {
85 return Err(RuntimeSpecError::InvalidAptPackage { package });
86 }
87 validated_packages.push(package);
88 }
89
90 Ok(validated_packages)
91}
92
93pub fn validate_setup_commands(commands: Vec<String>) -> Result<Vec<String>, RuntimeSpecError> {
107 let mut validated_commands = Vec::with_capacity(commands.len());
108 for command in commands {
109 let command = command.trim().to_owned();
110 if command.is_empty() {
111 return Err(RuntimeSpecError::EmptySetupCommand);
112 }
113 validated_commands.push(command);
114 }
115
116 Ok(validated_commands)
117}
118
119fn is_valid_apt_package(package: &str) -> bool {
120 package.bytes().all(|byte| {
121 byte.is_ascii_alphanumeric()
122 || matches!(byte, b'+' | b'-' | b'.' | b'_' | b':' | b'=' | b'~')
123 })
124}
125
126#[derive(Debug, Error, PartialEq, Eq)]
128pub enum RuntimeSpecError {
129 #[error("runtime apt package cannot be empty")]
131 EmptyAptPackage,
132
133 #[error("invalid runtime apt package '{package}'")]
135 InvalidAptPackage {
136 package: String,
138 },
139
140 #[error("runtime setup command cannot be empty")]
142 EmptySetupCommand,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn validate_apt_packages_accepts_common_package_syntax() {
151 let packages = validate_apt_packages(vec![
152 " python3 ".to_owned(),
153 "libssl-dev:amd64".to_owned(),
154 "nodejs=22.0.0-1nodesource1".to_owned(),
155 ])
156 .expect("apt packages should validate");
157
158 assert_eq!(
159 packages,
160 vec![
161 "python3".to_owned(),
162 "libssl-dev:amd64".to_owned(),
163 "nodejs=22.0.0-1nodesource1".to_owned()
164 ]
165 );
166 }
167
168 #[test]
169 fn validate_apt_packages_rejects_shell_metacharacters() {
170 let error = validate_apt_packages(vec!["python3;curl".to_owned()])
171 .expect_err("shell metacharacters should fail");
172
173 assert!(matches!(
174 error,
175 RuntimeSpecError::InvalidAptPackage { package } if package == "python3;curl"
176 ));
177 }
178
179 #[test]
180 fn validate_setup_commands_rejects_empty_commands() {
181 let error =
182 validate_setup_commands(vec![" ".to_owned()]).expect_err("blank command should fail");
183
184 assert_eq!(error, RuntimeSpecError::EmptySetupCommand);
185 }
186}