Fix read-fault misclassification, DTS AMODE channel table, and untestable guards
- resolve_fmts_key_map: distinguish a genuinely-not-FMTS disc from a
transient live-drive read fault. read_filesystem now returns the new
Error::UdfNotFilesystem for a deterministic tag/format mismatch (no AVDP,
no partition descriptor, no FSD); resolve maps only UdfNotFilesystem (fs)
and UdfNotFound (.tbl absent) to Ok(None), and PROPAGATES DiscRead / other
I/O faults so a marginal AACS 2.1 disc fails loud instead of silently
dropping forensic content under a base-Unit-Key-only map.
- DTS_AMODE_CH (mp4/audio.rs): extend 10→16 entries
{1,2,2,2,2,3,3,4,4,5,6,6,6,7,8,8} (ff_dca_channels / ETSI TS 102 114) so
the spec-legal high AMODEs that now pass the decodability gate declare
their true channelcount (AMODE 13→7, 14/15→8) instead of a truncated 6.
- session.rs resolve_keys "called before scan" guard is now testable:
from_parts_for_test takes Option<Disc>; added a test that a disc-less
session returns a clean DeviceNotReady Err rather than panicking.
- mp4/read.rs: a track with samples but a missing/malformed stts (mandatory
per ISO/IEC 14496-12) is dropped rather than emitting all-zero timestamps,
matching the existing stco/stsc guards; all-tracks-dropped → Mp4Invalid.
- Remove the inert MuxInput::Iso.key_map field (the Iso path re-derives its
map inside build_iso_pipeline); the live path keeps Live.key_map.
All four fixes are mutation-verified.
This commit is contained in:
@@ -60,6 +60,7 @@ pub const E_NO_STREAMS: u16 = 6009;
|
||||
pub const E_HALTED: u16 = 6010;
|
||||
pub const E_MAPFILE_INVALID: u16 = 6011;
|
||||
pub const E_UDF_BUFFER_TOO_SMALL: u16 = 6012;
|
||||
pub const E_UDF_NOT_FILESYSTEM: u16 = 6013;
|
||||
|
||||
// AACS (7xxx)
|
||||
pub const E_AACS_NO_KEYS: u16 = 7000;
|
||||
@@ -272,6 +273,14 @@ pub enum Error {
|
||||
UdfNotFound {
|
||||
path: String,
|
||||
},
|
||||
/// The reader was addressable but the bytes are structurally NOT a UDF
|
||||
/// filesystem — a deterministic tag/format mismatch (e.g. no Anchor Volume
|
||||
/// Descriptor Pointer at sector 256, no partition descriptor, no File Set
|
||||
/// Descriptor). Distinct from [`Error::DiscRead`] (a transient I/O fault):
|
||||
/// this is a stable property of the media, not something a retry fixes. Lets
|
||||
/// callers (notably FMTS key resolution) treat "not a UDF/FMTS disc" as a
|
||||
/// clean negative while still failing loud on a real read fault.
|
||||
UdfNotFilesystem,
|
||||
/// A `SectorSource` caller passed a destination buffer smaller than one
|
||||
/// 2048-byte sector. A contract violation on the public reader API —
|
||||
/// returned instead of panicking on the slice.
|
||||
@@ -564,6 +573,7 @@ impl Error {
|
||||
Error::MplsParse => E_MPLS_PARSE,
|
||||
Error::ClpiParse => E_CLPI_PARSE,
|
||||
Error::UdfNotFound { .. } => E_UDF_NOT_FOUND,
|
||||
Error::UdfNotFilesystem => E_UDF_NOT_FILESYSTEM,
|
||||
Error::UdfBufferTooSmall => E_UDF_BUFFER_TOO_SMALL,
|
||||
Error::DiscTitleRange { .. } => E_DISC_TITLE_RANGE,
|
||||
Error::IfoParse => E_IFO_PARSE,
|
||||
@@ -1275,6 +1285,7 @@ mod tests {
|
||||
E_HALTED,
|
||||
E_MAPFILE_INVALID,
|
||||
E_UDF_BUFFER_TOO_SMALL,
|
||||
E_UDF_NOT_FILESYSTEM,
|
||||
E_AACS_NO_KEYS,
|
||||
E_AACS_CERT_SHORT,
|
||||
E_AACS_AGID_ALLOC,
|
||||
|
||||
+2
-8
@@ -111,10 +111,6 @@ pub enum MuxInput<'a> {
|
||||
format: crate::disc::ContentFormat,
|
||||
/// Decryption keys for the title (`DecryptKeys::None` for raw/clear).
|
||||
keys: DecryptKeys,
|
||||
/// Optional pre-resolved AACS key map. Carried for forward-compat and
|
||||
/// the live path; the file highway re-derives its own map from
|
||||
/// `keys`/`key_fetch` inside [`build_iso_pipeline`].
|
||||
key_map: Option<Arc<AacsKeyMap>>,
|
||||
/// Optional read-time key fetch closure (banked by `resolve_keys`).
|
||||
key_fetch: Option<KeyFetch>,
|
||||
},
|
||||
@@ -280,7 +276,6 @@ pub fn mux_stream(
|
||||
title,
|
||||
format,
|
||||
keys,
|
||||
key_map: _,
|
||||
key_fetch,
|
||||
} => {
|
||||
let reader = FileSectorSource::open(path)?;
|
||||
@@ -1307,7 +1302,6 @@ mod tests {
|
||||
title,
|
||||
format: crate::disc::ContentFormat::BdTs,
|
||||
keys: DecryptKeys::None,
|
||||
key_map: None,
|
||||
key_fetch: None,
|
||||
},
|
||||
"null://",
|
||||
@@ -1620,7 +1614,7 @@ mod tests {
|
||||
let disc = aacs_session_disc(title, unit_key);
|
||||
// No caller key_fetch: a single-CPS disc resolves its base map with the
|
||||
// banked unit key alone (the FMTS/multi-CPS fetch path is not exercised).
|
||||
let mut session = DiscSession::from_parts_for_test(disc, Some(reader), None);
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), Some(reader), None);
|
||||
|
||||
let opts = MuxOptions {
|
||||
skip_errors: false, // a DecryptFailed must PROPAGATE, not zero-fill
|
||||
@@ -1670,7 +1664,7 @@ mod tests {
|
||||
}];
|
||||
let disc = aacs_session_disc(title, unit_key);
|
||||
// reader: None — never staged.
|
||||
let mut session = DiscSession::from_parts_for_test(disc, None, None);
|
||||
let mut session = DiscSession::from_parts_for_test(Some(disc), None, None);
|
||||
|
||||
let opts = MuxOptions {
|
||||
skip_errors: false,
|
||||
|
||||
+37
-2
@@ -239,8 +239,12 @@ const DTS_SFREQ: [u32; 16] = [
|
||||
48_000, 8_000, 16_000, 32_000, 48_000, 48_000, 11_025, 22_050, 44_100, 48_000, 48_000, 12_000,
|
||||
24_000, 48_000, 96_000, 192_000,
|
||||
];
|
||||
/// DTS core base channel count per `AMODE` (0..=9); higher AMODEs are rare on disc.
|
||||
const DTS_AMODE_CH: [u8; 10] = [1, 2, 2, 2, 2, 3, 3, 4, 4, 5];
|
||||
/// DTS core base channel count per `AMODE` (all 16 defined values). Matches the
|
||||
/// reference `ff_dca_channels[16]` table (ETSI TS 102 114) that the decodability
|
||||
/// gate in `dts.rs` (`DTS_AMODE_COUNT`) also uses, so a spec-legal DTS-ES / 6.1 /
|
||||
/// 7.1 core (AMODE 13→7, 14/15→8) is DECLARED with its true channel count in the
|
||||
/// mp4 AudioSampleEntry / `ddts` box rather than a truncated 6.
|
||||
const DTS_AMODE_CH: [u8; 16] = [1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8];
|
||||
|
||||
/// Decoded DTS core parameters needed for the `ddts` box.
|
||||
struct DtsConfig {
|
||||
@@ -557,6 +561,37 @@ mod tests {
|
||||
assert_eq!(&e[4..8], b"dtsh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dts_high_amode_channel_counts_are_declared() {
|
||||
// The 16-entry DTS_AMODE_CH must declare the true core channel count for the
|
||||
// spec-legal high AMODEs that now pass the decodability gate: AMODE 13→7,
|
||||
// 14→8, 15→8 (ETSI TS 102 114 / ff_dca_channels). The old 10-entry table
|
||||
// fell through `unwrap_or(6)` → every one of these was declared as 6.
|
||||
//
|
||||
// Frame layout (mirrors dts_core_5_1_and_ddts): SFREQ=13 (48k), LFF=0 (no
|
||||
// LFE) so `channels` is the bare base count. AMODE is split across
|
||||
// f[7] low nibble (amode>>2) and f[8] top 2 bits (amode&3).
|
||||
// f[8] = (amode&3)<<6 | 13<<2 = ... (keeps SFREQ=13)
|
||||
let frame = |f7: u8, f8: u8| {
|
||||
vec![
|
||||
0x7F, 0xFE, 0x80, 0x01, 0x00, 0x05, 0xF2, f7, f8, 0x00, 0x00, 0x00,
|
||||
]
|
||||
};
|
||||
// AMODE 13 → base 7 channels.
|
||||
let c = parse_dts(&frame(0xF3, 0x74)).expect("amode 13 parses");
|
||||
assert_eq!(c.amode, 13);
|
||||
assert!(!c.lfe);
|
||||
assert_eq!(c.channels, 7, "AMODE 13 core is 7 channels, not 6");
|
||||
// AMODE 14 → base 8 channels.
|
||||
let c = parse_dts(&frame(0xF3, 0xB4)).expect("amode 14 parses");
|
||||
assert_eq!(c.amode, 14);
|
||||
assert_eq!(c.channels, 8, "AMODE 14 core is 8 channels, not 6");
|
||||
// AMODE 15 → base 8 channels.
|
||||
let c = parse_dts(&frame(0xF3, 0xF4)).expect("amode 15 parses");
|
||||
assert_eq!(c.amode, 15);
|
||||
assert_eq!(c.channels, 8, "AMODE 15 core is 8 channels, not 6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_entry_samplerate_does_not_overflow_at_96k() {
|
||||
// 96 kHz > 65535: the 16.16 integer part must saturate, not wrap to garbage.
|
||||
|
||||
@@ -201,6 +201,15 @@ impl<R: Read + Seek> Mp4Reader<R> {
|
||||
let durations = find_box(stbl, b"stts")
|
||||
.map(|b| parse_stts(b, n))
|
||||
.unwrap_or_default();
|
||||
if durations.is_empty() {
|
||||
// Samples exist but there is no decoding-time table: `stts` is
|
||||
// mandatory in a valid stbl (ISO/IEC 14496-12 §8.6.1). Without it
|
||||
// every sample would take dur=0 → all-zero, identical timestamps,
|
||||
// collapsing the whole track onto one instant. Drop the track
|
||||
// rather than emit degenerate timing (mirrors the stco/stsc
|
||||
// guards above); an all-tracks-dropped file fails Mp4Invalid below.
|
||||
continue;
|
||||
}
|
||||
let ctts = find_box(stbl, b"ctts")
|
||||
.map(|b| parse_ctts(b, n))
|
||||
.unwrap_or_default();
|
||||
@@ -1216,11 +1225,21 @@ mod tests {
|
||||
p.extend_from_slice(&0u32.to_be_bytes()); // sample_desc_idx
|
||||
mp4_box(b"stsc", &p)
|
||||
};
|
||||
let stts = {
|
||||
// Mandatory time-to-sample box: 1 entry → 1 sample × 1000 ticks.
|
||||
let mut p = Vec::new();
|
||||
p.extend_from_slice(&[0, 0, 0, 0]); // version+flags
|
||||
p.extend_from_slice(&1u32.to_be_bytes()); // entry_count
|
||||
p.extend_from_slice(&1u32.to_be_bytes()); // sample_count
|
||||
p.extend_from_slice(&1000u32.to_be_bytes()); // sample_delta
|
||||
mp4_box(b"stts", &p)
|
||||
};
|
||||
let mut stbl = Vec::new();
|
||||
stbl.extend_from_slice(&stsd);
|
||||
stbl.extend_from_slice(&stsz);
|
||||
stbl.extend_from_slice(&stco);
|
||||
stbl.extend_from_slice(&stsc);
|
||||
stbl.extend_from_slice(&stts);
|
||||
let minf = mp4_box(b"minf", &mp4_box(b"stbl", &stbl));
|
||||
let mut mdia = Vec::new();
|
||||
mdia.extend_from_slice(&mdhd);
|
||||
@@ -1319,11 +1338,20 @@ mod tests {
|
||||
p.extend_from_slice(&0u32.to_be_bytes()); // sample_desc_idx
|
||||
mp4_box(b"stsc", &p)
|
||||
};
|
||||
let stts = {
|
||||
let mut p = Vec::new();
|
||||
p.extend_from_slice(&[0, 0, 0, 0]); // version+flags
|
||||
p.extend_from_slice(&1u32.to_be_bytes()); // entry_count
|
||||
p.extend_from_slice(&1u32.to_be_bytes()); // sample_count
|
||||
p.extend_from_slice(&1000u32.to_be_bytes()); // sample_delta
|
||||
mp4_box(b"stts", &p)
|
||||
};
|
||||
let mut stbl = Vec::new();
|
||||
stbl.extend_from_slice(&stsd);
|
||||
stbl.extend_from_slice(&stsz);
|
||||
stbl.extend_from_slice(&stco);
|
||||
stbl.extend_from_slice(&stsc);
|
||||
stbl.extend_from_slice(&stts);
|
||||
let minf = mp4_box(b"minf", &mp4_box(b"stbl", &stbl));
|
||||
let mut mdia = Vec::new();
|
||||
mdia.extend_from_slice(&mdhd);
|
||||
@@ -1397,6 +1425,15 @@ mod tests {
|
||||
p.extend_from_slice(&0u32.to_be_bytes()); // sample_desc_idx
|
||||
mp4_box(b"stsc", &p)
|
||||
};
|
||||
let stts = {
|
||||
// Mandatory time-to-sample box: 3 entries' worth via one run (3×1000).
|
||||
let mut p = Vec::new();
|
||||
p.extend_from_slice(&[0, 0, 0, 0]); // version+flags
|
||||
p.extend_from_slice(&1u32.to_be_bytes()); // entry_count
|
||||
p.extend_from_slice(&3u32.to_be_bytes()); // sample_count
|
||||
p.extend_from_slice(&1000u32.to_be_bytes()); // sample_delta
|
||||
mp4_box(b"stts", &p)
|
||||
};
|
||||
let mut stbl = Vec::new();
|
||||
stbl.extend_from_slice(&stsd);
|
||||
stbl.extend_from_slice(&stsz);
|
||||
@@ -1406,6 +1443,9 @@ mod tests {
|
||||
if omit != b"stsc" {
|
||||
stbl.extend_from_slice(&stsc);
|
||||
}
|
||||
if omit != b"stts" {
|
||||
stbl.extend_from_slice(&stts);
|
||||
}
|
||||
let minf = mp4_box(b"minf", &mp4_box(b"stbl", &stbl));
|
||||
let mut mdia = Vec::new();
|
||||
mdia.extend_from_slice(&mdhd);
|
||||
@@ -1446,6 +1486,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// A track with samples (`stsz`) but no time-to-sample table (`stts`, mandatory
|
||||
/// per ISO/IEC 14496-12 §8.6.1) must be DROPPED — without it every sample takes
|
||||
/// dur=0, collapsing the whole track onto one instant (all-zero timestamps).
|
||||
/// With it the only track, the file fails `Mp4Invalid`.
|
||||
/// Mutation check: delete the `if durations.is_empty() { continue; }` guard and
|
||||
/// `from_reader` returns `Ok` (all-zero-timestamp samples), flipping this to FAIL.
|
||||
#[test]
|
||||
fn missing_stts_drops_track_all_dropped_is_invalid() {
|
||||
use std::io::Cursor;
|
||||
let moov = mp4_box(b"moov", &audio_trak_missing(b"stts"));
|
||||
let rd = Mp4Reader::from_reader(Cursor::new(moov), "no-stts".into());
|
||||
assert!(
|
||||
rd.is_err(),
|
||||
"a track with stsz but no stts must be dropped; all-dropped → Mp4Invalid"
|
||||
);
|
||||
}
|
||||
|
||||
/// Sanity companion: the SAME builder WITH both tables present yields a valid,
|
||||
/// indexed single-track file — proving the two Err results above come from the
|
||||
/// missing table, not from some unrelated defect in the fixture builder.
|
||||
|
||||
+110
-5
@@ -789,12 +789,26 @@ fn resolve_fmts_key_map(
|
||||
Ok(())
|
||||
};
|
||||
|
||||
// Load the segment map; absent → not an FMTS disc.
|
||||
let Ok(udf) = crate::udf::read_filesystem(reader) else {
|
||||
return Ok(None);
|
||||
// Load the segment map. Distinguish a genuine "not an FMTS disc" negative
|
||||
// from a transient live-drive I/O fault: swallowing the latter into Ok(None)
|
||||
// would fall through to a base-Unit-Key-only map, garble the forensic units,
|
||||
// let the demux drop them, and complete the mux with NO error — silently
|
||||
// losing forensic content, contradicting this function's fail-loud contract.
|
||||
// - `UdfNotFilesystem`: bytes read fine but are not a UDF disc (deterministic
|
||||
// tag/format mismatch) → genuinely not FMTS → Ok(None).
|
||||
// - `UdfNotFound`: the disc is UDF but has no `IndividualSegment.tbl`
|
||||
// → genuinely not FMTS → Ok(None).
|
||||
// - any other error (notably `DiscRead`): a read fault → propagate so the
|
||||
// rip fails loud / can be retried rather than dropping forensic content.
|
||||
let udf = match crate::udf::read_filesystem(reader) {
|
||||
Ok(u) => u,
|
||||
Err(crate::error::Error::UdfNotFilesystem) => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let Ok(tbl) = udf.read_file(reader, "/AACS/IndividualSegment.tbl") else {
|
||||
return Ok(None);
|
||||
let tbl = match udf.read_file(reader, "/AACS/IndividualSegment.tbl") {
|
||||
Ok(t) => t,
|
||||
Err(crate::error::Error::UdfNotFound { .. }) => return Ok(None),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let Some(segments) = parse_individual_segments(&tbl) else {
|
||||
return Ok(None);
|
||||
@@ -2834,4 +2848,95 @@ mod tests {
|
||||
"the fetched key is appended at pool index 2 and keys the extent"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Fix 1: read-fault vs genuinely-not-FMTS in resolve_fmts_key_map ──────
|
||||
|
||||
/// A SectorSource whose every read is a transient I/O fault (`DiscRead`),
|
||||
/// modelling a marginal live drive stalling while `resolve_fmts_key_map`
|
||||
/// probes the UDF metadata / segment table.
|
||||
struct FaultSource;
|
||||
impl SectorSource for FaultSource {
|
||||
fn capacity_sectors(&self) -> u32 {
|
||||
1_000_000
|
||||
}
|
||||
fn read_sectors(
|
||||
&mut self,
|
||||
lba: u32,
|
||||
_count: u16,
|
||||
_buf: &mut [u8],
|
||||
_recovery: bool,
|
||||
) -> crate::error::Result<usize> {
|
||||
Err(crate::error::Error::DiscRead {
|
||||
sector: lba as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A transient `DiscRead` fault while reading the UDF metadata for the segment
|
||||
/// table must PROPAGATE (fail loud / retryable), NOT be swallowed into the
|
||||
/// not-FMTS `Ok(None)` fall-through — otherwise a marginal AACS 2.1 disc would
|
||||
/// silently drop its forensic content under a base-Unit-Key-only map and the
|
||||
/// mux would report success.
|
||||
///
|
||||
/// Mutation: revert the read_filesystem arm to `let Ok(udf) = ... else { return
|
||||
/// Ok(None) }` → this returns `Ok(None)` and the assert fails.
|
||||
#[test]
|
||||
fn resolve_fmts_key_map_read_fault_propagates() {
|
||||
let mut reader = FaultSource;
|
||||
let title = multi_cps_title(1000, 30);
|
||||
let mut keys = DecryptKeys::Aacs {
|
||||
unit_keys: vec![(0, [0x01u8; 16])],
|
||||
read_data_key: None,
|
||||
format: ContentFormat::BdTs,
|
||||
};
|
||||
let got = super::resolve_fmts_key_map(
|
||||
&mut reader,
|
||||
&title,
|
||||
&mut keys,
|
||||
None,
|
||||
ContentFormat::BdTs,
|
||||
None,
|
||||
);
|
||||
let err = got.expect_err("a transient DiscRead must fail loud, never Ok(None)");
|
||||
let expected = std::io::Error::from(crate::error::Error::DiscRead {
|
||||
sector: 256,
|
||||
status: None,
|
||||
sense: None,
|
||||
})
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
expected,
|
||||
"the DiscRead fault must propagate"
|
||||
);
|
||||
}
|
||||
|
||||
/// A reader whose bytes are structurally NOT a UDF disc (all zeros → no AVDP at
|
||||
/// sector 256 → `UdfNotFilesystem`) is genuinely not FMTS: it must map to the
|
||||
/// clean `Ok(None)` negative, NOT fail loud. Guards against Fix 1 over-reaching
|
||||
/// and rejecting benign non-FMTS discs.
|
||||
#[test]
|
||||
fn resolve_fmts_key_map_not_udf_is_clean_none() {
|
||||
// CipherSource with no registered units reads as all zeros everywhere, so
|
||||
// read_filesystem sees tag_id 0 at sector 256 → UdfNotFilesystem.
|
||||
let mut reader = CipherSource { units: Vec::new() };
|
||||
let title = multi_cps_title(1000, 30);
|
||||
let mut keys = DecryptKeys::Aacs {
|
||||
unit_keys: vec![(0, [0x01u8; 16])],
|
||||
read_data_key: None,
|
||||
format: ContentFormat::BdTs,
|
||||
};
|
||||
let got = super::resolve_fmts_key_map(
|
||||
&mut reader,
|
||||
&title,
|
||||
&mut keys,
|
||||
None,
|
||||
ContentFormat::BdTs,
|
||||
None,
|
||||
)
|
||||
.expect("a structurally non-UDF disc is a clean not-FMTS negative");
|
||||
assert!(got.is_none(), "not a UDF/FMTS disc → Ok(None)");
|
||||
}
|
||||
}
|
||||
|
||||
+23
-2
@@ -381,9 +381,12 @@ impl DiscSession {
|
||||
/// The drive slot stays `None` (a `MuxInput::Session` mux never touches it —
|
||||
/// it reads through the staged `reader`); `device` carries a sentinel path so
|
||||
/// the driver's missing-reader error still has a name.
|
||||
///
|
||||
/// `disc` is an `Option` so a test can construct a session that has NOT been
|
||||
/// scanned (`None`) to exercise the `resolve_keys` "called before scan" guard.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn from_parts_for_test(
|
||||
disc: Disc,
|
||||
disc: Option<Disc>,
|
||||
reader: Option<Box<dyn SectorSource>>,
|
||||
key_fetch: Option<KeyFetch>,
|
||||
) -> DiscSession {
|
||||
@@ -391,7 +394,7 @@ impl DiscSession {
|
||||
drive: None,
|
||||
device: "test://session".to_string(),
|
||||
spec: KeySpec::default(),
|
||||
disc: Some(disc),
|
||||
disc,
|
||||
reader,
|
||||
key_fetch,
|
||||
}
|
||||
@@ -733,4 +736,22 @@ mod tests {
|
||||
"the disc is left untouched"
|
||||
);
|
||||
}
|
||||
|
||||
/// `resolve_keys` called before `scan` (disc slot still `None`) must return the
|
||||
/// clean typed `DeviceNotReady` guard, never reach the `.expect("disc present
|
||||
/// (checked above)")` below it and panic.
|
||||
///
|
||||
/// Mutation: change the `if self.disc.is_none()` guard to `.expect()`/panic
|
||||
/// (e.g. drop the early return) → this test panics instead of getting an Err.
|
||||
#[test]
|
||||
fn resolve_keys_before_scan_is_clean_device_not_ready() {
|
||||
let mut session = DiscSession::from_parts_for_test(None, None, None);
|
||||
let err = session
|
||||
.resolve_keys(factory_of(|| HasUnitKey([1; 16])))
|
||||
.expect_err("resolve_keys before scan must error, not panic");
|
||||
assert!(
|
||||
matches!(err, Error::DeviceNotReady { .. }),
|
||||
"expected DeviceNotReady, got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-15
@@ -731,11 +731,9 @@ pub fn read_filesystem(reader: &mut dyn SectorSource) -> Result<UdfFs> {
|
||||
|
||||
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
|
||||
if tag_id != 2 {
|
||||
return Err(Error::DiscRead {
|
||||
sector: 256,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
// Sector 256 read fine but carries no Anchor Volume Descriptor Pointer:
|
||||
// this is deterministically not a UDF disc, not a transient read fault.
|
||||
return Err(Error::UdfNotFilesystem);
|
||||
}
|
||||
|
||||
// Main VDS extent location: bytes [16:20] = LBA, [20:24] = length
|
||||
@@ -776,11 +774,8 @@ pub fn read_filesystem(reader: &mut dyn SectorSource) -> Result<UdfFs> {
|
||||
}
|
||||
|
||||
if partition_start == 0 {
|
||||
return Err(Error::DiscRead {
|
||||
sector: 0,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
// No Partition Descriptor found in the VDS: structurally not a UDF disc.
|
||||
return Err(Error::UdfNotFilesystem);
|
||||
}
|
||||
|
||||
// Step 3: Parse partition maps from LVD to find metadata partition
|
||||
@@ -871,11 +866,9 @@ pub fn read_filesystem(reader: &mut dyn SectorSource) -> Result<UdfFs> {
|
||||
|
||||
let fsd_tag = u16::from_le_bytes([fsd[0], fsd[1]]);
|
||||
if fsd_tag != 256 {
|
||||
return Err(Error::DiscRead {
|
||||
sector: metadata_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
// The metadata sector read fine but carries no File Set Descriptor:
|
||||
// structurally not a UDF disc, not a transient read fault.
|
||||
return Err(Error::UdfNotFilesystem);
|
||||
}
|
||||
|
||||
// Root Directory ICB: long_ad at FSD offset 400
|
||||
|
||||
Reference in New Issue
Block a user