use crate::analyzer::hadolint::parser::instruction::Instruction;
use crate::analyzer::hadolint::rules::{SimpleRule, simple_rule};
use crate::analyzer::hadolint::shell::ParsedShell;
use crate::analyzer::hadolint::types::Severity;
pub fn rule() -> SimpleRule<impl Fn(&Instruction, Option<&ParsedShell>) -> bool + Send + Sync> {
simple_rule(
"DL3051",
Severity::Warning,
"Label `org.opencontainers.image.created` is empty or not a valid RFC3339 date.",
|instr, _shell| match instr {
Instruction::Label(pairs) => {
for (key, value) in pairs {
if key == "org.opencontainers.image.created"
&& (value.is_empty() || !is_valid_rfc3339(value))
{
return false;
}
}
true
}
_ => true,
},
)
}
fn is_valid_rfc3339(date: &str) -> bool {
if date.len() < 20 {
return false;
}
let chars: Vec<char> = date.chars().collect();
if chars.len() < 10 {
return false;
}
if !chars[0..4].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars[4] != '-' {
return false;
}
if !chars[5..7].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars[7] != '-' {
return false;
}
if !chars[8..10].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars.get(10) != Some(&'T') && chars.get(10) != Some(&'t') {
return false;
}
if chars.len() < 19 {
return false;
}
if !chars[11..13].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars[13] != ':' {
return false;
}
if !chars[14..16].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars[16] != ':' {
return false;
}
if !chars[17..19].iter().all(|c| c.is_ascii_digit()) {
return false;
}
if chars.len() == 20 && chars[19] == 'Z' {
return true;
}
let tz_start = if chars.get(19) == Some(&'.') {
let mut i = 20;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
i
} else {
19
};
if chars.len() > tz_start {
let tz_char = chars[tz_start];
if tz_char == 'Z' || tz_char == 'z' {
return true;
}
if (tz_char == '+' || tz_char == '-') && chars.len() >= tz_start + 6 {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::hadolint::config::HadolintConfig;
use crate::analyzer::hadolint::lint::{LintResult, lint};
fn lint_dockerfile(content: &str) -> LintResult {
lint(content, &HadolintConfig::default())
}
#[test]
fn test_valid_date() {
let result = lint_dockerfile(
"FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"2023-01-15T14:30:00Z\"",
);
assert!(!result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
}
#[test]
fn test_empty_date() {
let result =
lint_dockerfile("FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"\"");
assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
}
#[test]
fn test_invalid_date() {
let result = lint_dockerfile(
"FROM ubuntu:20.04\nLABEL org.opencontainers.image.created=\"not-a-date\"",
);
assert!(result.failures.iter().any(|f| f.code.as_str() == "DL3051"));
}
}