mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f80551f278 | ||
|
|
41a6d89cd1 |
@@ -1,5 +1,39 @@
|
||||
# Changelog
|
||||
|
||||
## 0.31.6 (2026-06-09)
|
||||
|
||||
### Fixed
|
||||
|
||||
- UDF `read_file`: the AACS Media Key Block (`MKB_RO.inf`) is allocated to a
|
||||
fixed ~128 MiB on UHD discs and zero-padded, but the real record stream is
|
||||
only a few MiB. 0.31.0 added a 64 MiB `MAX_FILE_BYTES` cap on `read_file`,
|
||||
which rejected the padded MKB outright — so `read_aacs_inputs` failed and the
|
||||
online key-resolve path reported "could not read this disc's key files" and
|
||||
**never contacted the keyserver** (a regression on every disc whose MKB
|
||||
exceeds 64 MiB). The MKB is now read length-aware: a bounded prefix is read,
|
||||
the real record length is found via the MKB header, and exactly that is
|
||||
returned — never the padding, and never tripping the cap. Validated
|
||||
end-to-end against a real UHD ISO (MKB reads and trims to its record length).
|
||||
- UDF `read_file`: honor inline/embedded allocation descriptors (ICB Tag flags
|
||||
low bits == 3). Tiny files (some AACS `*.inf` key files) store their data
|
||||
embedded directly in the ICB with no out-of-line extents; these are now read
|
||||
from the ICB payload instead of misparsing the embedded bytes as allocation
|
||||
descriptors (which could hard-error since 0.31.0).
|
||||
|
||||
## 0.31.5 (2026-06-08)
|
||||
|
||||
### Fixed
|
||||
|
||||
- MKV mux: stop forcing strictly-monotonic block timestamps on the **video**
|
||||
track. The monotonic nudge (added for audio PES that collide on a
|
||||
millisecond) was clobbering B-frame video PTS — which are legitimately
|
||||
non-monotonic in decode/storage order — to prev+1ms. A `copy` remux
|
||||
preserved the wrong value, but decoding derived DTS from the HEVC POC and
|
||||
found them colliding, emitting thousands of "non monotonically increasing
|
||||
dts" warnings per title. Video now keeps its true PES PTS (Matroska
|
||||
SimpleBlock permits non-monotonic block timestamps); audio/subtitle tracks
|
||||
still get the nudge. Verified: a re-mux drops the warning count to zero.
|
||||
|
||||
## 0.31.4 (2026-06-08)
|
||||
|
||||
Test cleanup — no runtime changes. Removed 144 unit tests flagged in adversarial
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "0.31.4"
|
||||
version = "0.31.6"
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
+34
-9
@@ -1307,15 +1307,40 @@ impl Disc {
|
||||
.read_file(reader, "/AACS/Unit_Key_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/DUPLICATE/Unit_Key_RO.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let mkb = udf_fs
|
||||
.read_file(reader, "/AACS/MKB_RO.inf")
|
||||
.or_else(|_| udf_fs.read_file(reader, "/AACS/MKB_RW.inf"))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
// Trim trailing padding to the real MKB content length, never zeroing
|
||||
// an unrecognised MKB (see `crate::aacs::trim_mkb` — restores the
|
||||
// pre-0.31.0 guard so the online key service never receives an empty
|
||||
// MKB).
|
||||
Ok((inf, crate::aacs::trim_mkb(mkb)))
|
||||
let mkb = Self::read_mkb_content(reader, udf_fs)?;
|
||||
Ok((inf, mkb))
|
||||
}
|
||||
|
||||
/// Read the AACS MKB's real record stream — NOT its zero padding.
|
||||
///
|
||||
/// `MKB_RO.inf` / `MKB_RW.inf` are allocated to a fixed ~128 MiB and
|
||||
/// zero-padded; the actual record stream is a few MiB. We read a bounded
|
||||
/// prefix, find the record-stream length via [`crate::aacs::mkb_content_len`]
|
||||
/// and return exactly that, growing the prefix if the records run past it.
|
||||
/// This avoids reading 100+ MiB of padding on every scan AND avoids the
|
||||
/// `read_file` `MAX_FILE_BYTES` cap that (since 0.31.0) rejected the padded
|
||||
/// 128 MiB MKB outright — which made `read_aacs_inputs` fail and autorip
|
||||
/// report "could not read this disc's key files" without ever contacting
|
||||
/// the keyserver.
|
||||
fn read_mkb_content(reader: &mut dyn SectorSource, udf_fs: &udf::UdfFs) -> Result<Vec<u8>> {
|
||||
const START_BYTES: usize = 16 * 1024 * 1024;
|
||||
const MAX_BYTES: usize = 64 * 1024 * 1024;
|
||||
let mut want = START_BYTES;
|
||||
loop {
|
||||
let buf = udf_fs
|
||||
.read_file_prefix(reader, "/AACS/MKB_RO.inf", want)
|
||||
.or_else(|_| udf_fs.read_file_prefix(reader, "/AACS/MKB_RW.inf", want))
|
||||
.map_err(|_| Error::AacsNoKeys)?;
|
||||
let n = crate::aacs::mkb_content_len(&buf);
|
||||
// `n` strictly inside `buf` => the record walk reached the padding
|
||||
// boundary (full content captured). `buf` shorter than `want` =>
|
||||
// the whole file is already read. Otherwise the records may run
|
||||
// past the prefix — grow and retry, bounded by MAX_BYTES.
|
||||
if (n > 0 && n < buf.len()) || buf.len() < want || want >= MAX_BYTES {
|
||||
return Ok(crate::aacs::trim_mkb(buf));
|
||||
}
|
||||
want = (want * 2).min(MAX_BYTES);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a disc's AACS key-input files from an ISO image: returns
|
||||
|
||||
+73
-6
@@ -270,6 +270,26 @@ fn monotonic_ts(prev: Option<i64>, pts_ms: i64) -> i64 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-track block timestamp. The strictly-monotonic nudge is applied to
|
||||
/// AUDIO/SUBTITLE tracks only; VIDEO (track 0) is returned UNCHANGED.
|
||||
///
|
||||
/// With B-frames, a video frame's presentation PTS is legitimately
|
||||
/// non-monotonic in decode/storage order (a B-frame sits between its anchors,
|
||||
/// below the frame stored just before it). Forcing it strictly-increasing
|
||||
/// clobbers those PTS to prev+1ms — a `copy` remux preserves the (wrong) value,
|
||||
/// but a decoder derives DTS from the HEVC POC and finds them colliding
|
||||
/// ("non monotonically increasing dts", thousands per title). Matroska
|
||||
/// SimpleBlock permits non-monotonic block timestamps (signed block-relative
|
||||
/// offsets), so video keeps its true PES PTS; only no-reorder tracks (audio,
|
||||
/// subtitles), where a same-millisecond collision IS a real defect, get nudged.
|
||||
fn block_ts(track_idx: usize, prev: Option<i64>, pts_ms: i64) -> i64 {
|
||||
if track_idx == 0 {
|
||||
pts_ms
|
||||
} else {
|
||||
monotonic_ts(prev, pts_ms)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode a Matroska track number as an EBML VINT into a stack buffer,
|
||||
/// returning the buffer and the used length. Track numbers are small (1-based,
|
||||
/// a handful of tracks), so 1 byte covers `< 0x80` and 2 bytes covers the rest;
|
||||
@@ -550,12 +570,19 @@ impl<W: Write + Seek> MkvMuxer<W> {
|
||||
// kept keyframe are clamped to t=0 rather than corrupting the timeline.
|
||||
let pts_ms = (raw_ms - base).max(0);
|
||||
|
||||
// Enforce strictly-monotonic per-track block timestamps. Some audio PES
|
||||
// PTS truncate to the same millisecond as the previous frame (or, rarely,
|
||||
// tick back 1ms), which surfaces as "non-monotonic DTS" and is rejected
|
||||
// by ffmpeg/strict players. Nudge to prev+1ms — sub-frame, inaudible,
|
||||
// and A/V sync is unaffected at millisecond granularity.
|
||||
let pts_ms = monotonic_ts(self.last_pts_ms.get(&track_idx).copied(), pts_ms);
|
||||
// Strictly-monotonic block timestamps — AUDIO/SUBTITLE ONLY. Some audio
|
||||
// PES PTS truncate to the same millisecond as the previous frame (or
|
||||
// tick back 1ms); nudge those to prev+1ms (sub-frame, inaudible).
|
||||
//
|
||||
// VIDEO (track 0) is EXEMPT: with B-frames, presentation PTS is
|
||||
// legitimately non-monotonic in decode/storage order (a B-frame's PTS
|
||||
// sits between its anchors, below the frame stored before it). Forcing
|
||||
// it strictly-increasing clobbers those PTS to prev+1ms, which a `copy`
|
||||
// remux preserves but a decoder rejects — it derives DTS from the HEVC
|
||||
// POC and finds them colliding ("non monotonically increasing dts").
|
||||
// Matroska SimpleBlock permits non-monotonic block timestamps (negative
|
||||
// block-relative offsets), so leave the true PES PTS intact for video.
|
||||
let pts_ms = block_ts(track_idx, self.last_pts_ms.get(&track_idx).copied(), pts_ms);
|
||||
|
||||
let needs_new_cluster = !self.cluster_open
|
||||
|| (is_video_key && (pts_ms - self.cluster_ts_ms) >= CLUSTER_DURATION_MS);
|
||||
@@ -974,6 +1001,46 @@ mod tests {
|
||||
assert_eq!(out, [1000, 1001, 1002, 1003, 1032, 1033, 1064]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_ts_exempts_video_from_monotonic_nudge() {
|
||||
// VIDEO (track 0) keeps its true PTS even when non-monotonic in storage
|
||||
// order — a B-frame whose presentation PTS sits below the frame stored
|
||||
// before it must NOT be nudged to prev+1ms (that clobbering is what
|
||||
// produced the "non monotonically increasing dts" flood on decode).
|
||||
assert_eq!(
|
||||
block_ts(0, Some(1040), 1000),
|
||||
1000,
|
||||
"video B-frame PTS preserved"
|
||||
);
|
||||
assert_eq!(
|
||||
block_ts(0, Some(1000), 1000),
|
||||
1000,
|
||||
"video dup-ms PTS preserved"
|
||||
);
|
||||
// A realistic decode-order GOP (I, then B-frames dipping below it):
|
||||
// every value passes through untouched for video.
|
||||
let gop = [1000i64, 960, 920, 1080, 1040];
|
||||
let mut prev = None;
|
||||
let out: Vec<i64> = gop
|
||||
.iter()
|
||||
.map(|&p| {
|
||||
let t = block_ts(0, prev, p);
|
||||
prev = Some(t);
|
||||
t
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(out, gop, "video timestamps must be left exactly as-is");
|
||||
|
||||
// AUDIO/SUBTITLE (track != 0) still get the strictly-monotonic nudge —
|
||||
// a same-ms collision there is a real defect.
|
||||
assert_eq!(block_ts(1, Some(1000), 1000), 1001, "audio dup-ms nudged");
|
||||
assert_eq!(
|
||||
block_ts(2, Some(1001), 1000),
|
||||
1002,
|
||||
"subtitle back-tick nudged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mkv_multiple_tracks() {
|
||||
let buf = Cursor::new(Vec::new());
|
||||
|
||||
+148
-43
@@ -21,11 +21,19 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::sector::SectorSource;
|
||||
|
||||
/// Upper bound on a single metadata file read (`read_file`). BD-ROM
|
||||
/// metadata files (.mpls/.clpi/.inf/.bdmv) are a few KiB to tens of MiB;
|
||||
/// 64 MiB is a generous ceiling. Caps the allocation so a crafted ICB
|
||||
/// info_length / extent length cannot force a huge zeroed reservation
|
||||
/// before any data is read.
|
||||
/// Upper bound on a single UNBOUNDED metadata file read (`read_file`).
|
||||
/// BD-ROM metadata files (.mpls/.clpi/.bdmv/.inf) are a few KiB to a few
|
||||
/// MiB; 64 MiB is a generous ceiling that bounds the allocation a crafted
|
||||
/// ICB info_length / extent length can force.
|
||||
///
|
||||
/// The one legitimately huge file — the AACS `MKB_RO.inf`, allocated to a
|
||||
/// fixed ~128 MiB and zero-padded — is NOT read through the unbounded path:
|
||||
/// `read_aacs_inputs_from_reader` reads a bounded prefix via
|
||||
/// `read_file_prefix` and trims to the real record length, so it never
|
||||
/// trips this cap (and never reads 100+ MiB of padding). A 0.31.0
|
||||
/// regression added this cap and read the MKB unbounded, so the cap
|
||||
/// rejected it → `read_aacs_inputs` failed → autorip reported "could not
|
||||
/// read this disc's key files" and never contacted the keyserver.
|
||||
const MAX_FILE_BYTES: u64 = 64 * 1024 * 1024;
|
||||
|
||||
/// Upper bound on a single directory's on-disc data. Real BD-ROM
|
||||
@@ -135,6 +143,35 @@ impl UdfFs {
|
||||
/// Read a file by path, returning its raw bytes.
|
||||
/// Reads all data extents sector by sector from disc — no buffering.
|
||||
pub fn read_file(&self, reader: &mut dyn SectorSource, path: &str) -> Result<Vec<u8>> {
|
||||
self.read_file_limited(reader, path, None)
|
||||
}
|
||||
|
||||
/// Read at most `max_bytes` of a file (rounded up to a whole sector),
|
||||
/// stopping early rather than reading the whole file.
|
||||
///
|
||||
/// Used to read only the real, record-length portion of the AACS
|
||||
/// `MKB_RO.inf` — allocated to a fixed ~128 MiB and zero-padded — instead
|
||||
/// of reading 100+ MiB of padding (and tripping `MAX_FILE_BYTES`). The
|
||||
/// caller trims the returned prefix to the MKB record length.
|
||||
pub fn read_file_prefix(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
path: &str,
|
||||
max_bytes: usize,
|
||||
) -> Result<Vec<u8>> {
|
||||
self.read_file_limited(reader, path, Some(max_bytes))
|
||||
}
|
||||
|
||||
/// Shared implementation of [`read_file`] / [`read_file_prefix`]. When
|
||||
/// `max_bytes` is `Some`, reads at most that many bytes and the whole-file
|
||||
/// `MAX_FILE_BYTES` anti-DoS cap on the declared size / extent lengths is
|
||||
/// not applied (the read is already bounded by `max_bytes`).
|
||||
fn read_file_limited(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
path: &str,
|
||||
max_bytes: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let parts: Vec<&str> = path.trim_matches('/').split('/').collect();
|
||||
let mut current = &self.root;
|
||||
|
||||
@@ -166,16 +203,34 @@ impl UdfFs {
|
||||
path: path.to_string(),
|
||||
})?;
|
||||
|
||||
// Read ALL the file's data extents. Multi-extent files (fragmented or
|
||||
// split across dual layers) would otherwise be silently truncated to
|
||||
// the first extent, since the buffer is sized to entry.size and
|
||||
// truncate() can't grow it.
|
||||
// `max_bytes == None` => read the whole file; `Some(n)` => read at most
|
||||
// n bytes (rounded up to a sector) and skip the anti-DoS caps below.
|
||||
let limit = max_bytes.unwrap_or(usize::MAX);
|
||||
|
||||
// Tiny files (notably the AACS `*.inf` key files) may store their data
|
||||
// embedded inline in the ICB (AD type 3), with no out-of-line extents.
|
||||
// Honor that before the extent path, which would otherwise misparse the
|
||||
// embedded bytes as allocation descriptors and (since 0.31.0) hard-error
|
||||
// on the resulting bogus extent length.
|
||||
if let Some(mut inline) = self.read_inline_data(reader, entry.meta_lba)? {
|
||||
let want = (entry.size as usize).min(limit);
|
||||
if inline.len() > want {
|
||||
inline.truncate(want);
|
||||
}
|
||||
return Ok(inline);
|
||||
}
|
||||
|
||||
// Read the file's data extents. Multi-extent files (fragmented or split
|
||||
// across dual layers) would otherwise be silently truncated to the
|
||||
// first extent, since the buffer is sized to entry.size and truncate()
|
||||
// can't grow it.
|
||||
let extents = self.read_icb_extents(reader, entry.meta_lba)?;
|
||||
|
||||
// Reject an oversized declared total before allocating: entry.size is
|
||||
// a raw u64 off the ICB, so a crafted file could otherwise force a
|
||||
// multi-hundred-MiB / GiB allocation across its extents.
|
||||
if entry.size > MAX_FILE_BYTES {
|
||||
// Reject an oversized declared total before allocating: entry.size is a
|
||||
// raw u64 off the ICB, so a crafted file could otherwise force a
|
||||
// multi-hundred-MiB / GiB allocation. Only for the UNBOUNDED path — a
|
||||
// bounded read is limited by `limit` regardless of the declared size.
|
||||
if max_bytes.is_none() && entry.size > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
@@ -183,33 +238,30 @@ impl UdfFs {
|
||||
});
|
||||
}
|
||||
|
||||
// Read ALL the file's data extents. File DATA is in the physical
|
||||
// partition (partition_start + lba), NOT the metadata partition: ICBs
|
||||
// are in metadata, data is in physical.
|
||||
let mut data = Vec::with_capacity(entry.size as usize);
|
||||
// Read the file DATA from the physical partition (partition_start +
|
||||
// lba), NOT the metadata partition: ICBs are in metadata, data is in
|
||||
// physical. Pre-allocate to the smaller of declared size and the
|
||||
// requested prefix (capped so a bogus entry.size can't reserve GiB).
|
||||
let cap_hint = (entry.size as usize)
|
||||
.min(limit)
|
||||
.min(MAX_FILE_BYTES as usize);
|
||||
let mut data = Vec::with_capacity(cap_hint);
|
||||
let mut sector = [0u8; 2048];
|
||||
for (data_lba, data_len) in extents {
|
||||
// Cumulative guard: entry.size and each per-extent data_len are
|
||||
// capped individually above, but a crafted ICB can chain many
|
||||
// small extents (read_icb_extents follows type-3 chains up to
|
||||
// MAX_AD_BLOCKS) whose running total grows `data` into GiB. Reject
|
||||
// once the accumulated bytes would exceed MAX_FILE_BYTES.
|
||||
if data.len() as u64 + data_len as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
}
|
||||
// data_len is the disc-controlled 30-bit extent length; reject an
|
||||
// oversized extent before reading so a crafted ICB can't grow the
|
||||
// buffer past MAX_FILE_BYTES.
|
||||
if data_len as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
'extents: for (data_lba, data_len) in extents {
|
||||
if max_bytes.is_none() {
|
||||
// Anti-DoS guards for the unbounded path: a crafted ICB can
|
||||
// chain many extents whose running total grows `data` into GiB,
|
||||
// or declare a single oversized extent. (Skipped when bounded —
|
||||
// `limit` already caps the read.)
|
||||
if data.len() as u64 + data_len as u64 > MAX_FILE_BYTES
|
||||
|| data_len as u64 > MAX_FILE_BYTES
|
||||
{
|
||||
return Err(Error::DiscRead {
|
||||
sector: self.partition_start as u64,
|
||||
status: None,
|
||||
sense: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
let abs_start = self
|
||||
.partition_start
|
||||
@@ -221,6 +273,9 @@ impl UdfFs {
|
||||
})?;
|
||||
let sector_count = (data_len as u64).div_ceil(2048) as u32;
|
||||
for i in 0..sector_count {
|
||||
if data.len() >= limit {
|
||||
break 'extents;
|
||||
}
|
||||
let abs = abs_start.checked_add(i).ok_or(Error::DiscRead {
|
||||
sector: abs_start as u64,
|
||||
status: None,
|
||||
@@ -231,10 +286,12 @@ impl UdfFs {
|
||||
}
|
||||
}
|
||||
|
||||
// Trim to the real file size; if extents under-covered the file (e.g.
|
||||
// sparse), leave what we have rather than over-reporting.
|
||||
if data.len() > entry.size as usize {
|
||||
data.truncate(entry.size as usize);
|
||||
// Trim to the real file size, or to the requested prefix — whichever is
|
||||
// smaller. If extents under-covered the file (e.g. sparse), leave what
|
||||
// we have rather than over-reporting.
|
||||
let trim_to = (entry.size as usize).min(limit);
|
||||
if data.len() > trim_to {
|
||||
data.truncate(trim_to);
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
@@ -382,6 +439,54 @@ impl UdfFs {
|
||||
})
|
||||
}
|
||||
|
||||
/// If this ICB stores its file data INLINE (embedded — ICB Tag flags low
|
||||
/// 3 bits == 3) rather than via out-of-line extents, return the embedded
|
||||
/// bytes. Tiny files such as the AACS `*.inf` key files are routinely
|
||||
/// embedded directly in the ICB; `read_icb_extents` finds no real extents
|
||||
/// for them (it would misparse the embedded payload as allocation
|
||||
/// descriptors), so `read_file` must read the inline payload here. Returns
|
||||
/// `Ok(None)` for the normal extent-backed case.
|
||||
///
|
||||
/// (Regression guard: 0.31.0 added a per-extent `MAX_FILE_BYTES` cap that
|
||||
/// turned the misparsed-embedded case into a hard error, which surfaced as
|
||||
/// autorip "could not read this disc's key files" on discs whose AACS
|
||||
/// `.inf` files are ICB-embedded — the keyserver was then never called.)
|
||||
fn read_inline_data(
|
||||
&self,
|
||||
reader: &mut dyn SectorSource,
|
||||
meta_lba: u32,
|
||||
) -> Result<Option<Vec<u8>>> {
|
||||
let icb_abs = self.meta_to_abs(meta_lba)?;
|
||||
let mut icb = [0u8; 2048];
|
||||
read_sector(reader, icb_abs, &mut icb)?;
|
||||
let tag = u16::from_le_bytes([icb[0], icb[1]]);
|
||||
let (ad_offset, l_ad) = match tag {
|
||||
// Extended File Entry (266) / standard File Entry (261): the
|
||||
// allocation-descriptors field (which, for embedded files, holds
|
||||
// the data itself) begins after the extended attributes.
|
||||
266 => {
|
||||
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
||||
(216 + l_ea, l_ad)
|
||||
}
|
||||
261 => {
|
||||
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
||||
(176 + l_ea, l_ad)
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
// ICB Tag flags: u16 at absolute offset 34, low 3 bits select the AD
|
||||
// type. 3 == data embedded inline in the ICB.
|
||||
let icb_flags = u16::from_le_bytes([icb[34], icb[35]]);
|
||||
if (icb_flags & 0x07) != 3 {
|
||||
return Ok(None);
|
||||
}
|
||||
let start = ad_offset.min(icb.len());
|
||||
let end = ad_offset.saturating_add(l_ad).min(icb.len());
|
||||
Ok(Some(icb[start..end].to_vec()))
|
||||
}
|
||||
|
||||
/// Read ALL allocation extents for a file from its ICB.
|
||||
/// Returns Vec of (partition_relative_lba, byte_length) pairs.
|
||||
/// Handles files with many extents (e.g. 88 GB m2ts files have ~90 extents)
|
||||
|
||||
Reference in New Issue
Block a user