syncable_cli/analyzer/hadolint/rules/
dl3045.rs1use crate::analyzer::hadolint::parser::instruction::Instruction;
7use crate::analyzer::hadolint::rules::{custom_rule, CustomRule, RuleState};
8use crate::analyzer::hadolint::shell::ParsedShell;
9use crate::analyzer::hadolint::types::Severity;
10
11pub fn rule() -> CustomRule<impl Fn(&mut RuleState, u32, &Instruction, Option<&ParsedShell>) + Send + Sync> {
12 custom_rule(
13 "DL3045",
14 Severity::Warning,
15 "`COPY` to a relative destination without `WORKDIR` set.",
16 |state, line, instr, _shell| {
17 match instr {
18 Instruction::From(base) => {
19 let stage_name = base.alias.as_ref()
21 .map(|a| a.as_str().to_string())
22 .unwrap_or_else(|| base.image.name.clone());
23 state.data.set_string("current_stage", &stage_name);
24
25 let parent_had_workdir = state.data.set_contains("stages_with_workdir", &base.image.name);
27 if parent_had_workdir {
28 state.data.insert_to_set("stages_with_workdir", &stage_name);
29 }
30 }
31 Instruction::Workdir(_) => {
32 let stage = state.data.get_string("current_stage")
34 .map(|s| s.to_string())
35 .unwrap_or_else(|| "__none__".to_string());
36 state.data.insert_to_set("stages_with_workdir", &stage);
37 }
38 Instruction::Copy(args, _) => {
39 let dest = &args.dest;
40
41 let has_workdir = state.data.get_string("current_stage")
43 .map(|s| state.data.set_contains("stages_with_workdir", s))
44 .unwrap_or_else(|| state.data.set_contains("stages_with_workdir", "__none__"));
45
46 if has_workdir {
48 return;
49 }
50
51 let trimmed = dest.trim_matches(|c| c == '"' || c == '\'');
53
54 if trimmed.starts_with('/') {
56 return;
57 }
58
59 if is_windows_absolute(trimmed) {
61 return;
62 }
63
64 if trimmed.starts_with('$') {
66 return;
67 }
68
69 state.add_failure(
71 "DL3045",
72 Severity::Warning,
73 "`COPY` to a relative destination without `WORKDIR` set.",
74 line,
75 );
76 }
77 _ => {}
78 }
79 },
80 )
81}
82
83fn is_windows_absolute(path: &str) -> bool {
85 let chars: Vec<char> = path.chars().collect();
86 chars.len() >= 2 && chars[0].is_ascii_alphabetic() && chars[1] == ':'
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::analyzer::hadolint::parser::instruction::{BaseImage, CopyArgs, CopyFlags};
93 use crate::analyzer::hadolint::rules::Rule;
94
95 #[test]
96 fn test_absolute_dest() {
97 let rule = rule();
98 let mut state = RuleState::new();
99
100 let from = Instruction::From(BaseImage::new("ubuntu"));
101 let copy = Instruction::Copy(
102 CopyArgs::new(vec!["app.js".to_string()], "/app/"),
103 CopyFlags::default(),
104 );
105
106 rule.check(&mut state, 1, &from, None);
107 rule.check(&mut state, 2, ©, None);
108 assert!(state.failures.is_empty());
109 }
110
111 #[test]
112 fn test_relative_dest_without_workdir() {
113 let rule = rule();
114 let mut state = RuleState::new();
115
116 let from = Instruction::From(BaseImage::new("ubuntu"));
117 let copy = Instruction::Copy(
118 CopyArgs::new(vec!["app.js".to_string()], "app/"),
119 CopyFlags::default(),
120 );
121
122 rule.check(&mut state, 1, &from, None);
123 rule.check(&mut state, 2, ©, None);
124 assert_eq!(state.failures.len(), 1);
125 assert_eq!(state.failures[0].code.as_str(), "DL3045");
126 }
127
128 #[test]
129 fn test_relative_dest_with_workdir() {
130 let rule = rule();
131 let mut state = RuleState::new();
132
133 let from = Instruction::From(BaseImage::new("ubuntu"));
134 let workdir = Instruction::Workdir("/app".to_string());
135 let copy = Instruction::Copy(
136 CopyArgs::new(vec!["app.js".to_string()], "."),
137 CopyFlags::default(),
138 );
139
140 rule.check(&mut state, 1, &from, None);
141 rule.check(&mut state, 2, &workdir, None);
142 rule.check(&mut state, 3, ©, None);
143 assert!(state.failures.is_empty());
144 }
145
146 #[test]
147 fn test_variable_dest() {
148 let rule = rule();
149 let mut state = RuleState::new();
150
151 let from = Instruction::From(BaseImage::new("ubuntu"));
152 let copy = Instruction::Copy(
153 CopyArgs::new(vec!["app.js".to_string()], "$APP_DIR"),
154 CopyFlags::default(),
155 );
156
157 rule.check(&mut state, 1, &from, None);
158 rule.check(&mut state, 2, ©, None);
159 assert!(state.failures.is_empty());
160 }
161}