1use std::collections::HashMap;
2
3#[derive(Copy, Clone, Debug, Eq, PartialEq)]
4pub enum Command {
5 Workdir,
6 Workspace,
7 Env,
8 Echo,
9 Run,
10 RunBg,
11 Copy,
12 Capture,
13 CopyGit,
14 Symlink,
15 Mkdir,
16 Ls,
17 Cwd,
18 Cat,
19 Write,
20 Exit,
21}
22
23pub const COMMANDS: &[Command] = &[
24 Command::Workdir,
25 Command::Workspace,
26 Command::Env,
27 Command::Echo,
28 Command::Run,
29 Command::RunBg,
30 Command::Copy,
31 Command::Capture,
32 Command::CopyGit,
33 Command::Symlink,
34 Command::Mkdir,
35 Command::Ls,
36 Command::Cwd,
37 Command::Cat,
38 Command::Write,
39 Command::Exit,
40];
41
42impl Command {
43 pub const fn as_str(self) -> &'static str {
44 match self {
45 Command::Workdir => "WORKDIR",
46 Command::Workspace => "WORKSPACE",
47 Command::Env => "ENV",
48 Command::Echo => "ECHO",
49 Command::Run => "RUN",
50 Command::RunBg => "RUN_BG",
51 Command::Copy => "COPY",
52 Command::Capture => "CAPTURE",
53 Command::CopyGit => "COPY_GIT",
54 Command::Symlink => "SYMLINK",
55 Command::Mkdir => "MKDIR",
56 Command::Ls => "LS",
57 Command::Cwd => "CWD",
58 Command::Cat => "CAT",
59 Command::Write => "WRITE",
60 Command::Exit => "EXIT",
61 }
62 }
63
64 pub const fn expects_inner_command(self) -> bool {
65 matches!(self, Command::Capture)
66 }
67
68 pub fn parse(s: &str) -> Option<Self> {
69 match s {
70 "WORKDIR" => Some(Command::Workdir),
71 "WORKSPACE" => Some(Command::Workspace),
72 "ENV" => Some(Command::Env),
73 "ECHO" => Some(Command::Echo),
74 "RUN" => Some(Command::Run),
75 "RUN_BG" => Some(Command::RunBg),
76 "COPY" => Some(Command::Copy),
77 "CAPTURE" => Some(Command::Capture),
78 "COPY_GIT" => Some(Command::CopyGit),
79 "SYMLINK" => Some(Command::Symlink),
80 "MKDIR" => Some(Command::Mkdir),
81 "LS" => Some(Command::Ls),
82 "CWD" => Some(Command::Cwd),
83 "CAT" => Some(Command::Cat),
84 "WRITE" => Some(Command::Write),
85 "EXIT" => Some(Command::Exit),
86 _ => None,
87 }
88 }
89}
90
91#[derive(Copy, Clone, Debug, Eq, PartialEq)]
92pub enum PlatformGuard {
93 Unix,
94 Windows,
95 Macos,
96 Linux,
97}
98
99#[derive(Debug, Clone, Eq, PartialEq)]
100pub enum Guard {
101 Platform {
102 target: PlatformGuard,
103 invert: bool,
104 },
105 EnvExists {
106 key: String,
107 invert: bool,
108 },
109 EnvEquals {
110 key: String,
111 value: String,
112 invert: bool,
113 },
114}
115
116#[derive(Debug, Clone, Eq, PartialEq)]
117pub enum StepKind {
118 Workdir(String),
119 Workspace(WorkspaceTarget),
120 Env {
121 key: String,
122 value: String,
123 },
124 Run(String),
125 Echo(String),
126 RunBg(String),
127 Copy {
128 from: String,
129 to: String,
130 },
131 Symlink {
132 from: String,
133 to: String,
134 },
135 Mkdir(String),
136 Ls(Option<String>),
137 Cwd,
138 Cat(String),
139 Write {
140 path: String,
141 contents: String,
142 },
143 Capture {
144 path: String,
145 cmd: String,
146 },
147 CopyGit {
148 rev: String,
149 from: String,
150 to: String,
151 },
152 Exit(i32),
153}
154
155#[derive(Debug, Clone, Eq, PartialEq)]
156pub struct Step {
157 pub guards: Vec<Vec<Guard>>,
158 pub kind: StepKind,
159 pub scope_enter: usize,
160 pub scope_exit: usize,
161}
162
163#[derive(Debug, Clone, Eq, PartialEq)]
164pub enum WorkspaceTarget {
165 Snapshot,
166 Local,
167}
168
169fn platform_matches(target: PlatformGuard) -> bool {
170 #[allow(clippy::disallowed_macros)]
171 match target {
172 PlatformGuard::Unix => cfg!(unix),
173 PlatformGuard::Windows => cfg!(windows),
174 PlatformGuard::Macos => cfg!(target_os = "macos"),
175 PlatformGuard::Linux => cfg!(target_os = "linux"),
176 }
177}
178
179pub fn guard_allows(guard: &Guard, script_envs: &HashMap<String, String>) -> bool {
180 match guard {
181 Guard::Platform { target, invert } => {
182 let res = platform_matches(*target);
183 if *invert { !res } else { res }
184 }
185 Guard::EnvExists { key, invert } => {
186 let res = script_envs
187 .get(key)
188 .cloned()
189 .or_else(|| std::env::var(key).ok())
190 .map(|v| !v.is_empty())
191 .unwrap_or(false);
192 if *invert { !res } else { res }
193 }
194 Guard::EnvEquals { key, value, invert } => {
195 let res = script_envs
196 .get(key)
197 .cloned()
198 .or_else(|| std::env::var(key).ok())
199 .map(|v| v == *value)
200 .unwrap_or(false);
201 if *invert { !res } else { res }
202 }
203 }
204}
205
206pub fn guard_group_allows(group: &[Guard], script_envs: &HashMap<String, String>) -> bool {
207 group.iter().all(|g| guard_allows(g, script_envs))
208}
209
210pub fn guards_allow_any(groups: &[Vec<Guard>], script_envs: &HashMap<String, String>) -> bool {
211 if groups.is_empty() {
212 return true;
213 }
214 groups.iter().any(|g| guard_group_allows(g, script_envs))
215}
216
217use std::fmt;
218
219impl fmt::Display for PlatformGuard {
220 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221 match self {
222 PlatformGuard::Unix => write!(f, "unix"),
223 PlatformGuard::Windows => write!(f, "windows"),
224 PlatformGuard::Macos => write!(f, "macos"),
225 PlatformGuard::Linux => write!(f, "linux"),
226 }
227 }
228}
229
230impl fmt::Display for Guard {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Guard::Platform { target, invert } => {
234 if *invert {
235 write!(f, "!")?
236 }
237 write!(f, "platform:{}", target)
238 }
239 Guard::EnvExists { key, invert } => {
240 if *invert {
241 write!(f, "!")?
242 }
243 write!(f, "env:{}", key)
244 }
245 Guard::EnvEquals { key, value, invert } => {
246 if *invert {
247 write!(f, "!")?
248 }
249 write!(f, "env:{}={}", key, value)
250 }
251 }
252 }
253}
254
255impl fmt::Display for WorkspaceTarget {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257 match self {
258 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
259 WorkspaceTarget::Local => write!(f, "LOCAL"),
260 }
261 }
262}
263
264fn quote_arg(s: &str) -> String {
265 let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
269 && !s.starts_with(|c: char| c.is_ascii_digit());
270 if is_safe && !s.is_empty() {
271 s.to_string()
272 } else {
273 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
274 }
275}
276
277fn quote_msg(s: &str) -> String {
278 let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
284 && !s.starts_with(|c: char| c.is_ascii_digit());
285
286 if is_safe && !s.is_empty() {
287 s.to_string()
288 } else {
289 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
290 }
291}
292
293fn quote_run(s: &str) -> String {
294 let force_full_quote = s.is_empty()
302 || s.chars().any(|c| c == ';' || c == '\n' || c == '\r')
303 || s.contains("//")
304 || s.contains("/*");
305
306 if force_full_quote {
307 return format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
308 }
309
310 s.split(' ')
311 .map(|word| {
312 let needs_quote = word.starts_with(|c: char| c.is_ascii_digit())
313 || word.starts_with(['/', '.', '-', ':', '=']);
314 if needs_quote {
315 format!("\"{}\"", word.replace('\\', "\\\\").replace('"', "\\\""))
316 } else {
317 word.to_string()
318 }
319 })
320 .collect::<Vec<_>>()
321 .join(" ")
322}
323
324impl fmt::Display for StepKind {
325 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326 match self {
327 StepKind::Workdir(arg) => write!(f, "WORKDIR {}", quote_arg(arg)),
328 StepKind::Workspace(target) => write!(f, "WORKSPACE {}", target),
329 StepKind::Env { key, value } => write!(f, "ENV {}={}", key, quote_arg(value)),
330 StepKind::Run(cmd) => write!(f, "RUN {}", quote_run(cmd)),
331 StepKind::Echo(msg) => write!(f, "ECHO {}", quote_msg(msg)),
332 StepKind::RunBg(cmd) => write!(f, "RUN_BG {}", quote_run(cmd)),
333 StepKind::Copy { from, to } => write!(f, "COPY {} {}", quote_arg(from), quote_arg(to)),
334 StepKind::Symlink { from, to } => {
335 write!(f, "SYMLINK {} {}", quote_arg(from), quote_arg(to))
336 }
337 StepKind::Mkdir(arg) => write!(f, "MKDIR {}", quote_arg(arg)),
338 StepKind::Ls(arg) => {
339 write!(f, "LS")?;
340 if let Some(a) = arg {
341 write!(f, " {}", quote_arg(a))?;
342 }
343 Ok(())
344 }
345 StepKind::Cwd => write!(f, "CWD"),
346 StepKind::Cat(arg) => write!(f, "CAT {}", quote_arg(arg)),
347 StepKind::Write { path, contents } => {
348 write!(f, "WRITE {} {}", quote_arg(path), quote_msg(contents))
349 }
350 StepKind::Capture { path, cmd } => {
351 write!(f, "CAPTURE {} {}", quote_arg(path), quote_run(cmd))
352 }
353 StepKind::CopyGit { rev, from, to } => write!(
354 f,
355 "COPY_GIT {} {} {}",
356 quote_arg(rev),
357 quote_arg(from),
358 quote_arg(to)
359 ),
360 StepKind::Exit(code) => write!(f, "EXIT {}", code),
361 }
362 }
363}
364
365impl fmt::Display for Step {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 for group in &self.guards {
368 write!(f, "[")?;
369 for (i, guard) in group.iter().enumerate() {
370 if i > 0 {
371 write!(f, ", ")?
372 }
373 write!(f, "{}", guard)?;
374 }
375 write!(f, "] ")?;
376 }
377 write!(f, "{}", self.kind)
378 }
379}