use crate::SyncStats;
use crate::source_versions::CFN_LINT_SOURCE;
use log::info;
use std::path::Path;
use std::process::Command;
const EXTRACTED_FILES: &[&str] =
&["retention_period_requirements.json", "codepipeline_action_artifact_counts.json", "cfnlint_rule_tables.json"];
pub const GETATT_ADDITIONS_NAME: &str = "getatt_additions";
fn extract_cfn_lint_version(rule_source_dir: &Path) -> anyhow::Result<String> {
let source_dir = rule_source_dir.join("src");
anyhow::ensure!(source_dir.is_dir(), "cfn-lint source directory not found at {}", source_dir.display());
let output = Command::new("python3")
.arg("-c")
.arg("import sys; sys.path.insert(0, sys.argv[1]); from cfnlint.version import __version__; print(__version__)")
.arg(&source_dir)
.output()?;
if !output.status.success() {
anyhow::bail!(
"failed to read cfn-lint version ({}):\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
parse_cfn_lint_version(&output.stdout)
}
fn parse_cfn_lint_version(stdout: &[u8]) -> anyhow::Result<String> {
let output = std::str::from_utf8(stdout)?;
let version = output.trim();
anyhow::ensure!(!version.is_empty(), "cfn-lint version output was blank");
anyhow::ensure!(version.split_whitespace().count() == 1, "cfn-lint version output contained multiple values");
Ok(format!("{CFN_LINT_SOURCE}@{version}"))
}
pub fn sync_cfnlint_tables(
rule_source_dir: &Path,
data_output_dir: &Path,
upstream_dir: &Path,
) -> anyhow::Result<(SyncStats, String)> {
let mut stats = SyncStats::default();
let cfn_lint_version = extract_cfn_lint_version(rule_source_dir)?;
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let script = manifest.join("scripts").join("sync_cfnlint_data.py");
anyhow::ensure!(script.exists(), "cfn-lint table extractor not found at {}", script.display());
info!("Extracting cfn-lint data tables via {}", script.display());
let output = Command::new("python3")
.arg(&script)
.arg("--cfn-lint-root")
.arg(rule_source_dir)
.arg("--out")
.arg(data_output_dir)
.arg("--upstream-out")
.arg(upstream_dir)
.output()?;
if !output.status.success() {
anyhow::bail!(
"cfn-lint table extractor failed ({}):\n{}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
let stderr = String::from_utf8_lossy(&output.stderr);
for line in stderr.lines().filter(|l| !l.trim().is_empty()) {
info!(" cfnlint-tables: {}", line.trim());
}
for file in EXTRACTED_FILES {
let path = data_output_dir.join(file);
anyhow::ensure!(path.exists(), "extractor did not produce expected file: {}", path.display());
stats.files_written += 1;
}
let getatt_additions = upstream_dir.join(format!("{GETATT_ADDITIONS_NAME}.json"));
anyhow::ensure!(
getatt_additions.exists(),
"extractor did not produce expected file: {}",
getatt_additions.display()
);
stats.files_written += 1;
info!("Extracted {} cfn-lint data tables", stats.files_written);
Ok((stats, cfn_lint_version))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cfn_lint_version_parses_single_value() {
assert_eq!(
parse_cfn_lint_version(b"1.54.0\n").expect("version should parse"),
"https://github.com/aws-cloudformation/cfn-lint@1.54.0"
);
}
#[test]
fn cfn_lint_version_rejects_blank_output() {
assert!(parse_cfn_lint_version(b" \n").is_err());
}
#[test]
fn cfn_lint_version_rejects_multiple_values() {
let error = parse_cfn_lint_version(b"1.54.0\nunexpected\n").expect_err("multiple values must fail");
assert!(error.to_string().contains("multiple values"));
}
#[test]
fn cfn_lint_version_rejects_non_utf8_output() {
assert!(parse_cfn_lint_version(&[0xff]).is_err());
}
}