mirror of
https://github.com/cfal/shoes.git
synced 2026-11-06 08:14:11 +00:00
Add reusable rule-source groups and config-driven remote downloads with explicit proxy and DNS routing, in-memory retention, and optional private atomic disk caching. Preserve active traffic across download and reload failures while keeping configuration checks offline. Consolidate process-wide limits and rule_cache_dir under global_settings, retain global_limits as a warning-emitting alias, and remove ordered fallback groups and health_check configuration.
139 lines
5.1 KiB
Rust
139 lines
5.1 KiB
Rust
use std::io::Write;
|
|
|
|
use shoes::rule_set::{Behavior, Format, RuleSource, YAML_PARSER_SCRATCH_BYTES};
|
|
use shoes::rules::{Compiler, Expression};
|
|
use shoes_test_support::measure::CountingAllocator;
|
|
|
|
#[global_allocator]
|
|
static ALLOCATOR: CountingAllocator = CountingAllocator::new();
|
|
|
|
fn peak<T>(run: impl FnOnce() -> T) -> (T, usize) {
|
|
let baseline = ALLOCATOR.snapshot().live_bytes;
|
|
ALLOCATOR.reset_peak();
|
|
let result = run();
|
|
(
|
|
result,
|
|
ALLOCATOR.snapshot().peak_bytes.saturating_sub(baseline),
|
|
)
|
|
}
|
|
|
|
fn load_peak(bytes: &[u8], format: Format, precharged: usize) -> (std::io::Result<()>, usize) {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
let path = dir.path().join("source");
|
|
std::fs::write(&path, bytes).unwrap();
|
|
let source = RuleSource {
|
|
url: None,
|
|
download: None,
|
|
update_interval: None,
|
|
path,
|
|
format: Some(format),
|
|
behavior: (format == Format::MihomoYaml).then_some(Behavior::Domain),
|
|
code: (format == Format::GeositeDat).then(|| "TEST".into()),
|
|
attributes: vec![],
|
|
};
|
|
let mut compiler = Compiler::default();
|
|
compiler.charge(precharged, 0).unwrap();
|
|
peak(|| shoes::rule_set::load_source(&source, &mut compiler).map(|_| ()))
|
|
}
|
|
|
|
fn message(tag: u32, bytes: &[u8]) -> Vec<u8> {
|
|
let mut output = Vec::new();
|
|
prost::encoding::encode_key(tag, prost::encoding::WireType::LengthDelimited, &mut output);
|
|
prost::encoding::encode_varint(bytes.len() as u64, &mut output);
|
|
output.extend_from_slice(bytes);
|
|
output
|
|
}
|
|
|
|
// A separate test executable keeps allocator measurements isolated from parallel
|
|
// library tests. Inputs are allocated before measuring compiler-owned memory.
|
|
#[test]
|
|
fn adversarial_inputs_stay_within_their_compilation_reserves() {
|
|
for yaml in [
|
|
format!("payload: [{}]", "[],".repeat(350_000)),
|
|
format!("{{[{}]: a}}", "a,".repeat(350_000)),
|
|
format!("[[{}]]", ":,".repeat(350_000)),
|
|
] {
|
|
let (result, used) = load_peak(yaml.as_bytes(), Format::MihomoYaml, 0);
|
|
assert!(result.is_err());
|
|
assert!(
|
|
used < YAML_PARSER_SCRATCH_BYTES + yaml.len() * 2,
|
|
"YAML peak: {used}"
|
|
);
|
|
println!("YAML bytes={}, peak={used}", yaml.len());
|
|
}
|
|
let json = format!(r#"{{"{}\n":0}}"#, "a".repeat(1024 * 1024));
|
|
let (result, used) = load_peak(json.as_bytes(), Format::SingboxJson, 0);
|
|
assert!(result.is_err());
|
|
assert!(used < json.len() * 4 + 4096, "JSON peak: {used}");
|
|
println!("JSON bytes={}, peak={used}", json.len());
|
|
|
|
let available = 16 * 1024 * 1024;
|
|
for json in [
|
|
format!("[{}]", vec!["[]"; 200_000].join(",")),
|
|
format!("[{}]", vec![r#"{"a":0}"#; 200_000].join(",")),
|
|
format!(
|
|
r#"{{"version":3,"rules":[{{"domain":[{}]}}]}}"#,
|
|
vec![r#""a.example""#; 200_000].join(",")
|
|
),
|
|
] {
|
|
let (result, used) = load_peak(
|
|
json.as_bytes(),
|
|
Format::SingboxJson,
|
|
512 * 1024 * 1024 - available,
|
|
);
|
|
let error = result.unwrap_err();
|
|
assert!(error.to_string().contains("budget"), "{error}");
|
|
assert!(
|
|
used < available,
|
|
"JSON peak: {used}, available: {available}"
|
|
);
|
|
println!(
|
|
"JSON tree bytes={}, peak={used}, available={available}",
|
|
json.len()
|
|
);
|
|
}
|
|
|
|
let count = 100_000;
|
|
let mut domain = message(2, b"example.com");
|
|
domain.extend_from_slice(&[0x1a, 0].repeat(count));
|
|
let mut category = message(1, b"TEST");
|
|
category.extend(message(2, &domain));
|
|
let dat = message(1, &category);
|
|
let (result, used) = load_peak(&dat, Format::GeositeDat, 0);
|
|
result.unwrap();
|
|
assert!(
|
|
used < dat.len() * 10 + count * 160 + 4096,
|
|
"DAT peak: {used}"
|
|
);
|
|
println!("DAT attributes={count}, peak={used}");
|
|
|
|
let patterns = vec![format!("(?i){}", "a".repeat(60_000)); 8];
|
|
let bytes: usize = patterns.iter().map(String::len).sum();
|
|
let (_, used) = peak(|| Compiler::default().compile(Expression::DomainRegex(patterns)));
|
|
let reserve = bytes * 512 + 64 * 1024 * 1024;
|
|
assert!(used < reserve, "regex peak: {used}, reserve: {reserve}");
|
|
println!("regex bytes={bytes}, peak={used}, reserve={reserve}");
|
|
|
|
let inflated = vec![0; 8 * 1024 * 1024 + 1];
|
|
let mut zlib =
|
|
flate2::write::ZlibEncoder::new(b"SRS\x02".to_vec(), flate2::Compression::fast());
|
|
zlib.write_all(&inflated).unwrap();
|
|
let zstd = ruzstd::encoding::compress_to_vec(
|
|
inflated.as_slice(),
|
|
ruzstd::encoding::CompressionLevel::Fastest,
|
|
);
|
|
for (format, input, available) in [
|
|
(Format::Srs, zlib.finish().unwrap(), 20 * 1024 * 1024),
|
|
(Format::Mrs, zstd, 44 * 1024 * 1024),
|
|
] {
|
|
let (result, used) = load_peak(&input, format, 512 * 1024 * 1024 - available);
|
|
let error = result.unwrap_err();
|
|
assert!(error.to_string().contains("budget"), "{format:?}: {error}");
|
|
assert!(
|
|
used < available,
|
|
"{format:?} peak: {used}, available: {available}"
|
|
);
|
|
println!("{format:?} growth rejection peak={used}, available={available}");
|
|
}
|
|
}
|