use super::*;
use flate2::{Compression, GzBuilder};
use std::io::Write as _;
fn gzip(bytes: &[u8]) -> Vec<u8> {
let mut encoder = GzBuilder::new()
.mtime(0)
.write(Vec::new(), Compression::fast());
encoder.write_all(bytes).unwrap();
encoder.finish().unwrap()
}
#[test]
fn inclusive_compressed_and_decoded_limits_preserve_input() {
let payload = vec![42; 40_001];
let compressed = gzip(&payload);
let original = compressed.clone();
assert_eq!(
decode_gzip(&compressed, compressed.len(), payload.len()).unwrap(),
payload
);
assert!(
matches!(decode_gzip(&compressed, compressed.len() - 1, payload.len()),
Err(GzipError::InputLimit { actual, limit })
if actual == compressed.len() && limit == compressed.len() - 1)
);
assert!(matches!(
decode_gzip(&compressed, compressed.len(), payload.len() - 1),
Err(GzipError::Decode(ArtifactError::LimitExceeded {
limit: 40_000
}))
));
assert_eq!(compressed, original);
}
#[test]
fn empty_payloads_and_predecode_input_limits_are_distinct() {
let compressed = gzip(b"");
assert_eq!(
decode_gzip(&compressed, compressed.len(), 0).unwrap(),
[] as [u8; 0]
);
assert!(matches!(
decode_gzip(&[], 0, 0),
Err(GzipError::Decode(ArtifactError::Io(_)))
));
assert!(matches!(
decode_gzip(b"invalid gzip", 0, 0),
Err(GzipError::InputLimit {
actual: 12,
limit: 0
})
));
let nonempty = gzip(b"a");
assert!(matches!(
decode_gzip(&nonempty, nonempty.len(), 0),
Err(GzipError::Decode(ArtifactError::LimitExceeded { limit: 0 }))
));
}
#[test]
fn truncated_headers_payloads_and_footers_never_return_partial_bytes() {
let compressed = gzip(b"retained artifact payload");
for end in 0..compressed.len() {
assert!(matches!(
decode_gzip(&compressed[..end], compressed.len(), 1024),
Err(GzipError::Decode(ArtifactError::Io(_)))
));
}
}
#[test]
fn malformed_headers_crc_and_length_fail_with_structured_errors() {
let compressed = gzip(b"retained artifact payload");
for index in [0, 2, compressed.len() - 8, compressed.len() - 4] {
let mut corrupt = compressed.clone();
corrupt[index] ^= 1;
assert!(matches!(
decode_gzip(&corrupt, corrupt.len(), 1024),
Err(GzipError::Decode(ArtifactError::Io(_)))
));
}
}
#[test]
fn trailing_bytes_and_concatenated_members_are_rejected() {
let compressed = gzip(b"payload");
for tail in [vec![0], b"unrelated".to_vec(), gzip(b""), gzip(b"second")] {
let mut joined = compressed.clone();
joined.extend_from_slice(&tail);
assert!(matches!(
decode_gzip(&joined, joined.len(), 1024),
Err(GzipError::TrailingData)
));
}
let mut corrupt = compressed;
let crc = corrupt.len() - 8;
corrupt[crc] ^= 1;
corrupt.push(0);
assert!(matches!(
decode_gzip(&corrupt, corrupt.len(), 1024),
Err(GzipError::Decode(ArtifactError::Io(_)))
));
}
#[test]
fn inflation_is_bounded_independently_of_small_compressed_input() {
let compressed = gzip(&vec![0; 1_000_000]);
assert!(matches!(
decode_gzip(&compressed, compressed.len(), 16),
Err(GzipError::Decode(ArtifactError::LimitExceeded {
limit: 16
}))
));
}
#[test]
fn optional_header_fields_count_against_the_complete_input_allowance() {
let mut encoder = GzBuilder::new()
.mtime(0)
.filename("fixture.wasm")
.comment("saved artifact")
.extra(vec![1, 2, 3])
.write(Vec::new(), Compression::fast());
encoder.write_all(b"payload").unwrap();
let compressed = encoder.finish().unwrap();
assert_eq!(
decode_gzip(&compressed, compressed.len(), 7).unwrap(),
b"payload"
);
assert!(matches!(
decode_gzip(&compressed, compressed.len() - 1, 7),
Err(GzipError::InputLimit { .. })
));
}