syncable_cli/analyzer/hadolint/rules/
dl3051.rs1use crate::analyzer::hadolint::parser::instruction::Instruction;
6use crate::analyzer::hadolint::rules::{simple_rule, SimpleRule};
7use crate::analyzer::hadolint::shell::ParsedShell;
8use crate::analyzer::hadolint::types::Severity;
9
10pub fn rule() -> SimpleRule<impl Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync> {
11 simple_rule(
12 "DL3051",
13 Severity::Warning,
14 "Label `org.opencontainers.image.created` is empty or not a valid RFC3339 date.",
15 |instr, _shell| {
16 match instr {
17 Instruction::Label(pairs) => {
18 for (key, value) in pairs {
19 if key == "org.opencontainers.image.created" {
20 if value.is_empty() || !is_valid_rfc3339(value) {
21 return false;
22 }
23 }
24 }
25 true
26 }
27 _ => true,
28 }
29 },
30 )
31}
32
33fn is_valid_rfc3339(date: &str) -> bool {
34 if date.len() < 20 {
37 return false;
38 }
39
40 let chars: Vec<char> = date.chars().collect();
41
42 if chars.len() < 10 {
44 return false;
45 }
46
47 if !chars[0..4].iter().all(|c| c.is_ascii_digit()) { return false; }
49 if chars[4] != '-' { return false; }
50 if !chars[5..7].iter().all(|c| c.is_ascii_digit()) { return false; }
51 if chars[7] != '-' { return false; }
52 if !chars[8..10].iter().all(|c| c.is_ascii_digit()) { return false; }
53
54 if chars.get(10) != Some(&'T') && chars.get(10) != Some(&'t') {
56 return false;
57 }
58
59 if chars.len() < 19 { return false; }
61 if !chars[11..13].iter().all(|c| c.is_ascii_digit()) { return false; }
62 if chars[13] != ':' { return false; }
63 if !chars[14..16].iter().all(|c| c.is_ascii_digit()) { return false; }
64 if chars[16] != ':' { return false; }
65 if !chars[17..19].iter().all(|c| c.is_ascii_digit()) { return false; }
66
67 if chars.len() == 20 && chars[19] == 'Z' {
69 return true;
70 }
71
72 let tz_start = if chars.get(19) == Some(&'.') {
74 let mut i = 20;
76 while i < chars.len() && chars[i].is_ascii_digit() {
77 i += 1;
78 }
79 i
80 } else {
81 19
82 };
83
84 if chars.len() > tz_start {
85 let tz_char = chars[tz_start];
86 if tz_char == 'Z' || tz_char == 'z' {
87 return true;
88 }
89 if (tz_char == '+' || tz_char == '-') && chars.len() >= tz_start + 6 {
90 return true;
91 }
92 }
93
94 false
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::analyzer::hadolint::lint::{lint, LintResult};
101 use crate::analyzer::hadolint::config::HadolintConfig;
102
103 fn lint_dockerfile(content: &str) -> LintResult {
104 lint(content, &HadolintConfig::default())
105 }
106
107 #[test]
108 fn test_valid_date() {
109 let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"2023-01-15T14:30:00Z\"");
110 assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
111 }
112
113 #[test]
114 fn test_empty_date() {
115 let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"\"");
116 assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
117 }
118
119 #[test]
120 fn test_invalid_date() {
121 let result = lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"not-a-date\"");
122 assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
123 }
124}