use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
#[allow(dead_code)]
static CF_RESOURCE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\d{4}-\d{2}-\d{2}T[^\s]+ [A-Z_]+ [A-Z_]+ [^\n]+\n?").unwrap());
static PROGRESS_BAR_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s*[-=]{3,}[^\n]*\n?").unwrap());
pub fn compress_cloudformation(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let out: Vec<&str> = cleaned
.lines()
.filter(|l| !l.contains("_IN_PROGRESS") || l.contains("FAILED") || l.contains("ROLLBACK"))
.collect();
compactor::collapse_blanks(&out.join("\n"))
}
pub fn compress_s3(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = PROGRESS_BAR_RE.replace_all(&cleaned, "");
let out: Vec<&str> = s
.lines()
.filter(|l| {
l.trim_start().starts_with("upload:")
|| l.trim_start().starts_with("download:")
|| l.trim_start().starts_with("copy:")
|| l.trim_start().starts_with("delete:")
|| l.contains("error")
|| l.contains("Error")
|| l.contains("fatal")
})
.collect();
if out.is_empty() {
s.into_owned()
} else {
out.join("\n")
}
}
pub fn compress_generic_aws(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() > 40 {
return format!(
"{}\n... [{} more lines — use --query to filter] ...",
lines[..40].join("\n"),
lines.len() - 40
);
}
cleaned
}
pub fn compress_logs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
super::super::sys::log_dedup(&cleaned)
}
pub fn compress_lambda(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let noisy: &[&str] = &[
"\"FunctionArn\"",
"\"Role\"",
"\"CodeSha256\"",
"\"RevisionId\"",
"\"LastModified\"",
"\"CodeSize\"",
"\"Layers\"",
"\"VpcConfig\"",
"\"TracingConfig\"",
];
let out: Vec<&str> = cleaned
.lines()
.filter(|l| noisy.iter().all(|n| !l.contains(n)))
.collect();
compactor::collapse_blanks(&out.join("\n"))
}
pub fn compress_ecs(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.len() <= 30 {
return lines.join("\n");
}
format!(
"{}\n... [{} more lines] ...",
lines[..30].join("\n"),
lines.len() - 30
)
}
pub fn compress_iam(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let noisy: &[&str] = &[
"\"PolicyDocument\":",
"\"AssumeRolePolicyDocument\":",
"\"Statement\":",
"\"Action\":",
"\"Effect\":",
"\"Resource\":",
"\"Condition\":",
"\"Principal\":",
];
let out: Vec<&str> = cleaned
.lines()
.filter(|l| noisy.iter().all(|n| !l.trim().starts_with(n)))
.collect();
let lines = out.as_slice();
if lines.len() <= 40 {
return lines.join("\n");
}
format!(
"{}\n... [{} more lines — use --query to filter] ...",
lines[..40].join("\n"),
lines.len() - 40
)
}
pub fn compress_sts(raw: &str) -> String {
compactor::normalise(raw)
}
pub fn compress_aws(subcmd: &str, raw: &str) -> String {
let sub = subcmd.trim();
if sub.starts_with("cloudformation") {
return compress_cloudformation(raw);
}
if sub.starts_with("s3") {
return compress_s3(raw);
}
if sub.starts_with("logs") {
return compress_logs(raw);
}
if sub.starts_with("lambda") {
return compress_lambda(raw);
}
if sub.starts_with("ecs") {
return compress_ecs(raw);
}
if sub.starts_with("iam") {
return compress_iam(raw);
}
if sub.starts_with("sts") {
return compress_sts(raw);
}
compress_generic_aws(raw)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cloudformation_strips_in_progress() {
let raw = "MyBucket AWS::S3::Bucket CREATE_IN_PROGRESS\nMyBucket AWS::S3::Bucket CREATE_COMPLETE\n";
let out = compress_cloudformation(raw);
assert!(!out.contains("CREATE_IN_PROGRESS"), "{out}");
assert!(out.contains("CREATE_COMPLETE"));
}
#[test]
fn s3_keeps_upload_lines() {
let raw =
"upload: ./foo.txt to s3://bucket/foo.txt\nCompleted 1.0 MiB/1.0 MiB (500 KiB/s)\n";
let out = compress_s3(raw);
assert!(out.contains("upload:"));
}
#[test]
fn generic_truncates_large_json() {
let raw = (0..50)
.map(|i| format!(" \"key{i}\": \"value{i}\","))
.collect::<Vec<_>>()
.join("\n");
let out = compress_generic_aws(&raw);
assert!(out.contains("more lines"), "{out}");
}
#[test]
fn lambda_strips_noisy_fields() {
let raw = "{\n \"FunctionName\": \"my-fn\",\n \"FunctionArn\": \"arn:aws:lambda:us-east-1:123:function:my-fn\",\n \"Runtime\": \"nodejs18.x\",\n \"Role\": \"arn:aws:iam::123:role/role\",\n \"CodeSha256\": \"abc123\"\n}\n";
let out = compress_lambda(raw);
assert!(!out.contains("FunctionArn"), "{out}");
assert!(!out.contains("CodeSha256"), "{out}");
assert!(out.contains("FunctionName"), "{out}");
assert!(out.contains("Runtime"), "{out}");
}
#[test]
fn logs_deduplicates_repeated_entries() {
let raw = "[ERROR] timeout\n[ERROR] timeout\n[ERROR] timeout\n[INFO] recovered\n";
let out = compress_logs(raw);
assert!(
out.contains("repeated") || out.contains("[ERROR] timeout"),
"{out}"
);
assert!(out.contains("[INFO] recovered"), "{out}");
}
}