1use std::collections::HashMap;
2
3#[derive(Copy, Clone, Debug, Eq, PartialEq)]
4pub enum Command {
5 InheritEnv,
6 Workdir,
7 Workspace,
8 Env,
9 Echo,
10 Run,
11 RunBg,
12 Copy,
13 WithIo,
14 CopyGit,
15 HashSha256,
16 Symlink,
17 Mkdir,
18 Ls,
19 Cwd,
20 Read,
21 Write,
22 Exit,
23}
24
25pub const COMMANDS: &[Command] = &[
26 Command::InheritEnv,
27 Command::Workdir,
28 Command::Workspace,
29 Command::Env,
30 Command::Echo,
31 Command::Run,
32 Command::RunBg,
33 Command::Copy,
34 Command::WithIo,
35 Command::CopyGit,
36 Command::HashSha256,
37 Command::Symlink,
38 Command::Mkdir,
39 Command::Ls,
40 Command::Cwd,
41 Command::Read,
42 Command::Write,
43 Command::Exit,
44];
45
46impl Command {
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Command::InheritEnv => "INHERIT_ENV",
50 Command::Workdir => "WORKDIR",
51 Command::Workspace => "WORKSPACE",
52 Command::Env => "ENV",
53 Command::Echo => "ECHO",
54 Command::Run => "RUN",
55 Command::RunBg => "RUN_BG",
56 Command::Copy => "COPY",
57 Command::WithIo => "WITH_IO",
58 Command::CopyGit => "COPY_GIT",
59 Command::HashSha256 => "HASH_SHA256",
60 Command::Symlink => "SYMLINK",
61 Command::Mkdir => "MKDIR",
62 Command::Ls => "LS",
63 Command::Cwd => "CWD",
64 Command::Read => "READ",
65 Command::Write => "WRITE",
66 Command::Exit => "EXIT",
67 }
68 }
69
70 pub const fn expects_inner_command(self) -> bool {
71 matches!(self, Command::WithIo)
72 }
73
74 pub fn parse(s: &str) -> Option<Self> {
75 match s {
76 "INHERIT_ENV" => Some(Command::InheritEnv),
77 "WORKDIR" => Some(Command::Workdir),
78 "WORKSPACE" => Some(Command::Workspace),
79 "ENV" => Some(Command::Env),
80 "ECHO" => Some(Command::Echo),
81 "RUN" => Some(Command::Run),
82 "RUN_BG" => Some(Command::RunBg),
83 "COPY" => Some(Command::Copy),
84 "WITH_IO" => Some(Command::WithIo),
85 "COPY_GIT" => Some(Command::CopyGit),
86 "HASH_SHA256" => Some(Command::HashSha256),
87 "SYMLINK" => Some(Command::Symlink),
88 "MKDIR" => Some(Command::Mkdir),
89 "LS" => Some(Command::Ls),
90 "CWD" => Some(Command::Cwd),
91 "READ" => Some(Command::Read),
92 "WRITE" => Some(Command::Write),
93 "EXIT" => Some(Command::Exit),
94 _ => None,
95 }
96 }
97}
98
99#[derive(Copy, Clone, Debug, Eq, PartialEq)]
100pub enum PlatformGuard {
101 Unix,
102 Windows,
103 Macos,
104 Linux,
105}
106
107#[derive(Debug, Clone, Eq, PartialEq)]
108pub enum Guard {
109 Platform {
110 target: PlatformGuard,
111 invert: bool,
112 },
113 EnvExists {
114 key: String,
115 invert: bool,
116 },
117 EnvEquals {
118 key: String,
119 value: String,
120 invert: bool,
121 },
122}
123
124#[derive(Debug, Clone, Eq, PartialEq)]
125pub enum GuardExpr {
126 Predicate(Guard),
127 All(Vec<GuardExpr>),
128 Or(Vec<GuardExpr>),
129 Not(Box<GuardExpr>),
130}
131
132impl GuardExpr {
133 pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
134 let mut flat = Vec::new();
135 for expr in exprs {
136 match expr {
137 GuardExpr::All(children) => flat.extend(children),
138 other => flat.push(other),
139 }
140 }
141 match flat.len() {
142 0 => panic!("GuardExpr::all requires at least one expression"),
143 1 => flat.into_iter().next().unwrap(),
144 _ => GuardExpr::All(flat),
145 }
146 }
147
148 pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
149 let mut flat = Vec::new();
150 for expr in exprs {
151 match expr {
152 GuardExpr::Or(children) => flat.extend(children),
153 other => flat.push(other),
154 }
155 }
156 match flat.len() {
157 0 => panic!("GuardExpr::or requires at least one expression"),
158 1 => flat.into_iter().next().unwrap(),
159 _ => GuardExpr::Or(flat),
160 }
161 }
162
163 pub fn invert(expr: GuardExpr) -> GuardExpr {
164 match expr {
165 GuardExpr::Not(inner) => *inner,
166 other => GuardExpr::Not(Box::new(other)),
167 }
168 }
169}
170
171impl std::ops::Not for GuardExpr {
172 type Output = GuardExpr;
173
174 fn not(self) -> GuardExpr {
175 match self {
176 GuardExpr::Not(inner) => *inner,
177 other => GuardExpr::Not(Box::new(other)),
178 }
179 }
180}
181
182impl From<Guard> for GuardExpr {
183 fn from(guard: Guard) -> Self {
184 GuardExpr::Predicate(guard)
185 }
186}
187
188#[derive(Debug, Clone, Eq, PartialEq)]
189pub struct TemplateString(pub String);
190
191impl From<String> for TemplateString {
192 fn from(s: String) -> Self {
193 TemplateString(s)
194 }
195}
196
197impl From<&str> for TemplateString {
198 fn from(s: &str) -> Self {
199 TemplateString(s.to_string())
200 }
201}
202
203impl std::fmt::Display for TemplateString {
204 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205 write!(f, "{}", self.0)
206 }
207}
208
209impl AsRef<str> for TemplateString {
210 fn as_ref(&self) -> &str {
211 &self.0
212 }
213}
214
215impl PartialEq<str> for TemplateString {
216 fn eq(&self, other: &str) -> bool {
217 self.0 == other
218 }
219}
220
221impl PartialEq<&str> for TemplateString {
222 fn eq(&self, other: &&str) -> bool {
223 self.0 == *other
224 }
225}
226
227impl std::ops::Deref for TemplateString {
228 type Target = str;
229
230 fn deref(&self) -> &Self::Target {
231 &self.0
232 }
233}
234
235#[derive(Debug, Clone, Eq, PartialEq)]
236pub enum IoStream {
237 Stdin,
238 Stdout,
239 Stderr,
240}
241
242#[derive(Debug, Clone, Eq, PartialEq)]
243pub struct IoBinding {
244 pub stream: IoStream,
245 pub pipe: Option<String>,
246}
247
248#[derive(Debug, Clone, Eq, PartialEq)]
249pub enum StepKind {
250 Workdir(TemplateString),
251 Workspace(WorkspaceTarget),
252 Env {
253 key: String,
254 value: TemplateString,
255 },
256 InheritEnv {
259 keys: Vec<String>,
260 },
261 Run(TemplateString),
262 Echo(TemplateString),
263 RunBg(TemplateString),
264 Copy {
265 from_current_workspace: bool,
266 from: TemplateString,
267 to: TemplateString,
268 },
269 Symlink {
270 from: TemplateString,
271 to: TemplateString,
272 },
273 Mkdir(TemplateString),
274 Ls(Option<TemplateString>),
275 Cwd,
276 Read(Option<TemplateString>),
277 Write {
278 path: TemplateString,
279 contents: Option<TemplateString>,
280 },
281 WithIo {
282 bindings: Vec<IoBinding>,
283 cmd: Box<StepKind>,
284 },
285 WithIoBlock {
286 bindings: Vec<IoBinding>,
287 },
288 CopyGit {
289 rev: TemplateString,
290 from: TemplateString,
291 to: TemplateString,
292 include_dirty: bool,
293 },
294 HashSha256 {
295 path: TemplateString,
296 },
297 Exit(i32),
298}
299
300#[derive(Debug, Clone, Eq, PartialEq)]
301pub struct Step {
302 pub guard: Option<GuardExpr>,
303 pub kind: StepKind,
304 pub scope_enter: usize,
305 pub scope_exit: usize,
306}
307
308#[derive(Debug, Clone, Eq, PartialEq)]
309pub enum WorkspaceTarget {
310 Snapshot,
311 Local,
312}
313
314fn platform_matches(target: PlatformGuard) -> bool {
315 #[allow(clippy::disallowed_macros)]
316 match target {
317 PlatformGuard::Unix => cfg!(unix),
318 PlatformGuard::Windows => cfg!(windows),
319 PlatformGuard::Macos => cfg!(target_os = "macos"),
320 PlatformGuard::Linux => cfg!(target_os = "linux"),
321 }
322}
323
324pub fn guard_allows(guard: &Guard, script_envs: &HashMap<String, String>) -> bool {
325 match guard {
326 Guard::Platform { target, invert } => {
327 let res = platform_matches(*target);
328 if *invert { !res } else { res }
329 }
330 Guard::EnvExists { key, invert } => {
331 let res = script_envs
332 .get(key)
333 .cloned()
334 .or_else(|| std::env::var(key).ok())
335 .map(|v| !v.is_empty())
336 .unwrap_or(false);
337 if *invert { !res } else { res }
338 }
339 Guard::EnvEquals { key, value, invert } => {
340 let res = script_envs
341 .get(key)
342 .cloned()
343 .or_else(|| std::env::var(key).ok())
344 .map(|v| v == *value)
345 .unwrap_or(false);
346 if *invert { !res } else { res }
347 }
348 }
349}
350
351pub fn guard_expr_allows(expr: &GuardExpr, script_envs: &HashMap<String, String>) -> bool {
352 match expr {
353 GuardExpr::Predicate(guard) => guard_allows(guard, script_envs),
354 GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, script_envs)),
355 GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, script_envs)),
356 GuardExpr::Not(child) => !guard_expr_allows(child, script_envs),
357 }
358}
359
360pub fn guard_option_allows(
361 expr: Option<&GuardExpr>,
362 script_envs: &HashMap<String, String>,
363) -> bool {
364 match expr {
365 Some(e) => guard_expr_allows(e, script_envs),
366 None => true,
367 }
368}
369
370use std::fmt;
371
372impl fmt::Display for PlatformGuard {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 match self {
375 PlatformGuard::Unix => write!(f, "unix"),
376 PlatformGuard::Windows => write!(f, "windows"),
377 PlatformGuard::Macos => write!(f, "macos"),
378 PlatformGuard::Linux => write!(f, "linux"),
379 }
380 }
381}
382
383impl fmt::Display for Guard {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 match self {
386 Guard::Platform { target, invert } => {
387 if *invert {
388 write!(f, "!{}", target)
389 } else {
390 write!(f, "{}", target)
391 }
392 }
393 Guard::EnvExists { key, invert } => {
394 if *invert {
395 write!(f, "!")?
396 }
397 write!(f, "env:{}", key)
398 }
399 Guard::EnvEquals { key, value, invert } => {
400 if *invert {
401 write!(f, "env:{}!={}", key, value)
402 } else {
403 write!(f, "env:{}=={}", key, value)
404 }
405 }
406 }
407 }
408}
409
410impl fmt::Display for WorkspaceTarget {
411 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
412 match self {
413 WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
414 WorkspaceTarget::Local => write!(f, "LOCAL"),
415 }
416 }
417}
418
419fn quote_arg(s: &str) -> String {
420 let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
424 && !s.starts_with(|c: char| c.is_ascii_digit())
425 && super::Command::parse(s).is_none();
428 if is_safe && !s.is_empty() {
429 s.to_string()
430 } else {
431 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
432 }
433}
434
435fn quote_msg(s: &str) -> String {
436 let is_safe = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
442 && !s.starts_with(|c: char| c.is_ascii_digit())
443 && super::Command::parse(s).is_none();
445
446 if is_safe && !s.is_empty() {
447 s.to_string()
448 } else {
449 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
450 }
451}
452
453fn quote_run(s: &str) -> String {
454 let force_full_quote = s.is_empty()
462 || s.chars().any(|c| c == ';' || c == '\n' || c == '\r')
463 || s.contains("//")
464 || s.contains("/*");
465
466 if force_full_quote {
467 return format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""));
468 }
469
470 s.split(' ')
471 .map(|word| {
472 let needs_quote = word.starts_with(|c: char| c.is_ascii_digit())
473 || word.starts_with(['/', '.', '-', ':', '=']);
474 if needs_quote {
475 format!("\"{}\"", word.replace('\\', "\\\\").replace('"', "\\\""))
476 } else {
477 word.to_string()
478 }
479 })
480 .collect::<Vec<_>>()
481 .join(" ")
482}
483
484fn format_io_binding(binding: &IoBinding) -> String {
485 let stream = match binding.stream {
486 IoStream::Stdin => "stdin",
487 IoStream::Stdout => "stdout",
488 IoStream::Stderr => "stderr",
489 };
490 if let Some(pipe) = &binding.pipe {
491 format!("{}=pipe:{}", stream, pipe)
492 } else {
493 stream.to_string()
494 }
495}
496
497impl fmt::Display for StepKind {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 match self {
500 StepKind::InheritEnv { keys } => {
501 write!(f, "INHERIT_ENV [{}]", keys.join(", "))
502 }
503 StepKind::Workdir(arg) => write!(f, "WORKDIR {}", quote_arg(arg)),
504 StepKind::Workspace(target) => write!(f, "WORKSPACE {}", target),
505 StepKind::Env { key, value } => write!(f, "ENV {}={}", key, quote_arg(value)),
506 StepKind::Run(cmd) => write!(f, "RUN {}", quote_run(cmd)),
507 StepKind::Echo(msg) => write!(f, "ECHO {}", quote_msg(msg)),
508 StepKind::RunBg(cmd) => write!(f, "RUN_BG {}", quote_run(cmd)),
509 StepKind::Copy {
510 from_current_workspace,
511 from,
512 to,
513 } => {
514 if *from_current_workspace {
515 write!(
516 f,
517 "COPY --from-current-workspace {} {}",
518 quote_arg(from),
519 quote_arg(to)
520 )
521 } else {
522 write!(f, "COPY {} {}", quote_arg(from), quote_arg(to))
523 }
524 }
525 StepKind::Symlink { from, to } => {
526 write!(f, "SYMLINK {} {}", quote_arg(from), quote_arg(to))
527 }
528 StepKind::Mkdir(arg) => write!(f, "MKDIR {}", quote_arg(arg)),
529 StepKind::Ls(arg) => {
530 write!(f, "LS")?;
531 if let Some(a) = arg {
532 write!(f, " {}", quote_arg(a))?;
533 }
534 Ok(())
535 }
536 StepKind::Cwd => write!(f, "CWD"),
537 StepKind::Read(arg) => {
538 write!(f, "READ")?;
539 if let Some(a) = arg {
540 write!(f, " {}", quote_arg(a))?;
541 }
542 Ok(())
543 }
544 StepKind::Write { path, contents } => {
545 write!(f, "WRITE {}", quote_arg(path))?;
546 if let Some(body) = contents {
547 write!(f, " {}", quote_msg(body))?;
548 }
549 Ok(())
550 }
551 StepKind::WithIo { bindings, cmd } => {
552 let parts: Vec<String> = bindings.iter().map(format_io_binding).collect();
553 write!(f, "WITH_IO [{}] {}", parts.join(", "), cmd)
554 }
555 StepKind::WithIoBlock { bindings } => {
556 let parts: Vec<String> = bindings.iter().map(format_io_binding).collect();
557 write!(f, "WITH_IO [{}] {{...}}", parts.join(", "))
558 }
559 StepKind::CopyGit {
560 rev,
561 from,
562 to,
563 include_dirty,
564 } => {
565 if *include_dirty {
566 write!(
567 f,
568 "COPY_GIT --include-dirty {} {} {}",
569 quote_arg(rev),
570 quote_arg(from),
571 quote_arg(to)
572 )
573 } else {
574 write!(
575 f,
576 "COPY_GIT {} {} {}",
577 quote_arg(rev),
578 quote_arg(from),
579 quote_arg(to)
580 )
581 }
582 }
583 StepKind::HashSha256 { path } => write!(f, "HASH_SHA256 {}", quote_arg(path)),
584 StepKind::Exit(code) => write!(f, "EXIT {}", code),
585 }
586 }
587}
588
589enum GuardDisplayContext {
590 Root,
591 InOrArg,
592 InNot,
593 InAll,
594}
595
596impl GuardExpr {
597 fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
598 match self {
599 GuardExpr::Predicate(guard) => write!(f, "{}", guard),
600 GuardExpr::All(children) => {
601 let wrap = matches!(
602 ctx,
603 GuardDisplayContext::InOrArg | GuardDisplayContext::InNot
604 ) && children.len() > 1;
605 if wrap {
606 write!(f, "(")?;
607 }
608 for (i, child) in children.iter().enumerate() {
609 if i > 0 {
610 write!(f, ", ")?;
611 }
612 child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
613 }
614 if wrap {
615 write!(f, ")")?;
616 }
617 Ok(())
618 }
619 GuardExpr::Or(children) => {
620 write!(f, "or(")?;
621 for (i, child) in children.iter().enumerate() {
622 if i > 0 {
623 write!(f, ", ")?;
624 }
625 child.fmt_with_ctx(f, GuardDisplayContext::InOrArg)?;
626 }
627 write!(f, ")")
628 }
629 GuardExpr::Not(child) => {
630 write!(f, "!")?;
631 let needs_paren =
632 !matches!(child.as_ref(), GuardExpr::Predicate(_) | GuardExpr::Not(_));
633 if needs_paren {
634 write!(f, "(")?;
635 }
636 child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
637 if needs_paren {
638 write!(f, ")")?;
639 }
640 Ok(())
641 }
642 }
643 }
644}
645
646impl fmt::Display for GuardExpr {
647 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
648 self.fmt_with_ctx(f, GuardDisplayContext::Root)
649 }
650}
651
652impl fmt::Display for Step {
653 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
654 if let Some(expr) = &self.guard {
655 write!(f, "[{}] ", expr)?;
656 }
657 write!(f, "{}", self.kind)
658 }
659}