mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Security - Remove the unsafe Vec::set_len over recycled sector buffers — UB reachable from safe code (GHSA-j8ww-f5fg-9pmh, low). Uses Vec::resize; pooling unchanged. CI / tooling - clippy on stable with the MSRV single-sourced from Cargo.toml; cargo-deny dependency-audit gate with first-party git sources allowed (#15); audience-based comment-guard (#17). Docs - README (keydb ownership, BD-J parser count), drive-access transport/reset notes, mux timeline module-doc links.
441 lines
24 KiB
Markdown
441 lines
24 KiB
Markdown
# UDF 2.50 Filesystem Parser
|
|
|
|
## What is UDF?
|
|
|
|
UDF (Universal Disc Format) is the filesystem standard for optical media. It is defined by ECMA-167 with extensions from the OSTA (Optical Storage Technology Association). BD-ROM discs use UDF revision 2.50, which introduces the **metadata partition** -- a critical feature that separates file metadata from file content on disc.
|
|
|
|
## Why UDF 2.50 for Blu-ray?
|
|
|
|
Older UDF revisions (1.02, 1.50) scatter ICBs (file metadata) and file data across the same partition. On a high-capacity Blu-ray disc (25-100 GB), this creates excessive seeking when the drive needs to read a directory listing or locate a file. UDF 2.50 solves this by placing all metadata into a contiguous region near the beginning of the disc. The drive reads metadata from one compact area and streams file data from another -- no interleaved seeks.
|
|
|
|
BD-ROM Part 3 of the Blu-ray specification mandates UDF 2.50.
|
|
|
|
## Metadata Partitions
|
|
|
|
A UDF 2.50 BD-ROM has two logical partitions:
|
|
|
|
- **Partition 0 (Type 1)** -- the physical partition. Contains actual file data (m2ts streams, playlist files, etc.). Mapped directly to disc sectors starting at the Partition Descriptor's `partitionStartingLocation`.
|
|
|
|
- **Partition 1 (Type 2)** -- the metadata partition. Contains all ICBs (Inode-like structures), directory data, and the File Set Descriptor. The metadata partition is itself stored as a file within the physical partition. Its location is found by reading an Extended File Entry at LBA 0 of the physical partition.
|
|
|
|
The key rule: **ICBs and directory data live in the metadata partition. File content lives in the physical partition.** When an ICB's allocation descriptor gives an LBA, the partition it refers to depends on what the LBA describes -- metadata-relative for directory entries, physical-partition-relative for file data extents.
|
|
|
|
## Pointer Chain
|
|
|
|
Reading a UDF 2.50 filesystem follows a fixed chain of pointers. Each step reads one or two 2048-byte sectors:
|
|
|
|
```
|
|
Sector 256: AVDP (Anchor Volume Descriptor Pointer, tag 2)
|
|
|
|
|
v
|
|
Sectors 32-63: VDS (Volume Descriptor Sequence)
|
|
|-- Partition Descriptor (tag 5) --> partition_start (physical sector)
|
|
|-- Logical Volume Descriptor (tag 6) --> partition maps, FSD location
|
|
|
|
|
v
|
|
Partition Maps in LVD (offset 440):
|
|
|-- Map 0: Type 1 (physical partition)
|
|
|-- Map 1: Type 2 (metadata partition, identified by "*UDF Metadata Partition")
|
|
|
|
|
v
|
|
Metadata file ICB at partition_start + 0 (Extended File Entry, tag 266)
|
|
|-- Allocation descriptor --> metadata content location
|
|
|
|
|
v
|
|
metadata_start = partition_start + allocation_position
|
|
|
|
|
v
|
|
FSD at metadata_start + 0 (File Set Descriptor, tag 256)
|
|
|-- Root Directory ICB: long_ad at offset 400 --> root_lba (metadata-relative)
|
|
|
|
|
v
|
|
Root Directory ICB at metadata_start + root_lba (Extended File Entry, tag 266)
|
|
|-- Allocation descriptor --> directory data location (metadata-relative)
|
|
|
|
|
v
|
|
Directory data: File Identifier Descriptors (tag 257)
|
|
|-- Each FID names a file/subdirectory and points to its ICB
|
|
|-- Recurse into subdirectories to build the full file tree
|
|
```
|
|
|
|
### AVDP (Sector 256)
|
|
|
|
The Anchor Volume Descriptor Pointer is always at sector 256 (ECMA-167 section 10.2). It points to the Main Volume Descriptor Sequence. Tag identifier = 2.
|
|
|
|
### VDS (Sectors 32+)
|
|
|
|
The Volume Descriptor Sequence contains:
|
|
|
|
- **Partition Descriptor (tag 5)**: byte offset 188 holds `partitionStartingLocation` -- the absolute sector where the physical partition begins.
|
|
- **Logical Volume Descriptor (tag 6)**: byte offset 268 holds the number of partition maps. The partition maps themselves start at offset 440. For BD-ROM, map 0 is Type 1 (physical) and map 1 is Type 2 (metadata).
|
|
- **Terminating Descriptor (tag 8)**: signals the end of the VDS.
|
|
|
|
### Metadata File
|
|
|
|
When two partition maps exist and the second is Type 2, the metadata partition content is located by reading the Extended File Entry at the first sector of the physical partition (partition_start + 0). This ICB's allocation descriptor gives the offset and length of the metadata content within the physical partition.
|
|
|
|
### File Set Descriptor
|
|
|
|
The FSD (tag 256) sits at metadata-relative LBA 0 (the first sector of the metadata content). It contains a long allocation descriptor at offset 400 pointing to the root directory ICB. The LBA in this long_ad is at bytes 404-407.
|
|
|
|
### Directory Traversal
|
|
|
|
Each directory is an ICB (Extended File Entry, tag 266, or File Entry, tag 261) whose allocation extent points to directory data. The directory data is a sequence of File Identifier Descriptors (FIDs, tag 257):
|
|
|
|
| FID Field | Offset | Size | Description |
|
|
|-----------|--------|------|-------------|
|
|
| Tag | 0 | 2 | Always 257 |
|
|
| File characteristics | 18 | 1 | Bit 1 = directory, bit 3 = parent |
|
|
| L_FI (name length) | 19 | 1 | Length of filename |
|
|
| ICB (long_ad) | 20 | 16 | Points to the entry's ICB |
|
|
| L_IU | 36 | 2 | Implementation use length |
|
|
| Filename | 38 + L_IU | L_FI | UDF-encoded filename |
|
|
|
|
FIDs are 4-byte aligned. The parser advances by `(38 + L_IU + L_FI + 3) & !3` bytes per entry.
|
|
|
|
### ICB Layout
|
|
|
|
Both File Entry (tag 261) and Extended File Entry (tag 266) share the same info_length field:
|
|
|
|
| Field | Tag 261 Offset | Tag 266 Offset |
|
|
|-------|---------------|---------------|
|
|
| info_length (u64) | 56 | 56 |
|
|
| L_EA (u32) | 168 | 208 |
|
|
| L_AD (u32) | 172 | 212 |
|
|
| Allocation descriptors | 176 + L_EA | 216 + L_EA |
|
|
|
|
Allocation descriptors use the Short Allocation Descriptor format: 4 bytes extent length (upper 2 bits = type), 4 bytes extent position (LBA).
|
|
|
|
## How read_filesystem() Works
|
|
|
|
The `read_filesystem()` function in `src/udf.rs` follows the pointer chain above:
|
|
|
|
1. Reads sector 256, validates AVDP (tag 2).
|
|
2. Scans sectors 32-63 for the Partition Descriptor and Logical Volume Descriptor.
|
|
3. If two partition maps exist and the second is Type 2, reads the metadata file ICB at partition_start to find metadata_start.
|
|
4. Reads the FSD at metadata_start, extracts the root directory ICB LBA.
|
|
5. Calls `read_directory()` recursively (max depth `MAX_DIR_DEPTH` = 8) to build the full file tree.
|
|
|
|
Each directory read involves two sector reads: one for the ICB, then one or more for the directory data. File sizes are read from info_length in each file's ICB.
|
|
|
|
`read_file()` reads a file by navigating the directory tree, reading the file's ICB to get its data extent, then reading the data sector by sector from the **physical partition** (partition_start + LBA, not metadata_start).
|
|
|
|
## Buffered Sector Reads
|
|
|
|
USB optical drives have ~500ms round-trip latency per SCSI command. Since `read_filesystem()` and `read_file()` issue one SCSI READ per sector, a full disc scan can require hundreds of commands -- taking 10+ minutes on USB.
|
|
|
|
`Disc::scan()` wraps the drive in a `BufferedSectorReader` before reading. On a single-sector read, the buffer prefetches a batch of sectors (sized from the kernel's `max_hw_sectors_kb` for the device) and caches them. Subsequent reads to nearby LBAs return from cache with zero SCSI overhead. After parsing the UDF directory structure, the entire metadata partition is pre-read into the cache, so all ICB lookups during title scanning and encryption resolution are instant.
|
|
|
|
`prefetch_ranges` pre-reads multiple sector ranges into the permanent cache,
|
|
in batch-sized chunks, stored per-sector in a `HashMap`. This is used to
|
|
bulk-load all small files (AACS, MPLS, CLPI, META) before scanning. The
|
|
permanent cache holds one ~2 KB `Vec` per sector in the `HashMap`, so the
|
|
total sector count bounds RAM; a crafted UDF (a bogus metadata-file size in
|
|
`metadata_sector_ranges`) could otherwise drive that count to billions. The
|
|
cumulative prefetched sectors are capped at `MAX_PREFETCH_SECTORS` (~1 GiB
|
|
of cache); once exceeded, prefetching stops seeding the cache. The
|
|
sliding-window read path still serves any LBA on demand, so this only
|
|
forgoes the bulk speed-up — it never loses data.
|
|
|
|
The buffer is transparent -- `read_filesystem()`, `read_file()`, and all downstream code still call `read_sectors(lba, 1, buf)` as before. The batching happens inside the `SectorSource` implementation.
|
|
|
|
### UDF Filename Encoding
|
|
|
|
UDF filenames use a compression ID as the first byte:
|
|
- `8` = 8-bit characters (ASCII)
|
|
- `16` = 16-bit big-endian Unicode (UTF-16BE)
|
|
|
|
The parser handles both encodings. All path lookups are case-insensitive.
|
|
|
|
## BD-ROM Directory Structure
|
|
|
|
A typical Blu-ray disc has this directory layout:
|
|
|
|
```
|
|
/
|
|
+-- BDMV/
|
|
| +-- index.bdmv Disc index (title list, first play)
|
|
| +-- MovieObject.bdmv Movie objects (navigation commands)
|
|
| +-- PLAYLIST/
|
|
| | +-- 00000.mpls Main movie playlist
|
|
| | +-- 00001.mpls Director's commentary
|
|
| | +-- ...
|
|
| +-- CLIPINF/
|
|
| | +-- 00001.clpi Clip info for 00001.m2ts
|
|
| | +-- 00002.clpi
|
|
| | +-- ...
|
|
| +-- STREAM/
|
|
| | +-- 00001.m2ts Transport stream (video/audio/subtitle data)
|
|
| | +-- 00002.m2ts
|
|
| | +-- ...
|
|
| +-- BACKUP/ Duplicate of index, MovieObject, playlists, clip info
|
|
|
|
|
+-- AACS/ AACS encryption data (encrypted discs only)
|
|
| +-- Unit_Key_RO.inf Unit key file (encrypted)
|
|
| +-- MKB_RW.inf Media Key Block
|
|
| +-- Content000.cer Content certificate
|
|
| +-- DUPLICATE/ Backup copies
|
|
|
|
|
+-- CERTIFICATE/ BD+ certificate data (some discs)
|
|
```
|
|
|
|
The parser reads from `BDMV/PLAYLIST/` and `BDMV/CLIPINF/` to discover titles and their sector layouts. The `BDMV/STREAM/` directory contains the actual transport streams but is not parsed by the UDF layer -- stream data is read by LBA directly using extents computed from CLPI EP maps.
|
|
|
|
## metadata_sector_ranges() skip policy
|
|
|
|
`metadata_sector_ranges()` skips directories named `STREAM` (case-insensitive)
|
|
and individual files larger than 50 MB. Nothing else is filtered by name —
|
|
`BACKUP`/`DUPLICATE` are traversed, and `MKB_RO.inf` is excluded only because
|
|
it exceeds the 50 MB cap.
|
|
|
|
## metadata_file_location() and read_directory() internals
|
|
|
|
`metadata_file_location` returns `None` when the map does not fit wholly
|
|
inside the descriptor, or when its partition type identifier is not
|
|
"*UDF Metadata Partition" — a Virtual (UDF 2.50 2.2.8) or Sparable (2.2.9)
|
|
partition map is also ECMA-167 3/10.7.3 Type 2 and records unrelated fields
|
|
at that offset, so its bytes must never be read as a location.
|
|
|
|
`read_directory` walks each directory's ICB (Extended File Entry) to find
|
|
directory data containing File Identifier Descriptors (FIDs); each FID names
|
|
a file/subdir and points to its ICB. Directories deeper than `MAX_DIR_DEPTH`
|
|
are recorded as entries but not descended into. `budget` tracks total FID
|
|
entries consumed across the whole tree walk and the walk aborts with
|
|
`Error::DiscRead` once it exceeds `MAX_TOTAL_DIR_ENTRIES`. `visited` is the
|
|
set of metadata-relative ICB LBAs already opened as directories; a repeated
|
|
LBA is a cycle and is skipped. The function's wide argument list (reader,
|
|
partition/meta offsets, depth, the global entry budget, and the
|
|
cycle-detection visited-set) is inherent to the walk, not a refactor smell.
|
|
|
|
## extents_abs_at(), file_extents(), and file_extents_addressing()
|
|
|
|
`extents_abs_at` resolves multi-extent / Long-AD / continuation ICBs to
|
|
absolute disc extents. Unrecorded (ECMA-167 4/14.14.1.1 type-1) extents are
|
|
included: their space is allocated to the file and occupies its byte space,
|
|
so dropping them would slide every later extent's bytes down by the hole's
|
|
length in a sequential extraction. They are returned FLAGGED
|
|
(`AbsExtent::recorded == false`), because keeping the extent while losing
|
|
the flag is the same bug the other way round: the caller would then read
|
|
sectors the file never wrote and ship whatever the media holds there.
|
|
`read_file_limited` emits zeros for such an extent; every consumer of this
|
|
list has to be able to do the same. It is `pub(crate)` because it returns
|
|
`AbsExtent`, itself internal.
|
|
|
|
`file_extents` refuses a file that contains an unrecorded extent because a
|
|
`(lba, sector_count)` pair cannot say "this range is a hole": reading it
|
|
splices undefined sectors into the stream as content, and dropping it
|
|
slides every later extent's byte space. Until an extent can carry the flag
|
|
end-to-end, refusing is the only truthful answer at this signature — and a
|
|
title that is not offered is not a title that was silently mis-ripped.
|
|
|
|
`file_extents_addressing` is the byte-space-only counterpart: it includes
|
|
unrecorded extents UNFLAGGED, in a shape identical to `file_extents`'s safe
|
|
return, because omitting one would misaddress the rest of the file and
|
|
nothing on this path treats the returned sectors as stream bytes. It stays
|
|
`pub(crate)` rather than public: an external consumer reaching for the more
|
|
general-sounding name would silently obtain a read plan over a hole, and a
|
|
doc comment alone is not a guard against that — scoping makes the misuse a
|
|
compile error.
|
|
|
|
## read_icb_extent(): first RECORDED extent, not extents.first()
|
|
|
|
`read_icb_extents` retains ECMA-167 4/14.14.1.1 type-1 (allocated, not
|
|
recorded) descriptors, because dropping one would slide every later extent's
|
|
data down by the hole's length. But a type-1 extent's `lba` is where SPACE is
|
|
allocated, not where bytes live, and `read_icb_extent`'s one caller —
|
|
`file_start_lba` — hands its result out as "the absolute starting LBA of a
|
|
file's first data extent". `ifo.rs` then uses that as the base for every VTS
|
|
VOB extent (`file_start_lba(IFO) + vtstt_vobs + cell.first_sector`), so a
|
|
file whose FIRST descriptor is type-1 would put the entire video title set
|
|
at the wrong place on disc, with no error anywhere. `read_icb_extent`
|
|
therefore returns the first extent with `recorded == true`, not
|
|
`extents.first()`.
|
|
|
|
## read_inline_data() and the AACS .inf regression
|
|
|
|
Tiny files such as the AACS `*.inf` key files are routinely embedded
|
|
directly in the ICB (ICB Tag flags low 3 bits == 3) rather than via
|
|
out-of-line extents. `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 via `read_inline_data` instead. A
|
|
0.31.0 regression 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.
|
|
|
|
## MAX_FILE_BYTES and the AACS MKB
|
|
|
|
`read_file()` caps a single unbounded read at `MAX_FILE_BYTES` (64 MiB) to
|
|
bound the allocation a crafted ICB `info_length`/extent length can force.
|
|
The one legitimately huge file on disc, the AACS `MKB_RO.inf`, is allocated
|
|
to a fixed ~128 MiB and zero-padded, so it is NOT read through this unbounded
|
|
path: `read_aacs_inputs_from_reader` instead reads a bounded prefix via
|
|
`read_file_prefix` and trims to the real record length, so it never trips
|
|
the cap (and never reads 100+ MiB of padding). A 0.31.0 regression read the
|
|
MKB through the unbounded path instead, so the cap rejected it,
|
|
`read_aacs_inputs` failed, and autorip reported "could not read this disc's
|
|
key files" without ever contacting the keyserver.
|
|
|
|
## merge_ranges() half-open semantics
|
|
|
|
`merge_ranges` takes HALF-OPEN `(start, count)` ranges, so `start + count`
|
|
is the EXCLUSIVE end and two ranges touch exactly when the next `start`
|
|
equals the previous end. A range that starts one sector LATER than that has
|
|
a genuine, untouched sector between them, and merging across it claims
|
|
coverage of a sector neither input ever described. That matters because the
|
|
merged output is a boundary, not a hint: `disc::merged_extents` feeds
|
|
`Disc::encrypted_content_ranges`, which decides which sectors the
|
|
decrypting source treats as content. A sector in a real gap belongs to no
|
|
title extent — it is nav/UDF/padding — and must stay outside the content
|
|
gate, not be folded into it.
|
|
|
|
## build_dir_icb_tagged() offsets
|
|
|
|
`build_dir_icb_tagged` builds a directory ICB with an explicit descriptor
|
|
tag, extended attribute length, RAW (unmasked) allocation-descriptor length
|
|
field and allocation position. ECMA-167 4/14.17 Extended File Entry (tag
|
|
266): L_EA at byte 208, L_AD at 212, allocation descriptors start at
|
|
216 + L_EA. ECMA-167 4/14.9 File Entry (tag 261): L_EA at 168, L_AD at 172,
|
|
allocation descriptors start at 176 + L_EA. The extended-attribute area is
|
|
filled with a recognisable non-zero pattern (0xA5) so that reading the
|
|
allocation descriptor from the wrong offset cannot silently produce a
|
|
usable value.
|
|
|
|
## Test fixture builders
|
|
|
|
`build_efe_long` builds a tag-266 ICB whose allocation descriptors are LONG
|
|
ADs (16 bytes: len(4) | lba(4) | part_ref(2) | impl_use(6)); the trailing
|
|
`part_ref`/`impl_use` bytes are left zero, which is what trips the old
|
|
8-byte-stride parser into reading a bogus zero-length terminator.
|
|
|
|
`build_entry_ads` builds a tag-261/266 entry with short ADs and every
|
|
disc-controlled field of the descriptor area exposed: `l_ea` (extended
|
|
attribute length — descriptors begin at the entry type's own base, 176 for
|
|
261 or 216 for 266, plus this; the attribute area is filled with 0xA5 so a
|
|
descriptor read from the wrong offset cannot silently produce a usable
|
|
value), `l_ad` (the DECLARED descriptor-area length, independent of how many
|
|
descriptors are actually written), and `extra` (descriptors written
|
|
immediately after the declared area, i.e. bytes the entry does not claim are
|
|
descriptors at all).
|
|
|
|
## VDS anchor shape-vs-content fallback tests
|
|
|
|
An anchor whose recorded VDS extent passes every SHAPE check (length >= 16
|
|
sectors, non-zero location, no address wrap) but holds no sequence must
|
|
still mount, by sweeping the customary location. Shape is a property of the
|
|
FIELD, not of what is there: a mastering tool that wrote the reserve
|
|
location, a stale anchor on a rewritten volume, or deliberate corruption all
|
|
produce this. Selecting the fallback on shape alone would leave the branch a
|
|
damaged disc actually takes with no recovery path at all — the sibling
|
|
Metadata File Location chain in the same function already treats its
|
|
recorded value as a CANDIDATE and falls back on outcome; this is that
|
|
principle applied one level up.
|
|
|
|
The recorded extent and the customary fallback are two SWEEPS, not two
|
|
locations: an anchor may point at the customary LBA and still declare a
|
|
window narrower than the fallback's. De-duplicating them on start LBA alone
|
|
then throws the wider sweep away and the sequence is never reached, even
|
|
though it is exactly where the fallback would have looked. The regression
|
|
test's fixture records `(LBA 32, 16 sectors)` — the minimum ECMA-167
|
|
3/10.2.1 permits, so it passes every shape check — while the Partition and
|
|
Logical Volume Descriptors sit at 50/51, inside the customary 32-sector
|
|
window but past the declared 16. No Terminating Descriptor lies in 32..48,
|
|
or both sweeps would stop at the same sector and the wider one would buy
|
|
nothing.
|
|
|
|
`conformant_meta_vol`'s extent length and position are given distinct byte
|
|
patterns, and distinct patterns from one another, so a descriptor byte
|
|
sourced from the wrong offset — or the two fields transposed — changes the
|
|
reported metadata partition rather than landing on a value that happens to
|
|
work. The length is an exact multiple of the 2048-byte logical sector, as a
|
|
real metadata partition is: its only observable is a sector COUNT, which
|
|
rounds up, so an off-by-one byte in the low half is visible only when the
|
|
true value sits exactly on the boundary.
|
|
|
|
## u32::MAX overflow guards in BufferedSectorReader
|
|
|
|
`prefetch` advances the read LBA with `start_lba + offset`. A UDF whose
|
|
metadata descriptor declares a partition near the top of the LBA space
|
|
(ECMA-167 §14.1: `extent_location` is an unconstrained Uint32) drove that
|
|
add past `u32::MAX` — an 'attempt to add with overflow' panic in debug
|
|
inside the public `Disc::scan`, and in release a wrap to a low LBA that
|
|
filled the sliding cache with a completely different region while
|
|
`cache_start` still claimed the high one.
|
|
|
|
`prefetch_ranges` walks each disc-derived `(start_lba, sector_count)` range
|
|
with `start + offset + i`. `collect_file_ranges` permits any LBA up to
|
|
`u32::MAX` (ECMA-167 §14.14.1 allocation descriptors are unconstrained
|
|
Uint32s) and nothing bounds `start + count`, so a range within one batch of
|
|
the top of the space overflowed: debug panic inside `Disc::scan`, or in
|
|
release a wrap that seeded the PERMANENT cache with this file's bytes keyed
|
|
at LBA 0 — every later single-sector read of those low LBAs (the AVDP/VDS
|
|
re-reads) then returned the wrong sector.
|
|
|
|
The sliding-cache hit test computed `cache_start + cache_sectors`. Once
|
|
`cache_start` is a disc-controlled LBA near `u32::MAX` (set by a batch read)
|
|
that add overflowed: debug panic inside `SectorSource::read_sectors`,
|
|
release wrap to a small value that silently disabled the cache.
|
|
|
|
## file_extents() refusal and continuation-cycle test rationale
|
|
|
|
`file_extents` builds the extent list a Blu-ray / HD-DVD TITLE is actually
|
|
ripped through (`disc/bluray.rs`, `disc/hddvd.rs`, `mux/resolve.rs`). Its
|
|
`(lba, sector_count)` tuple cannot say "this range is a hole", and both ways
|
|
of pretending otherwise are wrong: folding an unrecorded extent in makes the
|
|
mux read undefined sectors and splice whatever the media holds there into
|
|
the stream as content, while dropping it slides every later extent's byte
|
|
space (that list IS a byte-space map, `aacs::segment::clip_byte_to_lba`). So
|
|
at this signature the only truthful answer is refusal.
|
|
|
|
`icb_extents_exhausted_continuation_budget_is_an_error_not_a_short_list`
|
|
covers hostile input: a type-3 continuation descriptor whose continuation
|
|
block points back at itself (a cycle). The `MAX_AD_BLOCKS` bound must make
|
|
this TERMINATE rather than loop forever, and terminating by exhausting the
|
|
budget is not the same as reaching the end of the chain. It used to fall
|
|
through to `Ok(extents)` with the list silently cut short at whatever the
|
|
256th hop had collected; `disc/extract.rs` then zero-pads the missing tail
|
|
out to the entry's declared size, sets `complete = true` with
|
|
`bytes_unreadable = 0`, and renames off `.partial` — a mostly-zero file
|
|
delivered as a verified complete extraction. Exhausting the budget means "I
|
|
do not know the rest of this file", and the only honest answer is an error.
|
|
|
|
## Embedded-data and zero-length-hole test fixtures
|
|
|
|
AD type 3 is EMBEDDED data, not a descriptor list. RED BEFORE GREEN: with
|
|
the old `_ => 8` fallback, `read_icb_extents` returned
|
|
`Ok([IcbExtent { lba: 999, len: 2048 }])` — an extent decoded out of the
|
|
file's own CONTENT bytes. A rip would then read sector 999, which holds
|
|
something else entirely, and emit it as this title's stream at rc=0. That is
|
|
the failure the refusal exists to prevent, so the
|
|
`icb_extents_embedded_data_is_refused_not_decoded_as_descriptors` fixture
|
|
deliberately makes the content decode as a PLAUSIBLE extent rather than as
|
|
garbage: garbage would have been caught by the terminator check anyway, and
|
|
would prove nothing.
|
|
|
|
`file_extents` refuses a file whose extent list contains an unrecorded hole,
|
|
because reading one splices undefined sectors into the rip and dropping one
|
|
slides every later extent's byte space. Neither is true of a hole with
|
|
LENGTH ZERO: it displaces nothing, every later extent sits at the same
|
|
offset with or without it, and the callers' own `sectors > 0 && lba > 0`
|
|
filter already discards it. Refusing on `!recorded` alone therefore dropped
|
|
whole titles off discs that ripped correctly before — the reverse of the
|
|
defect the refusal exists for, and just as silent, since the title simply
|
|
vanishes. `file_extents_accepts_a_zero_length_unrecorded_extent` covers this.
|
|
|
|
## build_efe_ext() extended allocation descriptor layout
|
|
|
|
`build_efe_ext` builds an Extended File Entry (tag 266) ICB whose allocation
|
|
descriptors are EXTENDED ADs (ECMA-167 §14.14.3, 20 bytes each):
|
|
`ExtentLength(4) | RecordedLength(4) | InformationLength(4) | ExtentLocation
|
|
lb_addr { logicalBlockNumber(4) | partitionRef(2) } | impl_use(2)`. The
|
|
30-bit length + 2-bit type live in ExtentLength (offset +0); the logical
|
|
block number lives in ExtentLocation at offset +12. It sets ICB Tag flags
|
|
(abs offset 34) low bits to 2 = Extended AD so the parser must select the
|
|
20-byte stride and read the LBA from off+12, not off+4.
|
|
|
|
## References
|
|
|
|
- ECMA-167: Volume and File Structure of Write-Once and Rewritable Media
|
|
- UDF 2.50 (OSTA): Universal Disk Format Specification
|
|
- BD-ROM Part 3: Blu-ray Disc Read-Only Format, File System Specifications
|