syncable_cli/analyzer/hadolint/rules/
dl3051.rs

1//! DL3051: Label `org.opencontainers.image.created` is empty or not a valid date
2//!
3//! The created label should contain a valid RFC3339 date.
4
5use 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    // Basic RFC3339 validation (YYYY-MM-DDTHH:MM:SSZ or with timezone offset)
35    // Full format: 2023-01-15T14:30:00Z or 2023-01-15T14:30:00+00:00
36    if date.len() < 20 {
37        return false;
38    }
39
40    let chars: Vec<char> = date.chars().collect();
41
42    // Check date part
43    if chars.len() < 10 {
44        return false;
45    }
46
47    // YYYY-MM-DD
48    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    // T separator
55    if chars.get(10) != Some(&'T') && chars.get(10) != Some(&'t') {
56        return false;
57    }
58
59    // HH:MM:SS
60    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    // Timezone (Z or +/-HH:MM)
68    if chars.len() == 20 && chars[19] == 'Z' {
69        return true;
70    }
71
72    // Allow fractional seconds before timezone
73    let tz_start = if chars.get(19) == Some(&'.') {
74        // Find where fractional seconds end
75        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}