syncable_cli/analyzer/hadolint/rules/
dl3051.rs1use crate::analyzer::hadolint::parser::instruction::Instruction;
6use crate::analyzer::hadolint::rules::{SimpleRule, simple_rule};
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| match instr {
16 Instruction::Label(pairs) => {
17 for (key, value) in pairs {
18 if key == "org.opencontainers.image.created"
19 && (value.is_empty() || !is_valid_rfc3339(value))
20 {
21 return false;
22 }
23 }
24 true
25 }
26 _ => true,
27 },
28 )
29}
30
31fn is_valid_rfc3339(date: &str) -> bool {
32 if date.len() < 20 {
35 return false;
36 }
37
38 let chars: Vec<char> = date.chars().collect();
39
40 if chars.len() < 10 {
42 return false;
43 }
44
45 if !chars[0..4].iter().all(|c| c.is_ascii_digit()) {
47 return false;
48 }
49 if chars[4] != '-' {
50 return false;
51 }
52 if !chars[5..7].iter().all(|c| c.is_ascii_digit()) {
53 return false;
54 }
55 if chars[7] != '-' {
56 return false;
57 }
58 if !chars[8..10].iter().all(|c| c.is_ascii_digit()) {
59 return false;
60 }
61
62 if chars.get(10) != Some(&'T') && chars.get(10) != Some(&'t') {
64 return false;
65 }
66
67 if chars.len() < 19 {
69 return false;
70 }
71 if !chars[11..13].iter().all(|c| c.is_ascii_digit()) {
72 return false;
73 }
74 if chars[13] != ':' {
75 return false;
76 }
77 if !chars[14..16].iter().all(|c| c.is_ascii_digit()) {
78 return false;
79 }
80 if chars[16] != ':' {
81 return false;
82 }
83 if !chars[17..19].iter().all(|c| c.is_ascii_digit()) {
84 return false;
85 }
86
87 if chars.len() == 20 && chars[19] == 'Z' {
89 return true;
90 }
91
92 let tz_start = if chars.get(19) == Some(&'.') {
94 let mut i = 20;
96 while i < chars.len() && chars[i].is_ascii_digit() {
97 i += 1;
98 }
99 i
100 } else {
101 19
102 };
103
104 if chars.len() > tz_start {
105 let tz_char = chars[tz_start];
106 if tz_char == 'Z' || tz_char == 'z' {
107 return true;
108 }
109 if (tz_char == '+' || tz_char == '-') && chars.len() >= tz_start + 6 {
110 return true;
111 }
112 }
113
114 false
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::analyzer::hadolint::config::HadolintConfig;
121 use crate::analyzer::hadolint::lint::{LintResult, lint};
122
123 fn lint_dockerfile(content: &str) -> LintResult {
124 lint(content, &HadolintConfig::default())
125 }
126
127 #[test]
128 fn test_valid_date() {
129 let result = lint_dockerfile(
130 "FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"2023-01-15T14:30:00Z\"",
131 );
132 assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
133 }
134
135 #[test]
136 fn test_empty_date() {
137 let result =
138 lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"\"");
139 assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
140 }
141
142 #[test]
143 fn test_invalid_date() {
144 let result = lint_dockerfile(
145 "FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"not-a-date\"",
146 );
147 assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
148 }
149}