use anyhow::{Context, Result};
use std::error::Error;
use std::fmt;
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
pub(crate) const MAX_PROJECT_INPUT_BYTES: u64 = 8 * 1024 * 1024;
pub(crate) const MAX_CONFIG_INPUT_BYTES: u64 = 1024 * 1024;
pub(crate) const MAX_SUPPRESSION_REASON_CHARS: usize = 512;
pub(crate) const MAX_BASELINE_INPUT_BYTES: u64 = 8 * 1024 * 1024;
pub(crate) const MAX_REGEX_PATTERN_BYTES: usize = 1024 * 1024;
pub(crate) const REGEX_SIZE_LIMIT_BYTES: usize = 1024 * 1024;
pub(crate) const REGEX_DFA_SIZE_LIMIT_BYTES: usize = 2 * 1024 * 1024;
pub(crate) const MAX_STATIC_OVERLAP_FINDINGS: usize = 10_000;
pub(crate) const MAX_REGISTRATION_MODULES: usize = 1_024;
pub(crate) const MAX_REGISTRATION_MODULE_BYTES: usize = 64 * 1024 * 1024;
pub(crate) const MAX_ASSERTION_RESOLUTION_WORK: usize = 1_000_000;
pub(crate) const MAX_REGISTRATION_RESOLUTION_STATES: usize = 16_384;
pub(crate) const MAX_PROJECT_CONFIG_EXTENDS_DEPTH: usize = 16;
pub(crate) const MAX_PROJECT_METADATA_FILES: usize = 1_024;
pub(crate) const MAX_PROJECT_METADATA_BYTES: usize = 16 * 1024 * 1024;
pub(crate) const MAX_PROJECT_MODULE_MAPPINGS: usize = 16_384;
pub(crate) const MAX_WORKSPACE_SCAN_ENTRIES: usize = 100_000;
pub(crate) const MAX_GHERKIN_MARKDOWN_CANDIDATES: usize = 10_000;
pub(crate) const MAX_GHERKIN_MARKDOWN_PROBE_BYTES: usize = 64 * 1024 * 1024;
pub(crate) const MAX_GHERKIN_MARKDOWN_PROBES: usize = 128;
pub(crate) const GHERKIN_MARKDOWN_RESTORE_BATCH_SIZE: usize = 32;
pub(crate) const MAX_CANDIDATE_COMPARISONS: usize = 2_000_000;
pub(crate) const MAX_STRUCTURAL_CLASS_COMPARISONS: usize = 250_000;
pub(crate) const SOURCE_CLASSIFICATION_PREFIX_BYTES: usize = 64 * 1024;
pub(crate) const MINIFIED_MEAN_LINE_BYTES: usize = 200;
#[derive(Debug)]
struct InputLimitExceeded {
path: PathBuf,
kind: &'static str,
limit: u64,
}
impl fmt::Display for InputLimitExceeded {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} {} exceeds the {}-byte input limit; reduce the file before retrying",
self.kind,
self.path.display(),
self.limit
)
}
}
impl Error for InputLimitExceeded {}
pub(crate) fn read_utf8(path: &Path, kind: &'static str, limit: u64) -> Result<String> {
let file =
File::open(path).with_context(|| format!("failed to read {kind} {}", path.display()))?;
let metadata = file
.metadata()
.with_context(|| format!("failed to inspect {kind} {}", path.display()))?;
if metadata.len() > limit {
return Err(InputLimitExceeded {
path: path.to_owned(),
kind,
limit,
}
.into());
}
let mut bytes = Vec::with_capacity(metadata.len().min(limit) as usize);
file.take(limit.saturating_add(1))
.read_to_end(&mut bytes)
.with_context(|| format!("failed to read {kind} {}", path.display()))?;
if bytes.len() as u64 > limit {
return Err(InputLimitExceeded {
path: path.to_owned(),
kind,
limit,
}
.into());
}
String::from_utf8(bytes)
.with_context(|| format!("{kind} {} is not valid UTF-8", path.display()))
}
pub(crate) fn is_input_limit_error(error: &anyhow::Error) -> bool {
error
.chain()
.any(|cause| cause.downcast_ref::<InputLimitExceeded>().is_some())
}
pub(crate) fn compile_regex(pattern: &str) -> Result<regex::Regex, regex::Error> {
compile_regex_with_limits(pattern, REGEX_SIZE_LIMIT_BYTES, REGEX_DFA_SIZE_LIMIT_BYTES)
}
fn compile_regex_with_limits(
pattern: &str,
size_limit: usize,
dfa_size_limit: usize,
) -> Result<regex::Regex, regex::Error> {
regex::RegexBuilder::new(pattern)
.size_limit(size_limit)
.dfa_size_limit(dfa_size_limit)
.build()
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn bounded_reader_accepts_exact_limit_and_rejects_next_byte() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("input.ts");
fs::write(&path, "1234").unwrap();
assert_eq!(read_utf8(&path, "source", 4).unwrap(), "1234");
fs::write(&path, "12345").unwrap();
let error = read_utf8(&path, "source", 4).unwrap_err();
assert!(is_input_limit_error(&error));
assert!(error.to_string().contains("4-byte input limit"));
}
#[test]
fn bounded_reader_rejects_invalid_utf8_without_misclassifying_it_as_a_limit() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("input.ts");
fs::write(&path, [0xff]).unwrap();
let error = read_utf8(&path, "source", 4).unwrap_err();
assert!(!is_input_limit_error(&error));
assert!(error.to_string().contains("not valid UTF-8"));
}
#[cfg(unix)]
#[test]
fn bounded_reader_limits_special_files_without_reliable_metadata_lengths() {
let error = read_utf8(Path::new("/dev/zero"), "special input", 4).unwrap_err();
assert!(is_input_limit_error(&error));
assert!(error.to_string().contains("4-byte input limit"));
}
#[test]
fn regex_builder_enforces_the_configured_program_limit() {
let pattern = (0..1_000)
.map(|index| format!("literal-{index}"))
.collect::<Vec<_>>()
.join("|");
let error = compile_regex_with_limits(&pattern, 128, 128).unwrap_err();
assert!(matches!(error, regex::Error::CompiledTooBig(128)));
}
#[test]
fn release_resource_limits_match_the_documented_policy() {
assert_eq!(MAX_PROJECT_INPUT_BYTES, 8 * 1024 * 1024);
assert_eq!(MAX_CONFIG_INPUT_BYTES, 1024 * 1024);
assert_eq!(MAX_SUPPRESSION_REASON_CHARS, 512);
assert_eq!(MAX_BASELINE_INPUT_BYTES, 8 * 1024 * 1024);
assert_eq!(MAX_REGEX_PATTERN_BYTES, 1024 * 1024);
assert_eq!(REGEX_SIZE_LIMIT_BYTES, 1024 * 1024);
assert_eq!(REGEX_DFA_SIZE_LIMIT_BYTES, 2 * 1024 * 1024);
assert_eq!(MAX_STATIC_OVERLAP_FINDINGS, 10_000);
assert_eq!(MAX_REGISTRATION_MODULES, 1_024);
assert_eq!(MAX_REGISTRATION_MODULE_BYTES, 64 * 1024 * 1024);
assert_eq!(MAX_REGISTRATION_RESOLUTION_STATES, 16_384);
assert_eq!(MAX_PROJECT_CONFIG_EXTENDS_DEPTH, 16);
assert_eq!(MAX_PROJECT_METADATA_FILES, 1_024);
assert_eq!(MAX_PROJECT_METADATA_BYTES, 16 * 1024 * 1024);
assert_eq!(MAX_PROJECT_MODULE_MAPPINGS, 16_384);
assert_eq!(MAX_WORKSPACE_SCAN_ENTRIES, 100_000);
assert_eq!(MAX_GHERKIN_MARKDOWN_CANDIDATES, 10_000);
assert_eq!(MAX_GHERKIN_MARKDOWN_PROBE_BYTES, 64 * 1024 * 1024);
assert_eq!(MAX_GHERKIN_MARKDOWN_PROBES, 128);
assert_eq!(GHERKIN_MARKDOWN_RESTORE_BATCH_SIZE, 32);
assert_eq!(MAX_CANDIDATE_COMPARISONS, 2_000_000);
assert_eq!(MAX_STRUCTURAL_CLASS_COMPARISONS, 250_000);
assert_eq!(SOURCE_CLASSIFICATION_PREFIX_BYTES, 64 * 1024);
assert_eq!(MINIFIED_MEAN_LINE_BYTES, 200);
}
}