fix: tighten nres and rsli decode contracts
This commit is contained in:
+174
-19
@@ -20,7 +20,7 @@
|
|||||||
)]
|
)]
|
||||||
//! Strict and lossless `NRes` archive support.
|
//! Strict and lossless `NRes` archive support.
|
||||||
|
|
||||||
use fparkan_binary::{Cursor, DecodeError};
|
use fparkan_binary::{checked_allocation_len, Cursor, DecodeError};
|
||||||
use fparkan_path::{ascii_lookup_key, LookupKey};
|
use fparkan_path::{ascii_lookup_key, LookupKey};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -51,6 +51,33 @@ pub enum WriteProfile {
|
|||||||
CanonicalCompact,
|
CanonicalCompact,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode-time archive limits.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct DecodeLimits {
|
||||||
|
/// Maximum accepted source archive bytes.
|
||||||
|
pub max_input_bytes: u64,
|
||||||
|
/// Maximum accepted entry count.
|
||||||
|
pub max_entries: u32,
|
||||||
|
/// Maximum accepted single payload byte length.
|
||||||
|
pub max_decoded_entry_bytes: u64,
|
||||||
|
/// Maximum accepted cumulative payload bytes.
|
||||||
|
pub max_total_decoded_bytes: u64,
|
||||||
|
/// Maximum accepted preserved-region bytes.
|
||||||
|
pub max_preserved_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DecodeLimits {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_input_bytes: 256 * 1024 * 1024,
|
||||||
|
max_entries: 1_000_000,
|
||||||
|
max_decoded_entry_bytes: 64 * 1024 * 1024,
|
||||||
|
max_total_decoded_bytes: 512 * 1024 * 1024,
|
||||||
|
max_preserved_bytes: 64 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// `NRes` archive header.
|
/// `NRes` archive header.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct NresHeader {
|
pub struct NresHeader {
|
||||||
@@ -343,16 +370,31 @@ impl From<DecodeError> for NresError {
|
|||||||
/// Returns [`NresError`] when the header, directory, payload ranges, or strict
|
/// Returns [`NresError`] when the header, directory, payload ranges, or strict
|
||||||
/// lookup permutation are malformed for the selected [`ReadProfile`].
|
/// lookup permutation are malformed for the selected [`ReadProfile`].
|
||||||
pub fn decode(bytes: Arc<[u8]>, profile: ReadProfile) -> Result<NresDocument, NresError> {
|
pub fn decode(bytes: Arc<[u8]>, profile: ReadProfile) -> Result<NresDocument, NresError> {
|
||||||
let header = parse_header(&bytes)?;
|
decode_with_limits(bytes, profile, DecodeLimits::default())
|
||||||
let entries = parse_entries(&bytes, &header)?;
|
}
|
||||||
|
|
||||||
|
/// Decodes `NRes` bytes with explicit archive limits.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`NresError`] when the input exceeds configured limits, the header,
|
||||||
|
/// directory, payload ranges, or strict lookup permutation are malformed.
|
||||||
|
pub fn decode_with_limits(
|
||||||
|
bytes: Arc<[u8]>,
|
||||||
|
profile: ReadProfile,
|
||||||
|
limits: DecodeLimits,
|
||||||
|
) -> Result<NresDocument, NresError> {
|
||||||
|
let header = parse_header(&bytes, limits)?;
|
||||||
|
let entries = parse_entries(&bytes, &header, limits)?;
|
||||||
validate_names(&entries)?;
|
validate_names(&entries)?;
|
||||||
validate_payload_ranges(&entries)?;
|
validate_payload_ranges(&entries, limits)?;
|
||||||
let lookup_order_valid = match validate_lookup_order(&entries) {
|
let lookup_order_valid = match validate_lookup_order(&entries) {
|
||||||
Ok(valid) => valid,
|
Ok(()) => true,
|
||||||
Err(err) if profile == ReadProfile::Strict => return Err(err),
|
Err(err) if profile == ReadProfile::Strict => return Err(err),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
};
|
};
|
||||||
let preserved_regions = find_preserved_regions(&bytes, &entries, header.directory_offset)?;
|
let preserved_regions =
|
||||||
|
find_preserved_regions(&bytes, &entries, header.directory_offset, limits)?;
|
||||||
Ok(NresDocument {
|
Ok(NresDocument {
|
||||||
bytes,
|
bytes,
|
||||||
header,
|
header,
|
||||||
@@ -684,7 +726,11 @@ impl NresEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_header(bytes: &[u8]) -> Result<NresHeader, NresError> {
|
fn parse_header(bytes: &[u8], limits: DecodeLimits) -> Result<NresHeader, NresError> {
|
||||||
|
enforce_limit(
|
||||||
|
u64::try_from(bytes.len()).map_err(|_| DecodeError::IntegerOverflow)?,
|
||||||
|
limits.max_input_bytes,
|
||||||
|
)?;
|
||||||
if bytes.len() < HEADER_LEN {
|
if bytes.len() < HEADER_LEN {
|
||||||
let mut got = [0; 4];
|
let mut got = [0; 4];
|
||||||
let copy_len = bytes.len().min(4);
|
let copy_len = bytes.len().min(4);
|
||||||
@@ -711,6 +757,7 @@ fn parse_header(bytes: &[u8]) -> Result<NresHeader, NresError> {
|
|||||||
}
|
}
|
||||||
let entry_count =
|
let entry_count =
|
||||||
u32::try_from(entry_count_signed).map_err(|_| DecodeError::IntegerOverflow)?;
|
u32::try_from(entry_count_signed).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
|
enforce_limit(u64::from(entry_count), u64::from(limits.max_entries))?;
|
||||||
let total_size = cursor.read_u32_le()?;
|
let total_size = cursor.read_u32_le()?;
|
||||||
let actual = u64::try_from(bytes.len()).map_err(|_| DecodeError::IntegerOverflow)?;
|
let actual = u64::try_from(bytes.len()).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
if u64::from(total_size) != actual {
|
if u64::from(total_size) != actual {
|
||||||
@@ -750,8 +797,16 @@ fn parse_header(bytes: &[u8]) -> Result<NresHeader, NresError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_entries(bytes: &[u8], header: &NresHeader) -> Result<Vec<NresEntry>, NresError> {
|
fn parse_entries(
|
||||||
let mut entries = Vec::with_capacity(header.entry_count as usize);
|
bytes: &[u8],
|
||||||
|
header: &NresHeader,
|
||||||
|
limits: DecodeLimits,
|
||||||
|
) -> Result<Vec<NresEntry>, NresError> {
|
||||||
|
let capacity = checked_allocation_len(
|
||||||
|
u64::from(header.entry_count),
|
||||||
|
u64::from(limits.max_entries),
|
||||||
|
)?;
|
||||||
|
let mut entries = Vec::with_capacity(capacity);
|
||||||
let directory_offset =
|
let directory_offset =
|
||||||
usize::try_from(header.directory_offset).map_err(|_| DecodeError::IntegerOverflow)?;
|
usize::try_from(header.directory_offset).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
for index in 0..header.entry_count {
|
for index in 0..header.entry_count {
|
||||||
@@ -832,7 +887,7 @@ fn parse_entry(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_payload_ranges(entries: &[NresEntry]) -> Result<(), NresError> {
|
fn validate_payload_ranges(entries: &[NresEntry], limits: DecodeLimits) -> Result<(), NresError> {
|
||||||
let mut ranges: Vec<(u32, Range<usize>)> = entries
|
let mut ranges: Vec<(u32, Range<usize>)> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| (entry.id.0, entry.data_range.clone()))
|
.map(|entry| (entry.id.0, entry.data_range.clone()))
|
||||||
@@ -843,6 +898,15 @@ fn validate_payload_ranges(entries: &[NresEntry]) -> Result<(), NresError> {
|
|||||||
.cmp(&right.1.start)
|
.cmp(&right.1.start)
|
||||||
.then_with(|| left.1.end.cmp(&right.1.end))
|
.then_with(|| left.1.end.cmp(&right.1.end))
|
||||||
});
|
});
|
||||||
|
let mut total_payload_bytes = 0_u64;
|
||||||
|
for entry in entries {
|
||||||
|
let payload_len = u64::from(entry.meta.data_size);
|
||||||
|
enforce_limit(payload_len, limits.max_decoded_entry_bytes)?;
|
||||||
|
total_payload_bytes = total_payload_bytes
|
||||||
|
.checked_add(payload_len)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
enforce_limit(total_payload_bytes, limits.max_total_decoded_bytes)?;
|
||||||
|
}
|
||||||
for pair in ranges.windows(2) {
|
for pair in ranges.windows(2) {
|
||||||
if pair[0].1.end > pair[1].1.start {
|
if pair[0].1.end > pair[1].1.start {
|
||||||
return Err(NresError::EntryDataOverlap {
|
return Err(NresError::EntryDataOverlap {
|
||||||
@@ -863,7 +927,7 @@ fn validate_names(entries: &[NresEntry]) -> Result<(), NresError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_lookup_order(entries: &[NresEntry]) -> Result<bool, NresError> {
|
fn validate_lookup_order(entries: &[NresEntry]) -> Result<(), NresError> {
|
||||||
let entry_count = saturating_u32_len(entries.len());
|
let entry_count = saturating_u32_len(entries.len());
|
||||||
let mut seen = vec![false; entries.len()];
|
let mut seen = vec![false; entries.len()];
|
||||||
for (position, entry) in entries.iter().enumerate() {
|
for (position, entry) in entries.iter().enumerate() {
|
||||||
@@ -881,7 +945,7 @@ fn validate_lookup_order(entries: &[NresEntry]) -> Result<bool, NresError> {
|
|||||||
}
|
}
|
||||||
seen[index_usize] = true;
|
seen[index_usize] = true;
|
||||||
}
|
}
|
||||||
for pair in entries.windows(2) {
|
for (position, pair) in entries.windows(2).enumerate() {
|
||||||
let left_index =
|
let left_index =
|
||||||
usize::try_from(pair[0].meta.sort_index).map_err(|_| DecodeError::IntegerOverflow)?;
|
usize::try_from(pair[0].meta.sort_index).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
let right_index =
|
let right_index =
|
||||||
@@ -889,16 +953,19 @@ fn validate_lookup_order(entries: &[NresEntry]) -> Result<bool, NresError> {
|
|||||||
let left = entries[left_index].name_bytes();
|
let left = entries[left_index].name_bytes();
|
||||||
let right = entries[right_index].name_bytes();
|
let right = entries[right_index].name_bytes();
|
||||||
if cmp_ascii_casefold(left, right) == Ordering::Greater {
|
if cmp_ascii_casefold(left, right) == Ordering::Greater {
|
||||||
return Ok(false);
|
return Err(NresError::SortOrderMismatch {
|
||||||
|
position: saturating_u32_len(position.saturating_add(1)),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_preserved_regions(
|
fn find_preserved_regions(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
entries: &[NresEntry],
|
entries: &[NresEntry],
|
||||||
directory_offset: u32,
|
directory_offset: u32,
|
||||||
|
limits: DecodeLimits,
|
||||||
) -> Result<Vec<PreservedRegion>, NresError> {
|
) -> Result<Vec<PreservedRegion>, NresError> {
|
||||||
let mut ranges: Vec<Range<usize>> = entries
|
let mut ranges: Vec<Range<usize>> = entries
|
||||||
.iter()
|
.iter()
|
||||||
@@ -914,18 +981,44 @@ fn find_preserved_regions(
|
|||||||
let directory_offset =
|
let directory_offset =
|
||||||
usize::try_from(directory_offset).map_err(|_| DecodeError::IntegerOverflow)?;
|
usize::try_from(directory_offset).map_err(|_| DecodeError::IntegerOverflow)?;
|
||||||
let mut preserved = Vec::new();
|
let mut preserved = Vec::new();
|
||||||
|
let mut preserved_bytes = 0_u64;
|
||||||
for range in ranges {
|
for range in ranges {
|
||||||
if cursor < range.start {
|
if cursor < range.start {
|
||||||
|
preserved_bytes = preserved_bytes
|
||||||
|
.checked_add(
|
||||||
|
u64::try_from(range.start - cursor)
|
||||||
|
.map_err(|_| DecodeError::IntegerOverflow)?,
|
||||||
|
)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
enforce_limit(preserved_bytes, limits.max_preserved_bytes)?;
|
||||||
preserved.push(make_preserved_region(bytes, cursor..range.start)?);
|
preserved.push(make_preserved_region(bytes, cursor..range.start)?);
|
||||||
}
|
}
|
||||||
cursor = cursor.max(range.end);
|
cursor = cursor.max(range.end);
|
||||||
}
|
}
|
||||||
if cursor < directory_offset {
|
if cursor < directory_offset {
|
||||||
|
preserved_bytes = preserved_bytes
|
||||||
|
.checked_add(
|
||||||
|
u64::try_from(directory_offset - cursor)
|
||||||
|
.map_err(|_| DecodeError::IntegerOverflow)?,
|
||||||
|
)
|
||||||
|
.ok_or(DecodeError::IntegerOverflow)?;
|
||||||
|
enforce_limit(preserved_bytes, limits.max_preserved_bytes)?;
|
||||||
preserved.push(make_preserved_region(bytes, cursor..directory_offset)?);
|
preserved.push(make_preserved_region(bytes, cursor..directory_offset)?);
|
||||||
}
|
}
|
||||||
Ok(preserved)
|
Ok(preserved)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn enforce_limit(value: u64, limit: u64) -> Result<(), NresError> {
|
||||||
|
if value > limit {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: value,
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn make_preserved_region(bytes: &[u8], range: Range<usize>) -> Result<PreservedRegion, NresError> {
|
fn make_preserved_region(bytes: &[u8], range: Range<usize>) -> Result<PreservedRegion, NresError> {
|
||||||
let all_zero = bytes[range.clone()].iter().all(|byte| *byte == 0);
|
let all_zero = bytes[range.clone()].iter().all(|byte| *byte == 0);
|
||||||
Ok(PreservedRegion {
|
Ok(PreservedRegion {
|
||||||
@@ -1176,7 +1269,10 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
decode(arc(bytes), ReadProfile::Strict),
|
decode(arc(bytes), ReadProfile::Strict),
|
||||||
Err(NresError::DirectoryOutOfBounds { .. })
|
Err(NresError::Binary(DecodeError::LimitExceeded {
|
||||||
|
count,
|
||||||
|
limit
|
||||||
|
})) if count == i32::MAX as u64 && limit == DecodeLimits::default().max_entries as u64
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1469,7 +1565,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unsorted_lookup_table_falls_back_to_linear_lookup() {
|
fn strict_rejects_unsorted_lookup_table() {
|
||||||
let mut bytes = build_archive(&[
|
let mut bytes = build_archive(&[
|
||||||
SyntheticEntry {
|
SyntheticEntry {
|
||||||
type_id: 1,
|
type_id: 1,
|
||||||
@@ -1497,9 +1593,68 @@ mod tests {
|
|||||||
bytes[directory_offset + ENTRY_LEN + 60..directory_offset + ENTRY_LEN + 64]
|
bytes[directory_offset + ENTRY_LEN + 60..directory_offset + ENTRY_LEN + 64]
|
||||||
.copy_from_slice(&1_u32.to_le_bytes());
|
.copy_from_slice(&1_u32.to_le_bytes());
|
||||||
|
|
||||||
let doc = decode(arc(bytes), ReadProfile::Strict).expect("strict nres");
|
assert!(matches!(
|
||||||
assert!(!doc.lookup_order_valid());
|
decode(arc(bytes), ReadProfile::Strict),
|
||||||
assert_eq!(doc.find("A"), Some(EntryId(1)));
|
Err(NresError::SortOrderMismatch { position: 1 })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_entry_count_above_limit() {
|
||||||
|
let bytes = build_archive(&[
|
||||||
|
SyntheticEntry {
|
||||||
|
type_id: 1,
|
||||||
|
attr1: 0,
|
||||||
|
attr2: 0,
|
||||||
|
attr3: 0,
|
||||||
|
name: "a",
|
||||||
|
payload: b"a",
|
||||||
|
},
|
||||||
|
SyntheticEntry {
|
||||||
|
type_id: 2,
|
||||||
|
attr1: 0,
|
||||||
|
attr2: 0,
|
||||||
|
attr3: 0,
|
||||||
|
name: "b",
|
||||||
|
payload: b"b",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_limits(
|
||||||
|
arc(bytes),
|
||||||
|
ReadProfile::Strict,
|
||||||
|
DecodeLimits {
|
||||||
|
max_entries: 1,
|
||||||
|
..DecodeLimits::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Err(NresError::Binary(DecodeError::LimitExceeded { count: 2, limit: 1 }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_preserved_bytes_above_limit() {
|
||||||
|
let bytes = build_archive_with_nonzero_prefix_gap(&[SyntheticEntry {
|
||||||
|
type_id: 1,
|
||||||
|
attr1: 0,
|
||||||
|
attr2: 0,
|
||||||
|
attr3: 0,
|
||||||
|
name: "payload",
|
||||||
|
payload: b"data",
|
||||||
|
}]);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_limits(
|
||||||
|
arc(bytes),
|
||||||
|
ReadProfile::Strict,
|
||||||
|
DecodeLimits {
|
||||||
|
max_preserved_bytes: 4,
|
||||||
|
..DecodeLimits::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Err(NresError::Binary(DecodeError::LimitExceeded { .. }))
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
fparkan-binary = { path = "../fparkan-binary" }
|
||||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
+321
-18
@@ -20,6 +20,7 @@
|
|||||||
)]
|
)]
|
||||||
//! Stage-1 `RsLi` archive contract.
|
//! Stage-1 `RsLi` archive contract.
|
||||||
|
|
||||||
|
use fparkan_binary::DecodeError;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -78,6 +79,33 @@ pub enum WriteProfile {
|
|||||||
Lossless,
|
Lossless,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode and payload loading limits.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct DecodeLimits {
|
||||||
|
/// Maximum accepted source archive bytes.
|
||||||
|
pub max_input_bytes: u64,
|
||||||
|
/// Maximum accepted entry count.
|
||||||
|
pub max_entries: u32,
|
||||||
|
/// Maximum accepted packed entry bytes.
|
||||||
|
pub max_packed_entry_bytes: u64,
|
||||||
|
/// Maximum accepted decoded entry bytes.
|
||||||
|
pub max_decoded_entry_bytes: u64,
|
||||||
|
/// Maximum accepted cumulative decoded bytes for a single load operation.
|
||||||
|
pub max_total_decoded_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DecodeLimits {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_input_bytes: 256 * 1024 * 1024,
|
||||||
|
max_entries: 1_000_000,
|
||||||
|
max_packed_entry_bytes: 64 * 1024 * 1024,
|
||||||
|
max_decoded_entry_bytes: 128 * 1024 * 1024,
|
||||||
|
max_total_decoded_bytes: 128 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Error returned when mutable editing is attempted.
|
/// Error returned when mutable editing is attempted.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum RsliMutationError {
|
pub enum RsliMutationError {
|
||||||
@@ -105,6 +133,11 @@ pub enum RsliMutationError {
|
|||||||
/// Format maximum (`u32::MAX`).
|
/// Format maximum (`u32::MAX`).
|
||||||
max: usize,
|
max: usize,
|
||||||
},
|
},
|
||||||
|
/// Method cannot be represented by the on-disk flags field.
|
||||||
|
UnsupportedMethod {
|
||||||
|
/// Requested method.
|
||||||
|
method: RsliMethod,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for RsliMutationError {
|
impl std::fmt::Display for RsliMutationError {
|
||||||
@@ -120,6 +153,9 @@ impl std::fmt::Display for RsliMutationError {
|
|||||||
Self::PackedPayloadTooLarge { size, max } => {
|
Self::PackedPayloadTooLarge { size, max } => {
|
||||||
write!(f, "packed payload is too large: {size} > {max}")
|
write!(f, "packed payload is too large: {size} > {max}")
|
||||||
}
|
}
|
||||||
|
Self::UnsupportedMethod { method } => {
|
||||||
|
write!(f, "unsupported authoring method: {method:?}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,6 +410,8 @@ pub enum RsliError {
|
|||||||
},
|
},
|
||||||
/// Integer conversion or arithmetic overflow.
|
/// Integer conversion or arithmetic overflow.
|
||||||
IntegerOverflow,
|
IntegerOverflow,
|
||||||
|
/// Shared bounded decode failure.
|
||||||
|
Binary(DecodeError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for RsliError {
|
impl fmt::Display for RsliError {
|
||||||
@@ -432,11 +470,25 @@ impl fmt::Display for RsliError {
|
|||||||
write!(f, "output size mismatch: expected={expected}, got={got}")
|
write!(f, "output size mismatch: expected={expected}, got={got}")
|
||||||
}
|
}
|
||||||
Self::IntegerOverflow => write!(f, "integer overflow"),
|
Self::IntegerOverflow => write!(f, "integer overflow"),
|
||||||
|
Self::Binary(source) => write!(f, "{source}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::error::Error for RsliError {}
|
impl std::error::Error for RsliError {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
match self {
|
||||||
|
Self::Binary(source) => Some(source),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DecodeError> for RsliError {
|
||||||
|
fn from(value: DecodeError) -> Self {
|
||||||
|
Self::Binary(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decodes an `RsLi` document.
|
/// Decodes an `RsLi` document.
|
||||||
///
|
///
|
||||||
@@ -446,7 +498,21 @@ impl std::error::Error for RsliError {}
|
|||||||
/// compatibility quirks, or packed payloads are invalid for the selected
|
/// compatibility quirks, or packed payloads are invalid for the selected
|
||||||
/// profile.
|
/// profile.
|
||||||
pub fn decode(bytes: Arc<[u8]>, profile: ReadProfile) -> Result<RsliDocument, RsliError> {
|
pub fn decode(bytes: Arc<[u8]>, profile: ReadProfile) -> Result<RsliDocument, RsliError> {
|
||||||
decode_with_profile(bytes, profile.into())
|
decode_with_limits(bytes, profile, DecodeLimits::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes an `RsLi` document with explicit archive limits.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RsliError`] when the input exceeds configured limits or the
|
||||||
|
/// archive is malformed for the selected profile.
|
||||||
|
pub fn decode_with_limits(
|
||||||
|
bytes: Arc<[u8]>,
|
||||||
|
profile: ReadProfile,
|
||||||
|
limits: DecodeLimits,
|
||||||
|
) -> Result<RsliDocument, RsliError> {
|
||||||
|
decode_with_profile_and_limits(bytes, profile.into(), limits)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes an `RsLi` document with explicit compatibility switches.
|
/// Decodes an `RsLi` document with explicit compatibility switches.
|
||||||
@@ -459,17 +525,33 @@ pub fn decode(bytes: Arc<[u8]>, profile: ReadProfile) -> Result<RsliDocument, Rs
|
|||||||
pub fn decode_with_profile(
|
pub fn decode_with_profile(
|
||||||
bytes: Arc<[u8]>,
|
bytes: Arc<[u8]>,
|
||||||
profile: RsliReadProfile,
|
profile: RsliReadProfile,
|
||||||
|
) -> Result<RsliDocument, RsliError> {
|
||||||
|
decode_with_profile_and_limits(bytes, profile, DecodeLimits::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes an `RsLi` document with explicit profile and archive limits.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RsliError`] when the input exceeds configured limits or the
|
||||||
|
/// archive is malformed for the selected profile.
|
||||||
|
pub fn decode_with_profile_and_limits(
|
||||||
|
bytes: Arc<[u8]>,
|
||||||
|
profile: RsliReadProfile,
|
||||||
|
limits: DecodeLimits,
|
||||||
) -> Result<RsliDocument, RsliError> {
|
) -> Result<RsliDocument, RsliError> {
|
||||||
let options = match profile {
|
let options = match profile {
|
||||||
RsliReadProfile::Strict => ParseOptions {
|
RsliReadProfile::Strict => ParseOptions {
|
||||||
allow_ao_trailer: false,
|
allow_ao_trailer: false,
|
||||||
allow_deflate_eof_plus_one: false,
|
allow_deflate_eof_plus_one: false,
|
||||||
allow_invalid_presorted_fallback: false,
|
allow_invalid_presorted_fallback: false,
|
||||||
|
limits,
|
||||||
},
|
},
|
||||||
RsliReadProfile::Compatible(profile) => ParseOptions {
|
RsliReadProfile::Compatible(profile) => ParseOptions {
|
||||||
allow_ao_trailer: profile.allow_ao_trailer,
|
allow_ao_trailer: profile.allow_ao_trailer,
|
||||||
allow_deflate_eof_plus_one: profile.allow_deflate_eof_plus_one,
|
allow_deflate_eof_plus_one: profile.allow_deflate_eof_plus_one,
|
||||||
allow_invalid_presorted_fallback: profile.allow_invalid_presorted_fallback,
|
allow_invalid_presorted_fallback: profile.allow_invalid_presorted_fallback,
|
||||||
|
limits,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let ParsedRsli {
|
let ParsedRsli {
|
||||||
@@ -545,6 +627,16 @@ impl RsliDocument {
|
|||||||
/// Returns [`RsliError`] when `id` is invalid or the packed payload cannot
|
/// Returns [`RsliError`] when `id` is invalid or the packed payload cannot
|
||||||
/// be decoded to the declared size.
|
/// be decoded to the declared size.
|
||||||
pub fn load(&self, id: EntryId) -> Result<Vec<u8>, RsliError> {
|
pub fn load(&self, id: EntryId) -> Result<Vec<u8>, RsliError> {
|
||||||
|
self.load_with_limits(id, DecodeLimits::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads and unpacks an entry with explicit decode limits.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RsliError`] when the packed payload exceeds configured
|
||||||
|
/// limits, `id` is invalid, or the payload cannot be decoded.
|
||||||
|
pub fn load_with_limits(&self, id: EntryId, limits: DecodeLimits) -> Result<Vec<u8>, RsliError> {
|
||||||
let record = self.record_by_id(id)?;
|
let record = self.record_by_id(id)?;
|
||||||
let packed = self.packed_slice(id, record)?;
|
let packed = self.packed_slice(id, record)?;
|
||||||
decode_payload(
|
decode_payload(
|
||||||
@@ -552,6 +644,7 @@ impl RsliDocument {
|
|||||||
record.meta.method,
|
record.meta.method,
|
||||||
record.key16,
|
record.key16,
|
||||||
record.meta.unpacked_size,
|
record.meta.unpacked_size,
|
||||||
|
limits,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,6 +744,7 @@ impl RsliEditor {
|
|||||||
/// Returns [`RsliMutationError`] when the entry id is unknown.
|
/// Returns [`RsliMutationError`] when the entry id is unknown.
|
||||||
pub fn set_method(&mut self, id: EntryId, method: RsliMethod) -> Result<(), RsliMutationError> {
|
pub fn set_method(&mut self, id: EntryId, method: RsliMethod) -> Result<(), RsliMutationError> {
|
||||||
let entry = self.entry_mut(id)?;
|
let entry = self.entry_mut(id)?;
|
||||||
|
entry.meta.flags = flags_with_method(entry.meta.flags, method)?;
|
||||||
entry.meta.method = method;
|
entry.meta.method = method;
|
||||||
self.dirty = true;
|
self.dirty = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -837,6 +931,7 @@ struct ParseOptions {
|
|||||||
allow_ao_trailer: bool,
|
allow_ao_trailer: bool,
|
||||||
allow_deflate_eof_plus_one: bool,
|
allow_deflate_eof_plus_one: bool,
|
||||||
allow_invalid_presorted_fallback: bool,
|
allow_invalid_presorted_fallback: bool,
|
||||||
|
limits: DecodeLimits,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -857,6 +952,10 @@ struct EntryRecord {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliError> {
|
fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliError> {
|
||||||
|
enforce_limit(
|
||||||
|
u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
||||||
|
options.limits.max_input_bytes,
|
||||||
|
)?;
|
||||||
if bytes.len() < 32 {
|
if bytes.len() < 32 {
|
||||||
return Err(RsliError::EntryTableOutOfBounds {
|
return Err(RsliError::EntryTableOutOfBounds {
|
||||||
table_offset: 32,
|
table_offset: 32,
|
||||||
@@ -892,6 +991,10 @@ fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliErr
|
|||||||
if count > usize::try_from(u32::MAX).map_err(|_| RsliError::IntegerOverflow)? {
|
if count > usize::try_from(u32::MAX).map_err(|_| RsliError::IntegerOverflow)? {
|
||||||
return Err(RsliError::TooManyEntries { got: count });
|
return Err(RsliError::TooManyEntries { got: count });
|
||||||
}
|
}
|
||||||
|
enforce_limit(
|
||||||
|
u64::try_from(count).map_err(|_| RsliError::IntegerOverflow)?,
|
||||||
|
u64::from(options.limits.max_entries),
|
||||||
|
)?;
|
||||||
|
|
||||||
let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
|
let presorted_flag = u16::from_le_bytes([bytes[14], bytes[15]]);
|
||||||
let xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
let xor_seed = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
|
||||||
@@ -935,6 +1038,14 @@ fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliErr
|
|||||||
let unpacked_size = u32::from_le_bytes([row[20], row[21], row[22], row[23]]);
|
let unpacked_size = u32::from_le_bytes([row[20], row[21], row[22], row[23]]);
|
||||||
let data_offset_raw = u32::from_le_bytes([row[24], row[25], row[26], row[27]]);
|
let data_offset_raw = u32::from_le_bytes([row[24], row[25], row[26], row[27]]);
|
||||||
let packed_size_declared = u32::from_le_bytes([row[28], row[29], row[30], row[31]]);
|
let packed_size_declared = u32::from_le_bytes([row[28], row[29], row[30], row[31]]);
|
||||||
|
enforce_limit(
|
||||||
|
u64::from(packed_size_declared),
|
||||||
|
options.limits.max_packed_entry_bytes,
|
||||||
|
)?;
|
||||||
|
enforce_limit(
|
||||||
|
u64::from(unpacked_size),
|
||||||
|
options.limits.max_decoded_entry_bytes,
|
||||||
|
)?;
|
||||||
let method_raw = u32::from(flags_signed.cast_unsigned()) & 0x1E0;
|
let method_raw = u32::from(flags_signed.cast_unsigned()) & 0x1E0;
|
||||||
let method = parse_method(method_raw);
|
let method = parse_method(method_raw);
|
||||||
|
|
||||||
@@ -1009,9 +1120,12 @@ fn parse_rsli(bytes: &[u8], options: ParseOptions) -> Result<ParsedRsli, RsliErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if presorted_flag == 0xABBA {
|
if presorted_flag == 0xABBA {
|
||||||
if validate_permutation(&records).is_err() {
|
let permutation = validate_permutation(&records);
|
||||||
|
let order = validate_lookup_order(&records);
|
||||||
|
if permutation.is_err() || order.is_err() {
|
||||||
if !options.allow_invalid_presorted_fallback {
|
if !options.allow_invalid_presorted_fallback {
|
||||||
validate_permutation(&records)?;
|
permutation?;
|
||||||
|
order?;
|
||||||
}
|
}
|
||||||
rebuild_sorted_mapping(&mut records)?;
|
rebuild_sorted_mapping(&mut records)?;
|
||||||
}
|
}
|
||||||
@@ -1086,6 +1200,29 @@ fn validate_permutation(records: &[EntryRecord]) -> Result<(), RsliError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_lookup_order(records: &[EntryRecord]) -> Result<(), RsliError> {
|
||||||
|
for pair in records.windows(2) {
|
||||||
|
let left_original = usize::try_from(i32::from(pair[0].meta.sort_to_original))
|
||||||
|
.map_err(|_| RsliError::IntegerOverflow)?;
|
||||||
|
let right_original = usize::try_from(i32::from(pair[1].meta.sort_to_original))
|
||||||
|
.map_err(|_| RsliError::IntegerOverflow)?;
|
||||||
|
let left = records
|
||||||
|
.get(left_original)
|
||||||
|
.ok_or(RsliError::CorruptEntryTable("sort_to_original is not a permutation"))?;
|
||||||
|
let right = records
|
||||||
|
.get(right_original)
|
||||||
|
.ok_or(RsliError::CorruptEntryTable("sort_to_original is not a permutation"))?;
|
||||||
|
if cmp_c_string(c_name_bytes(&left.meta.name_raw), c_name_bytes(&right.meta.name_raw))
|
||||||
|
== std::cmp::Ordering::Greater
|
||||||
|
{
|
||||||
|
return Err(RsliError::CorruptEntryTable(
|
||||||
|
"presorted lookup names are not sorted",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_method(raw: u32) -> RsliMethod {
|
fn parse_method(raw: u32) -> RsliMethod {
|
||||||
match raw {
|
match raw {
|
||||||
0x000 => RsliMethod::Stored,
|
0x000 => RsliMethod::Stored,
|
||||||
@@ -1147,32 +1284,39 @@ fn decode_payload(
|
|||||||
method: RsliMethod,
|
method: RsliMethod,
|
||||||
key16: u16,
|
key16: u16,
|
||||||
unpacked_size: u32,
|
unpacked_size: u32,
|
||||||
|
limits: DecodeLimits,
|
||||||
) -> Result<Vec<u8>, RsliError> {
|
) -> Result<Vec<u8>, RsliError> {
|
||||||
|
enforce_limit(
|
||||||
|
u64::try_from(packed.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
||||||
|
limits.max_packed_entry_bytes,
|
||||||
|
)?;
|
||||||
|
enforce_limit(u64::from(unpacked_size), limits.max_decoded_entry_bytes)?;
|
||||||
|
enforce_limit(u64::from(unpacked_size), limits.max_total_decoded_bytes)?;
|
||||||
let expected = usize::try_from(unpacked_size).map_err(|_| RsliError::IntegerOverflow)?;
|
let expected = usize::try_from(unpacked_size).map_err(|_| RsliError::IntegerOverflow)?;
|
||||||
let out = match method {
|
let out = match method {
|
||||||
RsliMethod::Stored => {
|
RsliMethod::Stored => {
|
||||||
if packed.len() < expected {
|
if packed.len() != expected {
|
||||||
return Err(RsliError::OutputSizeMismatch {
|
return Err(RsliError::OutputSizeMismatch {
|
||||||
expected: unpacked_size,
|
expected: unpacked_size,
|
||||||
got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
|
got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
packed[..expected].to_vec()
|
packed.to_vec()
|
||||||
}
|
}
|
||||||
RsliMethod::XorOnly => {
|
RsliMethod::XorOnly => {
|
||||||
if packed.len() < expected {
|
if packed.len() != expected {
|
||||||
return Err(RsliError::OutputSizeMismatch {
|
return Err(RsliError::OutputSizeMismatch {
|
||||||
expected: unpacked_size,
|
expected: unpacked_size,
|
||||||
got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
|
got: u32::try_from(packed.len()).unwrap_or(u32::MAX),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
xor_stream(&packed[..expected], key16)
|
xor_stream(packed, key16)
|
||||||
}
|
}
|
||||||
RsliMethod::Lzss => lzss_decompress_simple(packed, expected, None)?,
|
RsliMethod::Lzss => lzss_decompress_simple(packed, expected, None)?,
|
||||||
RsliMethod::XorLzss => lzss_decompress_simple(packed, expected, Some(key16))?,
|
RsliMethod::XorLzss => lzss_decompress_simple(packed, expected, Some(key16))?,
|
||||||
RsliMethod::AdaptiveLzss => lzss_huffman_decompress(packed, expected, None)?,
|
RsliMethod::AdaptiveLzss => lzss_huffman_decompress(packed, expected, None)?,
|
||||||
RsliMethod::XorAdaptiveLzss => lzss_huffman_decompress(packed, expected, Some(key16))?,
|
RsliMethod::XorAdaptiveLzss => lzss_huffman_decompress(packed, expected, Some(key16))?,
|
||||||
RsliMethod::RawDeflate => decode_deflate(packed)?,
|
RsliMethod::RawDeflate => decode_deflate(packed, expected)?,
|
||||||
RsliMethod::Unknown(raw) => return Err(RsliError::UnsupportedMethod { raw }),
|
RsliMethod::Unknown(raw) => return Err(RsliError::UnsupportedMethod { raw }),
|
||||||
};
|
};
|
||||||
if out.len() != expected {
|
if out.len() != expected {
|
||||||
@@ -1276,15 +1420,61 @@ fn read_packed_byte(data: &[u8], pos: usize, state: &mut Option<XorState>) -> Op
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_deflate(packed: &[u8]) -> Result<Vec<u8>, RsliError> {
|
fn decode_deflate(packed: &[u8], expected_size: usize) -> Result<Vec<u8>, RsliError> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::with_capacity(expected_size);
|
||||||
|
let mut chunk = [0u8; 4096];
|
||||||
let mut decoder = flate2::read::DeflateDecoder::new(packed);
|
let mut decoder = flate2::read::DeflateDecoder::new(packed);
|
||||||
decoder
|
loop {
|
||||||
.read_to_end(&mut out)
|
let read = decoder
|
||||||
.map_err(|_| RsliError::DecompressionFailed("deflate"))?;
|
.read(&mut chunk)
|
||||||
|
.map_err(|_| RsliError::DecompressionFailed("deflate"))?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let next_len = out
|
||||||
|
.len()
|
||||||
|
.checked_add(read)
|
||||||
|
.ok_or(RsliError::IntegerOverflow)?;
|
||||||
|
if next_len > expected_size {
|
||||||
|
return Err(RsliError::OutputSizeMismatch {
|
||||||
|
expected: u32::try_from(expected_size).unwrap_or(u32::MAX),
|
||||||
|
got: u32::try_from(next_len).unwrap_or(u32::MAX),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
out.extend_from_slice(&chunk[..read]);
|
||||||
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn method_bits(method: RsliMethod) -> Result<u16, RsliMutationError> {
|
||||||
|
match method {
|
||||||
|
RsliMethod::Stored => Ok(0x000),
|
||||||
|
RsliMethod::XorOnly => Ok(0x020),
|
||||||
|
RsliMethod::Lzss => Ok(0x040),
|
||||||
|
RsliMethod::XorLzss => Ok(0x060),
|
||||||
|
RsliMethod::AdaptiveLzss => Ok(0x080),
|
||||||
|
RsliMethod::XorAdaptiveLzss => Ok(0x0A0),
|
||||||
|
RsliMethod::RawDeflate => Ok(0x100),
|
||||||
|
RsliMethod::Unknown(_) => Err(RsliMutationError::UnsupportedMethod { method }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flags_with_method(flags: i32, method: RsliMethod) -> Result<i32, RsliMutationError> {
|
||||||
|
let method = i32::from(method_bits(method)?);
|
||||||
|
Ok((flags & !0x1E0) | method)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enforce_limit(value: u64, limit: u64) -> Result<(), RsliError> {
|
||||||
|
if value > limit {
|
||||||
|
return Err(DecodeError::LimitExceeded {
|
||||||
|
count: value,
|
||||||
|
limit,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
const LZH_N: usize = 4096;
|
const LZH_N: usize = 4096;
|
||||||
const LZH_F: usize = 60;
|
const LZH_F: usize = 60;
|
||||||
const LZH_THRESHOLD: usize = 2;
|
const LZH_THRESHOLD: usize = 2;
|
||||||
@@ -1702,6 +1892,28 @@ mod tests {
|
|||||||
assert_eq!(doc.find("B"), Some(EntryId(0)));
|
assert_eq!(doc.find("B"), Some(EntryId(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_rejects_unsorted_presorted_mapping() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[
|
||||||
|
SyntheticEntry::stored(b"B", 0, b"bee"),
|
||||||
|
SyntheticEntry::stored(b"A", 1, b"aye"),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
0x0103,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode(arc(bytes.clone()), ReadProfile::Strict),
|
||||||
|
Err(RsliError::CorruptEntryTable("presorted lookup names are not sorted"))
|
||||||
|
));
|
||||||
|
|
||||||
|
let doc = decode(arc(bytes), ReadProfile::Compatible).expect("compatible fallback");
|
||||||
|
assert_eq!(doc.find("A"), Some(EntryId(1)));
|
||||||
|
assert_eq!(doc.find("B"), Some(EntryId(0)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_profile_controls_invalid_presorted_fallback() {
|
fn explicit_profile_controls_invalid_presorted_fallback() {
|
||||||
let bytes = synthetic_rsli(
|
let bytes = synthetic_rsli(
|
||||||
@@ -1756,8 +1968,8 @@ mod tests {
|
|||||||
let packed = xor_stream(&plain, 1);
|
let packed = xor_stream(&plain, 1);
|
||||||
let bytes = synthetic_rsli(
|
let bytes = synthetic_rsli(
|
||||||
&[
|
&[
|
||||||
SyntheticEntry::with_payload(b"A", 0x020, 1, &plain, packed),
|
SyntheticEntry::with_payload(b"B", 0x020, 1, &plain, packed),
|
||||||
SyntheticEntry::stored(b"B", 0, b"plain"),
|
SyntheticEntry::stored(b"A", 0, b"plain"),
|
||||||
],
|
],
|
||||||
true,
|
true,
|
||||||
0x2222,
|
0x2222,
|
||||||
@@ -2141,8 +2353,9 @@ mod tests {
|
|||||||
let doc = decode(arc(bytes), ReadProfile::Strict).expect("editable archive");
|
let doc = decode(arc(bytes), ReadProfile::Strict).expect("editable archive");
|
||||||
let mut editor = doc.editor().expect("editor");
|
let mut editor = doc.editor().expect("editor");
|
||||||
editor.set_name(EntryId(1), b"ZETA").expect("edit name");
|
editor.set_name(EntryId(1), b"ZETA").expect("edit name");
|
||||||
|
let repacked = deflate_bytes(b"repacked-alpha");
|
||||||
editor
|
editor
|
||||||
.set_packed_payload(EntryId(0), b"repacked-alpha", 14)
|
.set_packed_payload(EntryId(0), repacked, 14)
|
||||||
.expect("edit packed payload");
|
.expect("edit packed payload");
|
||||||
editor
|
editor
|
||||||
.set_method(EntryId(0), RsliMethod::RawDeflate)
|
.set_method(EntryId(0), RsliMethod::RawDeflate)
|
||||||
@@ -2163,10 +2376,91 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
doc.entries()[original.0 as usize].method,
|
doc.entries()[original.0 as usize].method,
|
||||||
RsliMethod::Stored
|
RsliMethod::RawDeflate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_method_rejects_unknown_authoring_method() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[SyntheticEntry::stored(b"A", 0, b"alpha")],
|
||||||
|
true,
|
||||||
|
0x7780,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let doc = decode(arc(bytes), ReadProfile::Strict).expect("editable archive");
|
||||||
|
let mut editor = doc.editor().expect("editor");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
editor.set_method(EntryId(0), RsliMethod::Unknown(0x1E0)),
|
||||||
|
Err(RsliMutationError::UnsupportedMethod { .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn decode_rejects_entry_count_above_limit() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[
|
||||||
|
SyntheticEntry::stored(b"A", 0, b"alpha"),
|
||||||
|
SyntheticEntry::stored(b"B", 1, b"beta"),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
0x7781,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_limits(
|
||||||
|
arc(bytes),
|
||||||
|
ReadProfile::Strict,
|
||||||
|
DecodeLimits {
|
||||||
|
max_entries: 1,
|
||||||
|
..DecodeLimits::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Err(RsliError::Binary(DecodeError::LimitExceeded { count: 2, limit: 1 }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_entries_require_exact_packed_size() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[SyntheticEntry::with_payload(b"A", 0x000, 0, b"ok", b"ok!".to_vec())],
|
||||||
|
true,
|
||||||
|
0x7782,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let doc = decode(arc(bytes), ReadProfile::Strict).expect("stored archive");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
doc.load(EntryId(0)),
|
||||||
|
Err(RsliError::OutputSizeMismatch { expected: 2, got: 3 })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_unpacked_size_above_limit_before_allocation() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[SyntheticEntry::stored(b"A", 0, b"alpha")],
|
||||||
|
true,
|
||||||
|
0x7783,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let doc = decode(arc(bytes), ReadProfile::Strict).expect("stored archive");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
doc.load_with_limits(
|
||||||
|
EntryId(0),
|
||||||
|
DecodeLimits {
|
||||||
|
max_decoded_entry_bytes: 4,
|
||||||
|
max_total_decoded_bytes: 4,
|
||||||
|
..DecodeLimits::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Err(RsliError::Binary(DecodeError::LimitExceeded { count: 5, limit: 4 }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn editor_rejects_unknown_entry_id_and_invalid_name() {
|
fn editor_rejects_unknown_entry_id_and_invalid_name() {
|
||||||
let bytes = synthetic_rsli(
|
let bytes = synthetic_rsli(
|
||||||
@@ -2637,6 +2931,15 @@ mod tests {
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deflate_bytes(plain: &[u8]) -> Vec<u8> {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let mut encoder =
|
||||||
|
flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||||
|
encoder.write_all(plain).expect("deflate write");
|
||||||
|
encoder.finish().expect("deflate finish")
|
||||||
|
}
|
||||||
|
|
||||||
fn two_plain_rows_for_transform_test() -> Vec<[u8; 32]> {
|
fn two_plain_rows_for_transform_test() -> Vec<[u8; 32]> {
|
||||||
let mut a = [0u8; 32];
|
let mut a = [0u8; 32];
|
||||||
let mut b = [0u8; 32];
|
let mut b = [0u8; 32];
|
||||||
|
|||||||
Reference in New Issue
Block a user