#![cfg(feature = "server")]
use std::panic::{catch_unwind, AssertUnwindSafe};
use rs3gw::api::utils::{
parse_complete_multipart_parts, parse_cors_xml, parse_delete_objects_xml, parse_encryption_xml,
parse_http_date, parse_lifecycle_xml, parse_tagging_xml, parse_versioning_xml,
parse_website_xml,
};
struct XorShift64 {
state: u64,
}
impl XorShift64 {
fn new(seed: u64) -> Self {
Self {
state: seed | 0x9E37_79B9_7F4A_7C15,
}
}
fn next_u64(&mut self) -> u64 {
let mut x = self.state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.state = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, n: usize) -> usize {
if n == 0 {
0
} else {
(self.next_u64() % n as u64) as usize
}
}
fn pick<'a, T>(&mut self, choices: &'a [T]) -> &'a T {
&choices[self.below(choices.len())]
}
}
const FRAGMENTS: &[&str] = &[
"<Part>",
"</Part>",
"<PartNumber>",
"</PartNumber>",
"<ETag>",
"</ETag>",
"<Tag>",
"</Tag>",
"<Key>",
"</Key>",
"<Value>",
"</Value>",
"<Object>",
"</Object>",
"<Delete>",
"</Delete>",
"<Quiet>",
"</Quiet>",
"<Status>",
"</Status>",
"<CORSRule>",
"</CORSRule>",
"<AllowedMethod>",
"<AllowedOrigin>",
"<Rule>",
"</Rule>",
"<ID>",
"<Prefix>",
"<Expiration>",
"<Days>",
"<RoutingRules>",
"<IndexDocument>",
"<Suffix>",
"<ServerSideEncryptionConfiguration>",
"<SSEAlgorithm>",
"aws:kms",
"AES256",
""",
"&",
"<",
">",
"&",
"<",
">",
"\"",
"'",
"1",
"0",
"-1",
"999999999999999999999999",
"abc",
" ",
"\t",
"\n",
"\u{0}",
"é",
"🦀",
"\u{FFFD}",
"23",
"Enabled",
"Disabled",
];
const TEMPLATES: &[&str] = &[
"<CompleteMultipartUpload><Part><PartNumber>1</PartNumber><ETag>\"abc\"</ETag></Part></CompleteMultipartUpload>",
"<Tagging><TagSet><Tag><Key>k</Key><Value>v</Value></Tag></TagSet></Tagging>",
"<Delete><Object><Key>a</Key></Object><Object><Key>b</Key></Object><Quiet>true</Quiet></Delete>",
"<ServerSideEncryptionConfiguration><Rule><ApplyServerSideEncryptionByDefault><SSEAlgorithm>AES256</SSEAlgorithm></ApplyServerSideEncryptionByDefault></Rule></ServerSideEncryptionConfiguration>",
"<CORSConfiguration><CORSRule><AllowedMethod>GET</AllowedMethod><AllowedOrigin>*</AllowedOrigin></CORSRule></CORSConfiguration>",
"<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>",
"<LifecycleConfiguration><Rule><ID>r</ID><Status>Enabled</Status><Expiration><Days>30</Days></Expiration></Rule></LifecycleConfiguration>",
"<WebsiteConfiguration><IndexDocument><Suffix>index.html</Suffix></IndexDocument></WebsiteConfiguration>",
"Tue, 15 Nov 1994 08:12:31 GMT",
];
fn gen_soup(rng: &mut XorShift64) -> String {
let parts = 1 + rng.below(40);
let mut s = String::new();
for _ in 0..parts {
let fragment = *rng.pick(FRAGMENTS);
s.push_str(fragment);
}
let keep = rng.below(s.len() + 1);
truncate_on_char_boundary(&mut s, keep);
s
}
fn gen_mutation(rng: &mut XorShift64) -> String {
let template = *rng.pick(TEMPLATES);
let mut bytes = template.as_bytes().to_vec();
let mutations = 1 + rng.below(6);
for _ in 0..mutations {
if bytes.is_empty() {
break;
}
match rng.below(5) {
0 => {
let at = rng.below(bytes.len());
bytes.remove(at);
}
1 => {
let at = rng.below(bytes.len());
let b = bytes[at];
bytes.insert(at, b);
}
2 => {
let at = rng.below(bytes.len() + 1);
bytes.insert(at, (rng.next_u64() & 0xFF) as u8);
}
3 => {
let at = rng.below(bytes.len() + 1);
bytes.truncate(at);
}
_ => {
let at = rng.below(bytes.len());
bytes[at] = *rng.pick(&[b'<', b'>', b'&', b'"', b'/', 0u8]);
}
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
fn truncate_on_char_boundary(s: &mut String, mut len: usize) {
if len >= s.len() {
return;
}
while len > 0 && !s.is_char_boundary(len) {
len -= 1;
}
s.truncate(len);
}
const CURATED: &[&str] = &[
"",
" ",
"<",
">",
"&",
"\u{0}",
"<Part>",
"</Part>",
"<Part><PartNumber>",
"<Part><PartNumber>1",
"<Part><ETag>",
"<Part><ETag>\"",
"<Part><PartNumber>1</PartNumber><ETag>",
"<Tag><Key>",
"<Tag><Key>k</Key><Value>",
"<Object><Key>",
"<Object><Key>k",
"<Key>é",
"<PartNumber>123",
"<ETag>🦀</ETag>",
"<Value>🦀",
"<Part><PartNumber>999999999999999999999999</PartNumber><ETag>\"x\"</ETag></Part>",
"<Part><PartNumber>1</PartNumber><ETag>"</ETag></Part>",
"<Part><PartNumber>1</PartNumber><ETag>&</ETag></Part>",
"<Part><PartNumber>1</PartNumber><ETag>&</ETag></Part>",
"</PartNumber></Part>",
"<ETag></PartNumber>",
"<Part><Part><Part><Part><Part><Part><Part><Part>",
];
fn assert_all_parsers_survive(input: &str) {
macro_rules! check {
($name:literal, $call:expr) => {{
let result = catch_unwind(AssertUnwindSafe(|| {
let _ = $call;
}));
assert!(
result.is_ok(),
"parser `{}` panicked on input {:?}",
$name,
input
);
}};
}
check!(
"parse_complete_multipart_parts",
parse_complete_multipart_parts(input)
);
check!("parse_tagging_xml", parse_tagging_xml(input));
check!("parse_delete_objects_xml", parse_delete_objects_xml(input));
check!("parse_encryption_xml", parse_encryption_xml(input));
check!("parse_cors_xml", parse_cors_xml(input));
check!("parse_versioning_xml", parse_versioning_xml(input));
check!("parse_lifecycle_xml", parse_lifecycle_xml(input));
check!("parse_website_xml", parse_website_xml(input));
check!("parse_http_date", parse_http_date(input));
}
#[test]
fn curated_corpus_never_panics() {
for input in CURATED {
assert_all_parsers_survive(input);
}
}
#[test]
fn random_soup_never_panics() {
for seed in [0x1234_5678u64, 0xDEAD_BEEF, 0x0BAD_F00D, 1, u64::MAX] {
let mut rng = XorShift64::new(seed);
for _ in 0..5_000 {
let input = gen_soup(&mut rng);
assert_all_parsers_survive(&input);
}
}
}
#[test]
fn template_mutations_never_panic() {
for seed in [0xCAFE_BABEu64, 0x5EED_1234, 42, 0xFFFF_0000_FFFF_0000] {
let mut rng = XorShift64::new(seed);
for _ in 0..5_000 {
let input = gen_mutation(&mut rng);
assert_all_parsers_survive(&input);
}
}
}