use std::path::Path;
pub const MAX_FILE_SIZE_BYTES: u64 = 10 * 1024 * 1024;
pub const MAX_AUTOGEN_FILE_SIZE_BYTES: u64 = 512 * 1024;
const AUTOGEN_SUFFIXES: &[&str] = &[
".d.ts",
".d.mts",
".d.cts",
".min.js",
".min.mjs",
".min.cjs",
".min.css",
".bundle.js",
".bundle.mjs",
".bundle.css",
];
pub fn is_autogen_file(path: &Path) -> bool {
let lossy = path.to_string_lossy().to_lowercase();
AUTOGEN_SUFFIXES.iter().any(|s| lossy.ends_with(s))
}
pub fn max_size_for(path: &Path) -> u64 {
if is_autogen_file(path) {
MAX_AUTOGEN_FILE_SIZE_BYTES
} else {
MAX_FILE_SIZE_BYTES
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SizeCheck {
WithinLimit {
size_bytes: u64,
},
Oversize {
size_bytes: u64,
max_bytes: u64,
is_autogen: bool,
},
Unknown,
}
pub fn check_size(path: &Path) -> SizeCheck {
match std::fs::metadata(path) {
Ok(md) => {
let size_bytes = md.len();
let max_bytes = max_size_for(path);
if size_bytes > max_bytes {
SizeCheck::Oversize {
size_bytes,
max_bytes,
is_autogen: is_autogen_file(path),
}
} else {
SizeCheck::WithinLimit { size_bytes }
}
}
Err(_) => SizeCheck::Unknown,
}
}
pub fn format_oversize_warning(
path: &Path,
size_bytes: u64,
max_bytes: u64,
is_autogen: bool,
) -> String {
let category = if is_autogen {
"auto-generated/minified files"
} else {
"source files"
};
format!(
"Skipped {}: {} exceeds {} cap for {}",
path.display(),
format_size(size_bytes),
format_size(max_bytes),
category
)
}
fn format_size(bytes: u64) -> String {
if bytes < 1024 * 1024 {
let kb = bytes.div_ceil(1024);
format!("{}KB", kb)
} else {
let mb = bytes.div_ceil(1024 * 1024);
format!("{}MB", mb)
}
}
fn bytes_to_mb_ceil(bytes: u64) -> u64 {
bytes.div_ceil(1024 * 1024)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
#[test]
fn is_autogen_file_recognises_dts_min_bundle() {
assert!(is_autogen_file(&PathBuf::from("dom.generated.d.ts")));
assert!(is_autogen_file(&PathBuf::from("/abs/path/lib.d.ts")));
assert!(is_autogen_file(&PathBuf::from("foo.min.js")));
assert!(is_autogen_file(&PathBuf::from("vendor.bundle.js")));
assert!(is_autogen_file(&PathBuf::from("style.min.css")));
assert!(is_autogen_file(&PathBuf::from("Foo.D.TS")));
}
#[test]
fn is_autogen_file_rejects_normal_source() {
assert!(!is_autogen_file(&PathBuf::from("foo.ts")));
assert!(!is_autogen_file(&PathBuf::from("bar.js")));
assert!(!is_autogen_file(&PathBuf::from("lib.rs")));
assert!(!is_autogen_file(&PathBuf::from("index.tsx")));
}
#[test]
fn max_size_picks_5mb_for_autogen_10mb_for_source() {
assert_eq!(
max_size_for(&PathBuf::from("dom.d.ts")),
MAX_AUTOGEN_FILE_SIZE_BYTES
);
assert_eq!(
max_size_for(&PathBuf::from("dom.ts")),
MAX_FILE_SIZE_BYTES
);
}
#[test]
fn check_size_within_limit_for_small_file() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("small.ts");
std::fs::write(&path, b"hello").unwrap();
match check_size(&path) {
SizeCheck::WithinLimit { size_bytes } => assert_eq!(size_bytes, 5),
other => panic!("expected WithinLimit, got {:?}", other),
}
}
#[test]
fn check_size_oversize_for_dts_above_autogen_cap() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("huge.d.ts");
let bytes = vec![b'a'; (MAX_AUTOGEN_FILE_SIZE_BYTES as usize) + 1];
std::fs::write(&path, &bytes).unwrap();
match check_size(&path) {
SizeCheck::Oversize {
size_bytes,
max_bytes,
is_autogen,
} => {
assert_eq!(size_bytes, MAX_AUTOGEN_FILE_SIZE_BYTES + 1);
assert_eq!(max_bytes, MAX_AUTOGEN_FILE_SIZE_BYTES);
assert!(is_autogen);
}
other => panic!("expected Oversize, got {:?}", other),
}
}
#[test]
fn check_size_within_limit_for_dts_just_under_cap() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("borderline.d.ts");
let bytes = vec![b'a'; MAX_AUTOGEN_FILE_SIZE_BYTES as usize];
std::fs::write(&path, &bytes).unwrap();
match check_size(&path) {
SizeCheck::WithinLimit { size_bytes } => {
assert_eq!(size_bytes, MAX_AUTOGEN_FILE_SIZE_BYTES)
}
other => panic!("expected WithinLimit, got {:?}", other),
}
}
#[test]
fn check_size_within_limit_for_source_between_caps() {
let tmp = tempdir().unwrap();
let path = tmp.path().join("medium.ts");
let bytes = vec![b'a'; (MAX_AUTOGEN_FILE_SIZE_BYTES as usize) + 1024];
std::fs::write(&path, &bytes).unwrap();
match check_size(&path) {
SizeCheck::WithinLimit { .. } => {}
other => panic!("expected WithinLimit, got {:?}", other),
}
}
#[test]
fn check_size_unknown_for_missing_file() {
let outcome = check_size(Path::new("/nonexistent/path/abc.ts"));
assert_eq!(outcome, SizeCheck::Unknown);
}
#[test]
fn format_oversize_warning_matches_documented_shape() {
let msg = format_oversize_warning(
Path::new("/tmp/dom.d.ts"),
3 * 1024 * 1024,
512 * 1024,
true,
);
assert!(msg.contains("/tmp/dom.d.ts"));
assert!(msg.contains("3MB"));
assert!(msg.contains("512KB"));
assert!(msg.contains("auto-generated/minified files"));
let msg2 = format_oversize_warning(
Path::new("/tmp/big.ts"),
12 * 1024 * 1024,
10 * 1024 * 1024,
false,
);
assert!(msg2.contains("12MB"));
assert!(msg2.contains("10MB"));
assert!(msg2.contains("source files"));
}
#[test]
fn format_size_uses_kb_below_one_mib() {
assert_eq!(format_size(0), "0KB");
assert_eq!(format_size(1), "1KB"); assert_eq!(format_size(512 * 1024), "512KB");
assert_eq!(format_size(1023 * 1024), "1023KB");
}
#[test]
fn format_size_uses_mb_at_or_above_one_mib() {
assert_eq!(format_size(1024 * 1024), "1MB");
assert_eq!(format_size(2 * 1024 * 1024 + 1), "3MB");
assert_eq!(format_size(10 * 1024 * 1024), "10MB");
}
}