fix: expose configurable rsli read profiles
This commit is contained in:
+165
-11
@@ -14,6 +14,44 @@ pub enum ReadProfile {
|
|||||||
Compatible,
|
Compatible,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detailed read profile.
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum RsliReadProfile {
|
||||||
|
/// Reject compatibility quirks.
|
||||||
|
Strict,
|
||||||
|
/// Accept selected retail compatibility quirks.
|
||||||
|
Compatible(RsliCompatibilityProfile),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ReadProfile> for RsliReadProfile {
|
||||||
|
fn from(value: ReadProfile) -> Self {
|
||||||
|
match value {
|
||||||
|
ReadProfile::Strict => Self::Strict,
|
||||||
|
ReadProfile::Compatible => Self::Compatible(RsliCompatibilityProfile::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RsliReadProfile {
|
||||||
|
/// Strict profile with every compatibility quirk disabled.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn strict() -> Self {
|
||||||
|
Self::Strict
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retail-compatible profile with the default approved quirk set.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn compatible() -> Self {
|
||||||
|
Self::Compatible(RsliCompatibilityProfile::retail())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retail-compatible profile with a caller-provided quirk set.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn compatible_with(profile: RsliCompatibilityProfile) -> Self {
|
||||||
|
Self::Compatible(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Write profile.
|
/// Write profile.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum WriteProfile {
|
pub enum WriteProfile {
|
||||||
@@ -34,12 +72,30 @@ pub struct RsliCompatibilityProfile {
|
|||||||
|
|
||||||
impl Default for RsliCompatibilityProfile {
|
impl Default for RsliCompatibilityProfile {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
Self::retail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RsliCompatibilityProfile {
|
||||||
|
/// Retail-compatible profile with every approved quirk enabled.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn retail() -> Self {
|
||||||
Self {
|
Self {
|
||||||
allow_ao_trailer: true,
|
allow_ao_trailer: true,
|
||||||
allow_deflate_eof_plus_one: true,
|
allow_deflate_eof_plus_one: true,
|
||||||
allow_invalid_presorted_fallback: true,
|
allow_invalid_presorted_fallback: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Profile with every compatibility quirk disabled.
|
||||||
|
#[must_use]
|
||||||
|
pub const fn none() -> Self {
|
||||||
|
Self {
|
||||||
|
allow_ao_trailer: false,
|
||||||
|
allow_deflate_eof_plus_one: false,
|
||||||
|
allow_invalid_presorted_fallback: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `RsLi` packing method.
|
/// `RsLi` packing method.
|
||||||
@@ -197,6 +253,11 @@ pub enum RsliError {
|
|||||||
/// Archive byte length.
|
/// Archive byte length.
|
||||||
file_len: u64,
|
file_len: u64,
|
||||||
},
|
},
|
||||||
|
/// Registered `AO` overlay is rejected by the selected profile.
|
||||||
|
AoTrailerQuirkRejected {
|
||||||
|
/// Overlay byte offset.
|
||||||
|
overlay: u32,
|
||||||
|
},
|
||||||
/// Unsupported packing method.
|
/// Unsupported packing method.
|
||||||
UnsupportedMethod {
|
UnsupportedMethod {
|
||||||
/// Raw method bits.
|
/// Raw method bits.
|
||||||
@@ -266,6 +327,9 @@ impl fmt::Display for RsliError {
|
|||||||
"media overlay out of bounds: overlay={overlay}, file={file_len}"
|
"media overlay out of bounds: overlay={overlay}, file={file_len}"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Self::AoTrailerQuirkRejected { overlay } => {
|
||||||
|
write!(f, "AO trailer quirk rejected: overlay={overlay}")
|
||||||
|
}
|
||||||
Self::UnsupportedMethod { raw } => write!(f, "unsupported packing method: {raw:#x}"),
|
Self::UnsupportedMethod { raw } => write!(f, "unsupported packing method: {raw:#x}"),
|
||||||
Self::PackedSizePastEof {
|
Self::PackedSizePastEof {
|
||||||
id,
|
id,
|
||||||
@@ -298,20 +362,31 @@ 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decodes an `RsLi` document with explicit compatibility switches.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`RsliError`] when the header, table, payload ranges, registered
|
||||||
|
/// compatibility quirks, or packed payloads are invalid for the selected
|
||||||
|
/// profile.
|
||||||
|
pub fn decode_with_profile(
|
||||||
|
bytes: Arc<[u8]>,
|
||||||
|
profile: RsliReadProfile,
|
||||||
|
) -> Result<RsliDocument, RsliError> {
|
||||||
let options = match profile {
|
let options = match profile {
|
||||||
ReadProfile::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,
|
||||||
},
|
},
|
||||||
ReadProfile::Compatible => {
|
RsliReadProfile::Compatible(profile) => ParseOptions {
|
||||||
let profile = RsliCompatibilityProfile::default();
|
allow_ao_trailer: profile.allow_ao_trailer,
|
||||||
ParseOptions {
|
allow_deflate_eof_plus_one: profile.allow_deflate_eof_plus_one,
|
||||||
allow_ao_trailer: profile.allow_ao_trailer,
|
allow_invalid_presorted_fallback: profile.allow_invalid_presorted_fallback,
|
||||||
allow_deflate_eof_plus_one: profile.allow_deflate_eof_plus_one,
|
},
|
||||||
allow_invalid_presorted_fallback: profile.allow_invalid_presorted_fallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let ParsedRsli {
|
let ParsedRsli {
|
||||||
header,
|
header,
|
||||||
@@ -691,7 +766,7 @@ fn rebuild_sorted_mapping(records: &mut [EntryRecord]) -> Result<(), RsliError>
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ao_trailer(bytes: &[u8], allow: bool) -> Result<(u32, Option<[u8; 6]>), RsliError> {
|
fn parse_ao_trailer(bytes: &[u8], allow: bool) -> Result<(u32, Option<[u8; 6]>), RsliError> {
|
||||||
if !allow || bytes.len() < 6 || &bytes[bytes.len() - 6..bytes.len() - 4] != b"AO" {
|
if bytes.len() < 6 || &bytes[bytes.len() - 6..bytes.len() - 4] != b"AO" {
|
||||||
return Ok((0, None));
|
return Ok((0, None));
|
||||||
}
|
}
|
||||||
let mut raw = [0u8; 6];
|
let mut raw = [0u8; 6];
|
||||||
@@ -703,6 +778,9 @@ fn parse_ao_trailer(bytes: &[u8], allow: bool) -> Result<(u32, Option<[u8; 6]>),
|
|||||||
file_len: u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
file_len: u64::try_from(bytes.len()).map_err(|_| RsliError::IntegerOverflow)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if !allow {
|
||||||
|
return Err(RsliError::AoTrailerQuirkRejected { overlay });
|
||||||
|
}
|
||||||
Ok((overlay, Some(raw)))
|
Ok((overlay, Some(raw)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1330,6 +1408,39 @@ mod tests {
|
|||||||
assert_eq!(doc.find("B"), Some(EntryId(0)));
|
assert_eq!(doc.find("B"), Some(EntryId(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_profile_controls_invalid_presorted_fallback() {
|
||||||
|
let bytes = synthetic_rsli(
|
||||||
|
&[
|
||||||
|
SyntheticEntry::stored(b"B", 0, b"bee"),
|
||||||
|
SyntheticEntry::stored(b"A", 0, b"aye"),
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
0x0102,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let profile = RsliCompatibilityProfile {
|
||||||
|
allow_invalid_presorted_fallback: false,
|
||||||
|
..RsliCompatibilityProfile::retail()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_profile(
|
||||||
|
arc(bytes.clone()),
|
||||||
|
RsliReadProfile::compatible_with(profile)
|
||||||
|
),
|
||||||
|
Err(RsliError::CorruptEntryTable(_))
|
||||||
|
));
|
||||||
|
|
||||||
|
let profile = RsliCompatibilityProfile {
|
||||||
|
allow_invalid_presorted_fallback: true,
|
||||||
|
..RsliCompatibilityProfile::none()
|
||||||
|
};
|
||||||
|
let doc = decode_with_profile(arc(bytes), RsliReadProfile::compatible_with(profile))
|
||||||
|
.expect("presorted fallback only");
|
||||||
|
assert_eq!(doc.find("A"), Some(EntryId(1)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stored_method_uses_exact_size() {
|
fn stored_method_uses_exact_size() {
|
||||||
let bytes = synthetic_rsli(
|
let bytes = synthetic_rsli(
|
||||||
@@ -1510,6 +1621,16 @@ mod tests {
|
|||||||
decode(arc(approved.clone()), ReadProfile::Strict),
|
decode(arc(approved.clone()), ReadProfile::Strict),
|
||||||
Err(RsliError::DeflateEofPlusOneQuirkRejected { id: 0 })
|
Err(RsliError::DeflateEofPlusOneQuirkRejected { id: 0 })
|
||||||
));
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_profile(
|
||||||
|
arc(approved.clone()),
|
||||||
|
RsliReadProfile::compatible_with(RsliCompatibilityProfile {
|
||||||
|
allow_deflate_eof_plus_one: false,
|
||||||
|
..RsliCompatibilityProfile::retail()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
Err(RsliError::DeflateEofPlusOneQuirkRejected { id: 0 })
|
||||||
|
));
|
||||||
let doc = decode(arc(approved), ReadProfile::Compatible).expect("approved EOF+1 quirk");
|
let doc = decode(arc(approved), ReadProfile::Compatible).expect("approved EOF+1 quirk");
|
||||||
assert_eq!(doc.load(EntryId(0)).expect("approved payload"), b"raw");
|
assert_eq!(doc.load(EntryId(0)).expect("approved payload"), b"raw");
|
||||||
|
|
||||||
@@ -1606,11 +1727,22 @@ mod tests {
|
|||||||
Some(4),
|
Some(4),
|
||||||
);
|
);
|
||||||
|
|
||||||
let doc = decode(arc(bytes), ReadProfile::Compatible).expect("AO overlay");
|
let doc = decode(arc(bytes.clone()), ReadProfile::Compatible).expect("AO overlay");
|
||||||
let meta = doc.entry(EntryId(0)).expect("AO meta");
|
let meta = doc.entry(EntryId(0)).expect("AO meta");
|
||||||
assert_eq!(meta.data_offset, 64);
|
assert_eq!(meta.data_offset, 64);
|
||||||
assert_eq!(meta.data_offset_raw, 60);
|
assert_eq!(meta.data_offset_raw, 60);
|
||||||
assert_eq!(doc.load(EntryId(0)).expect("AO payload"), b"media");
|
assert_eq!(doc.load(EntryId(0)).expect("AO payload"), b"media");
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_profile(
|
||||||
|
arc(bytes),
|
||||||
|
RsliReadProfile::compatible_with(RsliCompatibilityProfile {
|
||||||
|
allow_ao_trailer: false,
|
||||||
|
..RsliCompatibilityProfile::retail()
|
||||||
|
})
|
||||||
|
),
|
||||||
|
Err(RsliError::AoTrailerQuirkRejected { overlay: 4 })
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1625,6 +1757,28 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn strict_profile_distinguishes_valid_ao_quirk_from_malformed_ao() {
|
||||||
|
let valid = synthetic_rsli(
|
||||||
|
&[SyntheticEntry::stored(b"A", 0, b"media")],
|
||||||
|
true,
|
||||||
|
0x3333,
|
||||||
|
Some(4),
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_profile(arc(valid), RsliReadProfile::strict()),
|
||||||
|
Err(RsliError::AoTrailerQuirkRejected { overlay: 4 })
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut malformed = synthetic_rsli(&[], false, 0, None);
|
||||||
|
malformed.extend_from_slice(b"AO");
|
||||||
|
malformed.extend_from_slice(&1000u32.to_le_bytes());
|
||||||
|
assert!(matches!(
|
||||||
|
decode_with_profile(arc(malformed), RsliReadProfile::strict()),
|
||||||
|
Err(RsliError::MediaOverlayOutOfBounds { overlay: 1000, .. })
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_header_bytes_are_lossless() {
|
fn unknown_header_bytes_are_lossless() {
|
||||||
let mut bytes = synthetic_rsli(
|
let mut bytes = synthetic_rsli(
|
||||||
|
|||||||
Reference in New Issue
Block a user