1462 Commits
Author SHA1 Message Date
Matthew Jackson 041ac5876f test: make case-collision test host-FS-independent
Probe dir_is_case_insensitive against the real target volume by creating
the dir first. Previously the test probed a not-yet-created dir, which
returns a conservative true; that disagreed with extract_tree's own
post-mkdir probe on a case-sensitive host (Linux CI), panicking the test.
Both branches (collision on insensitive, clean coexist on sensitive) still
verified.
2026-09-17 22:37:17 -07:00
Matthew Jackson e0875b630c docs: fix broken intra-doc links
- decrypt.rs: three refs to the #[cfg(test)]-only decrypt_sectors_mapped
  wrapper can't resolve in a non-test doc build; de-link to inline code
  (keeps the name, no broken link).
- Fix DecryptingSectorSource::with_content_ranges link to its full
  crate::sector path so it resolves.
2026-09-17 22:37:17 -07:00
Matthew Jackson d5902ff9f9 deps: sha1 0.10→0.11 (collapse RustCrypto digest/block-buffer/crypto-common dup); drop unused rand 2026-09-17 22:18:07 -07:00
Matthew Jackson 01cd85f514 changelog: open 1.7.2 — UNRELEASED (unified release; firmware ABI v2 in freemkv-unlock) 2026-09-17 14:35:17 -07:00
Matthew Jackson 6de61484a3 docs: point the freemkv-unlock path-dep note at release-orchestrate.yml (release.sh is gone) 2026-09-17 14:24:49 -07:00
Matthew Jackson 764519de43 aacs/disc: test coverage for VID key-derivation + handshake route selection; diag: print unknown mkb_version 2026-09-15 20:37:18 -07:00
Matthew Jackson 2fb7d78b29 libfreemkv: retry READ CAPACITY for ISO sizing; hard-error if unavailable
READ CAPACITY is the only authoritative whole-disc size for an image
(the UDF partition is a subset — measured 288 sectors / ~576 KB short on
a real BD, which would truncate the backup anchor). Its field failures
are sporadic (5/5 reliable on the target drive), so retry it a few times
rather than substituting a short value. On persistent failure the shared
scan/identify path stays lenient (capacity 0; MKV reads by extent) and
image_read_sectors() returns EmptyImage so an ISO fails cleanly instead
of writing a truncated or empty file.
2026-09-15 16:45:48 -07:00
Matthew Jackson 18024ea5f5 scsi/macos: fix io_service_t leak, honor bsd_name in fallback, fix FFI doc
shim_list_drives: reorder the drain loop condition so IOIteratorNext is
not called on the exit iteration, releasing every obtained io_service_t.

shim_open_exclusive: the fallback matcher ignored the requested bsd_name
and could open a different drive; iterate and accept only the candidate
whose IOMedia BSD name matches, else fail. Correct the misleading da_hold
TOCTOU comment.
2026-09-15 14:40:54 -07:00
Matthew Jackson 2287bb4449 labels/pes/disc: overflow + robustness fixes
class_reader: checked pc + size add (guards 32-bit overflow) and remove
the dead BadInstruction variant.

criterion: dedup duplicate stream numbers instead of emitting a number
twice. deluxe: continue past a bad studio attribute instead of aborting
the whole parse. bdmt: treat mixed-content XML (embedded markup) as XML
so it is dropped, without over-rejecting a bare '<' in prose.

pes: grow the frame buffer incrementally instead of an eager
MAX_FRAME_SIZE allocation. extract: give the case-probe a unique random
suffix so concurrent probes cannot collide (TOCTOU-safe).
2026-09-15 14:40:47 -07:00
Matthew Jackson 013cc3ce8d net/error/hex: NAT64 SSRF arm, Dir Display arms, reject leading +
network: add the NAT64 (64:ff9b::/96) arm to the IP classifier -
extract the embedded IPv4 from the last 32 bits and re-check it, matching
the keysources canonical copy, so a NAT64-wrapped private/special
address is blocked like the bare IPv4.

error: add Display arms for DirInsufficientSpace (E9027), DirNameCollision
(E9028), DirWriteFailed (E9029) so their fields render (codes-only, like
the sibling Dir variants).

hex: reject a leading + in the hex int parser.
2026-09-15 14:40:40 -07:00
Matthew Jackson ab2416d07a aacs: propagate MKB walk errors, add bounds/truncation guards, fix FMTS carry
inf: read_mkb_from_drive used .is_ok() after the first pack, silently
truncating the MKB on a mid-walk transport error; propagate the error
instead. Add checked offset guards (94/117) and reject a truncated title
table (161) like the key list already does.

segment: the FMTS key-range carry assumed 2048-aligned segment starts;
use the floor-difference of the actual LBAs so a non-2048-aligned
segment carries correctly.

Annotate css::keyless::recover_title_key as a test-only helper and
correct the clpi_audit dedup-by-PID doc.
2026-09-15 14:40:33 -07:00
Matthew Jackson b735fb3730 mux/codec: PTS carry, buffer cap, EOS core drop, PPS-by-id, dead code
lpcm: carry last PTS + DTS fallback (add last_pts_ns) and propagate
pes.discontinuity instead of hardcoding false, matching adts/flac/
mpegaudio.

pesbuf: add an internal MAX_BUFFERED_BYTES cap (defense-in-depth like
reorder.rs's MAX_GOP) so a misbehaving caller can't grow it unbounded.

dts: drop a truncated trailing core at end-of-stream instead of emitting
it, while keeping genuine trailing extensions.

hevc: resolve num_extra_slice_header_bits from the PPS the slice
references (slice_pic_parameter_set_id), not the last-active PPS.

crc: remove the dead, misnamed crc8_atm (impl was CRC-8/SMBUS, no
callers). Add the missing drop-gate mismatch test; fix the startcode
read_bits contract doc and the h264 SEI comment.
2026-09-15 14:40:26 -07:00
Matthew Jackson f1b7a73b93 mux: harden MKV child loops, read real mp4 sample rate, fix pre-video PTS origin
mkvstream: add the consumed > remaining => MkvSourceInvalid guard (as
BLOCK_GROUP already had) to the INFO/TRACKS/TrackEntry/Video/Audio child
loops, and route the DURATION/SAMPLING_FREQUENCY float reads through the
checked_size cap instead of truncating cs as usize.

mp4/read: audio sample_rate was taken from the mdhd timescale, ignoring
the AudioSampleEntry 16.16 samplerate at the stsd body; read the real
field and fall back to timescale only when it is 0. Drop the unreachable
MAX_TRACKS break (the box scan is already capped) for a debug_assert.

tsmux + m2ts_mux: PTS origin was seeded on video only, so audio
preceding the first video (or an audio-only stream) rebased onto its own
PTS and lost A/V offset; seed the origin from the first frame of any
kind.
2026-09-15 14:40:18 -07:00
Matthew Jackson fbe91895c4 mux: fix filtered-export drop gate, log skipped key-map, restore json fields
demux_sink: dropped_total() folded drops across ALL tracks while
frames_mapped now counts only persisted frames, so a filtered
(sub://audio://) multi-clip export could false-trip SeamPlanDroppedMost /
SinkWroteNothing. Compare drops and mapped frames over the same
persisted-track set via TimelineContinuity::dropped_for; update the
frames_mapped doc to the persisted-only semantics.

resolve: resolve_mux_key_map(...).ok() swallowed the error silently
(TrueHD channel correction skipped with no trace); log it like the
sibling reopen() fix before skipping.

meta_sink: TitleProfile::from_title dropped json:// stream fields
(pid, color/aspect/dimensions/interlaced/mvc, audio secondary/
sample_rate/purpose, subtitle pid/descriptive_service); restore them at
the json document level.
2026-09-15 14:40:09 -07:00
Matthew Jackson 679894e5ea aacs/disc/drive: fix bus-key gate credit, shift guard, tray-unlock log
disc/encrypt: on the disc-keyed cert route, drive_unlocked was derived
from is_drive_unlocker(matched), which is provably always false -
run_bus only ever dispatches the disc unlockers (AACS/DVD), never a
firmware/drive unlocker. Hardcode false with an honest comment; the
genuine drive-unlock case (drive_unlocked, no read_data_key) is the
OEM-VID short-circuit and the bus-key gate already credits it. Gate the
now test-only is_drive_unlocker behind cfg(test) and add a regression
test through resolve_vid_only.

aacs/derive: resolve_dk_node looped 0..u_mask_shift shifting 1u32 << b,
which panics/wraps when the disc/keydb-controlled u_mask_shift is >= 32.
Cap the search at the 32 real bit positions.

drive: unlock_tray dropped its SCSI result with let _; warn! on failure
so a stuck medium-removal PREVENT lock is visible. Fix the stale
probe_disc doc (now a no-op; calibration moved into the unlocker).
2026-09-15 14:39:53 -07:00
Matthew Jackson b7914c5995 decrypt: honor with_content_ranges in the mapped AACS read path
DecryptingSectorSource::with_content_ranges was a silent no-op: the
proactive AACS key_map path early-returned before consulting the content
extents, and decrypt_span ignored its _content argument. A whole-disc
reader (sweep/patch) that installed both a key map and content ranges
therefore decrypted/verified clear UDF/BDMV-nav units, and its
misaligned filesystem reads tripped the AACS unit-alignment gate.

Thread the content extents through decrypt_sectors_mapped_in_content ->
decrypt_span -> apply_aacs_map, skipping any unit whose LBA falls outside
every content range (never decrypted, verified, or counted as loss), and
gate the alignment check on span_touches_content so out-of-content
filesystem reads pass through untouched.

Also drop the redundant is_multiple_of clause in the prefetched
unit-tail guard (remaining > 0 is already guaranteed).
2026-09-15 14:39:31 -07:00
Matthew Jackson 747aeba4a4 error: drop premature E7031 KeyServiceConfig variant
Reverts the cross-repo E7031 addition: it forced enumerating the code across
freemkv test_support + freemkv-i18n's 29 locales for a LOW config-fault nicety.
keysources config faults keep borrowing the transient key-service code until a
coordinated i18n release; decrypt_bus-pub and the SSRF-parity additions stay.
2026-09-15 13:46:06 -07:00
Matthew Jackson 034a8b145b fix: cross-repo integration — widen decrypt_bus, SSRF parity, KeyServiceConfig
- aacs/content: widen decrypt_bus to pub so freemkv-drivetest can invert
  AACS 2.0 bus encryption out-of-tree (was pub(crate), broke that build).
- mux/network: port the 6to4 (2002::/16) and Teredo (2001:0000::/32)
  tunnel-decode arms and the 198.18.0.0/15 + 192.0.0.0/24 blocks from the
  canonical keysources SSRF classifier so both cover the same set.
- error: add distinct KeyServiceConfig variant (E_KEY_SERVICE_CONFIG 7031)
  for the standing key-service misconfiguration fault, separate from the
  transient KeyServiceUnavailable; keysources' online.rs adopts it later.
2026-09-15 13:36:47 -07:00
Matthew Jackson 6720ccdf8b style: cargo fmt the audit changes 2026-09-15 11:02:53 -07:00
Matthew Jackson 24ab408559 test/labels: cover dirimage nesting cap, long_ad dir, paramount feature hint
- dirimage: add a MAX_DEPTH nesting-cap test that walks a real
  deeper-than-cap directory chain and expects DirImageTooLarge.
- udf: add a long_ad (ICB flags = 1) directory read test — the LBA sits
  at AD offset 4, not 12 like extended_ad.
- labels/paramount: derive the feature hint's id and filename from one
  parsed number (canonical 5-digit NNNNN.mpls) so they can't disagree,
  the same fix already applied to fox, and add a hint-parse test.
2026-09-15 11:02:04 -07:00
Matthew Jackson 6e427e2eac test(scsi/linux): cover the fd-reopen path helpers
Add unit tests for to_c_path (single trailing NUL, path bytes preserved
for libc::open) and resolve_to_sg (sg passthrough plus the no-filename
and non-sr/sg fallbacks that need no sysfs) — the helpers the open /
drive_has_disc / recovery reopen path relies on. Verified compiling for
x86_64-unknown-linux-gnu (this module is Linux-only).
2026-09-15 10:58:38 -07:00
Matthew Jackson e130528fea test: cover drive tray/speed rejection, bdnav Call ops, mkv interlace patch
- drive: SET CD SPEED and PREVENT/ALLOW MEDIUM REMOVAL are best-effort; a
  drive rejection must warn, not fail the rip. New test drives a failing
  transport through set_speed/lock_tray/unlock_tray.
- bdnav: CallObject/CallTitle follow the same play path as JumpObject/
  JumpTitle for feature resolution; add tests for both.
- mkv: FlagInterlaced is corrected at finish() to the whole-stream scan
  majority; add a test that a progressive leader on an interlaced feature
  is patched from progressive to interlaced.
2026-09-15 10:56:26 -07:00
Matthew Jackson 0de2e598ef dvdnav(test): cover Vm set-ops (incl divide/mod by zero) and First-Play SetSystem
Add a direct Vm::set test exercising mov/add/sub/mul/div/mod/and/or/xor
and the divide- and modulo-by-zero guards (register left intact, no
panic) plus an unmodelled-op no-op, and a First-Play test that a
SetSystem instruction keeps resolution running so a following JumpTT
still resolves.
2026-09-15 10:51:23 -07:00
Matthew Jackson d0a5a90261 cleanup: remove dead MOBJ cmd cap, add early-break and doc clarifications
- bdnav/mobj: drop the unreachable MAX_CMDS cap (num_cmds is a u16 and the
  span is already bounded against the input) and its flagged NOTE.
- drive/wait_ready: break out immediately on a dead bus (transport
  failure) instead of polling a phantom for the full ~30 s; add a test.
- css/is_scrambled_pack: document that the 0x11 stream_id offset assumes
  zero pack stuffing (true for DVD-Video VOB packs) and why a stuffed pack
  degrades safely.
- scsi/macos_shim: document why releasing g_handle_lock before da_hold is
  TOCTOU-safe (da_hold re-validates the handle under the lock).
2026-09-15 10:49:16 -07:00
Matthew Jackson be296b3901 mux(timeline): tag provisionals with a monotonic epoch counter
passive_offset tagged a provisional offset with epoch_offsets.len() as
the epoch identity, but that length is capped at MAX_EPOCHS. Past the cap
the ring's remove+push leaves len() constant, so a provisional captured
then compared equal to the current identity forever and never retired,
drifting that track off every other one for the rest of the title. Add a
monotonic epoch_seq bumped once per open_epoch and tag provisionals with
it. Add a past-the-cap retirement test.
2026-09-15 10:45:35 -07:00
Matthew Jackson b30b06e04e mpls/ifo: fix STN gate off-by-one and pin the PGC palette chroma order
mpls: the STN table gate required item.len() > STN_OFFSET + 16, one byte
more than the 16-byte header actually spans, skipping a first play item
whose STN header ends exactly at the item boundary. Use >=, with a
boundary test.

ifo: the subtitle palette layout [padding, Y, Cr, Cb] (Cr BEFORE Cb) was
correct but under-documented; harden the comment against a future
byte-swap and add a test that cross-checks the parse against the DvdSub
ycbcr_to_rgb consumer so the two read Cr/Cb the same way.
2026-09-15 10:42:37 -07:00
Matthew Jackson e6d86b3582 labels: fix fox hint field disagreement and deluxe bare studio anchor
fox: playlist_id and filename were derived independently from the raw id,
so a 5-digit id above u16::MAX left playlist_id None while the filename
stayed Some — a half-hint naming different things. Derive both from one
parsed number via a feature_hint() helper with the canonical NNNNN.mpls
filename, and test that the fields agree.

deluxe: parse_studio_attr anchored on a bare find("studio"), which also
matched substudio=/studioId= and the word 'studio' inside another
attribute's value, then read a later attribute's quotes as the studio.
Require a word boundary before it and '=' after, and add anchoring tests.
2026-09-15 10:38:40 -07:00
Matthew Jackson 2a701a217a dirimage: refuse a read that begins past the image's capacity
read_sectors fabricated zeros and reported success for any LBA, including
one at or beyond the image's own capacity — an oversized/crafted offset
that a real file/ISO source rejects. Refuse a read whose START is past
capacity (as read_exact does past EOF) while still zero-padding a batch
that starts in range and overshoots the end, which the mux's final batch
and the FS scan's fixed-LBA probes rely on. Split the read-range test to
pin both behaviours.
2026-09-15 10:33:26 -07:00
Matthew Jackson 90e8e99152 mux(resolve): surface the reopen error instead of swallowing it
The TrueHD channel-correction probe's reopen closure returned Option<S>
via .ok(), collapsing a real io::Error into an indistinguishable None.
Make reopen return io::Result<S> and log the actual error on the failure
arm so a failed re-open is diagnosable, not silent.
2026-09-15 10:33:26 -07:00
Matthew Jackson 1edacf294f unlock: derive unlocker names/classification from the real unlockers
unlocker_names() was a hand-maintained list that could drift from the
unlockers that actually dispatch, and the cert route hardcoded
drive_unlocked:false. Factor the firmware and disc unlocker sets into
single constructors, derive unlocker_names() and a new is_drive_unlocker()
from their real .name(), and set HandshakeResult.drive_unlocked from
whichever unlocker claimed the drive so the bus-key gate credits a drive
unlock even on the cert dispatch path. Add derivation and classification
tests.
2026-09-15 10:27:09 -07:00
Matthew Jackson 7350577d91 disc(extract): fold host-path collisions by the target volume's case rule
The extract collision check always folded names to lowercase, so two
disc files differing only by case (Movie/movie) aborted the extract even
on a case-sensitive volume where they are distinct, coexisting files.
Probe the real target directory's case-sensitivity and fold only when the
host would (APFS/NTFS), defaulting to case-insensitive when the probe
can't run. Update the case-collision test to track the real volume, and
drop a dead duplicate pack-start write in the CSS cross-contamination test.
2026-09-15 10:24:37 -07:00
Matthew Jackson b985618fb5 mux(dts): continue the timeline for a PTS-less PES instead of resetting
When a PTS-less PES arrived with pending_pts invalidated (e.g. after a
forced flush), the fallback fell straight to 0, snapping the timeline
backwards. Add continuation_base_ns() to reach for the projected next-AU
start, then the last PES front, before conceding 0, and stop stashing a
drained buffer's stale front mark as pending_pts. Add a fallback test.
2026-09-15 10:22:04 -07:00
Matthew Jackson d3c4ce4a60 udf: read every FID extent of a directory, honouring the AD chain
read_directory read only the FIRST allocation descriptor of an out-of-
line directory, so a FID list spanning multiple extents was truncated at
the first boundary, and a type-3 continuation descriptor was mis-read as
FID data. Walk the whole descriptor list (types 0/1/2 as allocated FID
extents with the 2-bit type masked out of the length; type 3 followed as
a continuation block, bounded by MAX_AD_BLOCKS), gathering every extent
into one buffer while tracking the declared FID-list length separately
from the sector-rounded buffer. Bound the FID walk and the name read by
that declared length, and read a FID whose 38-byte header ends exactly at
the boundary (<=, not <) while still rejecting one whose name spills past
it. Add a multi-extent directory test.
2026-09-15 10:18:51 -07:00
Matthew Jackson f4a48480df mux(h264): gate open-GOP promotion on all-intra slices and base view
The open-GOP keyframe promotion trusted only the first slice's coding
type and wasn't gated on the base view. A picture whose first slice is I
but whose later slices are P/B is not a clean random-access point, and an
intra MVC dependent view is never an independent anchor. Track every VCL
slice's coding type (all_vcl_intra) and require a real VCL slice, then
promote only when all slices are intra AND !mvc. Add mixed-slice and MVC
non-promotion tests.
2026-09-15 10:06:20 -07:00
Matthew Jackson aad821c8a2 dirimage: count FID-list sectors in the metadata ceiling
The MAX_META_BYTES guard multiplied (dir_count + file_count) by the
sector size, counting one File Entry per node but omitting each
directory's FID-list data sectors. A wide, shallow tree's directory data
can dwarf the per-node entries, so an oversized folder could slip past
the guard and be materialized in full. Gate on metadata_block_count()
instead, which already sums File Entries and FID-list sectors, and add a
test proving the FID-list sectors are counted.
2026-09-15 10:04:12 -07:00
Matthew Jackson 959bba919d mux(demux): count only persisted frames toward frames_mapped
frames_mapped incremented for every mapped frame, including kind-filtered
ones that write to no track file (e.g. video during an audio:// export).
That inflated the denominator so a seam plan dropping every persisted
frame could ship zero-byte files at exit 0. Move the increment inside the
write block so it counts persisted frames only, and add a test where more
frames are seam-dropped than persisted -> SeamPlanDroppedMost fires.
2026-09-15 10:02:53 -07:00
Matthew Jackson ff3e46a560 error: give ImageTruncated a Display arm carrying have/want
ImageTruncated fell through to the bare-code catch-all, dropping the
have/want byte lengths a truncated-resume report needs. Add an explicit
arm emitting them in the identifier-free E{code} have/want form.
2026-09-15 10:01:14 -07:00
Matthew Jackson 14f97aacd6 aacs: fall through a primary DiscRead to the DUPLICATE copy
read_first propagated every non-UdfNotFound error immediately, so a
DiscRead on the primary Unit_Key_RO.inf aborted instead of trying the
/AACS/DUPLICATE/ backup that AACS ships for exactly this case. Treat a
DiscRead as retriable like an absent candidate: continue to the next
path, remembering the error, and only propagate it once every candidate
has been tried (still more informative than a bare AacsNoKeys). Add
fall-through and propagate-after-exhaustion tests.
2026-09-15 10:01:14 -07:00
Matthew Jackson 5c911d1573 mp4: reject hostile 64-bit largesize in find_boxes_capped
find_boxes_capped used plain `pos + box_size` on an attacker-controlled
64-bit largesize cast to usize. A value near usize::MAX at a nonzero pos
wraps past the length guard, producing a release panic on the payload
slice or an infinite loop on the advance. Mirror read_moov: compute the
end once with checked_add, reject on overflow or overrun, and reuse it
for the guard, slice, and advance. Add the hostile-largesize test.
2026-09-15 09:59:35 -07:00
Matthew Jackson cdc1d17993 v1.7.1: bump version (freemkv-unlock git-pinned for the tag) v1.7.1 v1.7.1-rc4 2026-09-14 03:34:47 +00:00
Matthew Jackson 7b4a0b31d5 docs(changelog): 1.7.1 release notes (LibreDrive unlock fix) v1.7.1-rc3 2026-09-13 20:02:14 -07:00
Matthew Jackson a85764da38 ci: bump actions/checkout v4->v7 in identity-guard (dependabot) v1.7.1-rc2 2026-09-14 01:30:58 +00:00
Matthew Jackson 5af336776c chore: bump version to 1.7.1 (unified release lockstep) 2026-09-14 01:22:18 +00:00
Matthew Jackson 86c2c3c501 Add Sponsor badge to README 2026-09-06 23:29:33 +00:00
Matthew Jackson c280478ef9 Add FUNDING.yml 2026-09-06 23:25:41 +00:00
Matthew Jackson e7bcef0c45 docs(changelog): record the scsi-only Windows build fix in 1.7.0
The scsi::windows::list_drives fix (build under default-features=false,
features=["scsi"]) ships in 1.7.0; add it to the changelog's Fixed list.
v1.7.0-rc9
2026-09-05 18:14:36 -07:00
Matthew Jackson 695569d29a scsi(windows): make list_drives self-contained (build under scsi-only)
scsi::windows::list_drives delegated to crate::drive::windows::find_drives(),
but the drive and identity modules are #[cfg(feature = "rip")]. A scsi-only
build (default-features = false, features = ["scsi"]) — which is exactly how
freemkv-firmware consumes libfreemkv — therefore failed to compile: 'could not
find drive in the crate root'. The --all-features CI never saw it (rip is on).

Enumerate directly at the SCSI layer via scsi::open + scsi::inquiry, mirroring
the Linux and macOS backends, which already build their DriveInfo without the
rip-gated modules. The rip-side drive::windows::find_drives() is untouched.

Verified: cargo check --target x86_64-pc-windows-msvc passes both scsi-only and
with default features.
v1.7.0-rc8
2026-09-05 15:54:45 -07:00
Matthew Jackson 77e4bca234 docs: reconcile unlock-bridge with the current single-unlock dispatch v1.7.0-rc7 2026-09-05 12:35:03 -07:00
Matthew Jackson ae8c20f1ca scsi/macos: assert the presence-probe leaves the lock unchanged, not the global bit 2026-09-05 12:35:03 -07:00
Matthew Jackson 086547b78b scsi/linux: cap detached fd-recovery threads on a wedged bridge 2026-09-05 12:35:03 -07:00