mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8870efa77b | ||
|
|
a693ef965b | ||
|
|
0596307c19 | ||
|
|
18c6365e08 | ||
|
|
981f30b1b0 | ||
|
|
04086d73a0 | ||
|
|
20e404e59a | ||
|
|
1bea8eb650 | ||
|
|
0165f18fad | ||
|
|
ca3914d537 |
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## 0.11.9 (2026-04-20)
|
||||
|
||||
### Fast verify reads
|
||||
- **read_sectors_fast()** — single-attempt 5s timeout SCSI read for verify. No recovery loop. Bad sectors detected in seconds instead of 10+ minutes.
|
||||
- **SectorReader trait** — added read_sectors_fast() with default fallback to read_sectors().
|
||||
|
||||
## 0.11.8 (2026-04-20)
|
||||
|
||||
### Disc verify
|
||||
- **verify::verify_title()** — sector-by-sector health check. Classifies sectors as Good/Slow/Recovered/Bad. Progress callback, chapter mapping, sector ranges.
|
||||
|
||||
## 0.11.7 (2026-04-19)
|
||||
|
||||
### TrueHD parser rewrite
|
||||
- **12-bit length mask** — access unit length is lower 12 bits of first 2 bytes, not full 16. Upper 4 bits are parity nibble. Wrong mask caused misaligned frame splits.
|
||||
- **AC-3 frame skipping** — BD-TS TrueHD PES contains interleaved AC-3 frames (same PID). Parser now detects AC-3 sync word (0x0B77) and skips those frames.
|
||||
- **Cross-PES buffering** — access units that span PES packet boundaries are correctly reassembled.
|
||||
- **Per-unit timestamps** — each access unit gets incrementing PTS (1/1200th second apart) instead of all units in one PES sharing the same timestamp.
|
||||
- **Major sync detection** — keyframe flag set when access unit contains MLP major sync (0xF8726FBA).
|
||||
- Result: zero TrueHD decode errors on UHD and BD (was ~19 per 30 seconds).
|
||||
|
||||
## 0.11.6 (2026-04-18)
|
||||
|
||||
### TrueHD fix (incomplete)
|
||||
- Initial attempt at TrueHD header stripping — wrong approach, superseded by 0.11.7.
|
||||
|
||||
## 0.11.5 (2026-04-18)
|
||||
|
||||
### MKV container fixes — Jellyfin/player compatibility
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "libfreemkv"
|
||||
version = "0.11.5"
|
||||
version = "0.11.9"
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
@@ -18,7 +18,7 @@ Part of the [freemkv](https://github.com/freemkv) project.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
libfreemkv = "0.10"
|
||||
libfreemkv = "0.11"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -134,7 +134,7 @@ All errors are structured with numeric codes. No user-facing English text — ap
|
||||
|
||||
## Contributing
|
||||
|
||||
Run `freemkv drive-info --share` with the [freemkv CLI](https://github.com/freemkv/freemkv) to contribute your drive's profile.
|
||||
Run `freemkv info disc:// --share` with the [freemkv CLI](https://github.com/freemkv/freemkv) to contribute your drive's profile.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1108,6 +1108,7 @@ impl Disc {
|
||||
// 4. Metadata + labels
|
||||
let meta_title = Self::read_meta_title(reader, &udf_fs);
|
||||
crate::labels::apply(reader, &udf_fs, &mut titles);
|
||||
crate::labels::fill_defaults(&mut titles);
|
||||
|
||||
// 5. Derive format, layers, region
|
||||
let format = Self::detect_format(&titles);
|
||||
|
||||
@@ -542,6 +542,32 @@ impl Drive {
|
||||
Err(Error::DiscRead { sector: lba as u64 })
|
||||
}
|
||||
|
||||
/// Fast single-attempt read for verification — no recovery, short timeout.
|
||||
/// Returns Ok on success, Err on any failure. Does not retry.
|
||||
pub fn read_fast(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
let cdb = [
|
||||
crate::scsi::SCSI_READ_10,
|
||||
0x00,
|
||||
(lba >> 24) as u8,
|
||||
(lba >> 16) as u8,
|
||||
(lba >> 8) as u8,
|
||||
lba as u8,
|
||||
0x00,
|
||||
(count >> 8) as u8,
|
||||
count as u8,
|
||||
0x00,
|
||||
];
|
||||
match self.scsi.as_mut().execute(
|
||||
&cdb,
|
||||
crate::scsi::DataDirection::FromDevice,
|
||||
buf,
|
||||
5_000, // 5 second timeout — enough for healthy reads, fast fail for bad sectors
|
||||
) {
|
||||
Ok(result) => Ok(result.bytes_transferred),
|
||||
Err(_) => Err(Error::DiscRead { sector: lba as u64 }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the disc capacity in sectors (2048 bytes each).
|
||||
pub fn read_capacity(&mut self) -> Result<u32> {
|
||||
let cdb = [
|
||||
@@ -643,6 +669,10 @@ impl SectorReader for Drive {
|
||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
self.read(lba, count, buf)
|
||||
}
|
||||
|
||||
fn read_sectors_fast(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
self.read_fast(lba, count, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all optical drives connected to this system.
|
||||
|
||||
@@ -138,6 +138,127 @@ pub fn apply(reader: &mut dyn SectorReader, udf: &UdfFs, titles: &mut [DiscTitle
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill in default labels for any streams that don't have one.
|
||||
/// Runs after BD-J label extraction — fills gaps with codec + channel descriptions.
|
||||
/// This is the central place for all fallback label generation.
|
||||
pub fn fill_defaults(titles: &mut [crate::disc::DiscTitle]) {
|
||||
use crate::disc::{Codec, HdrFormat, Stream};
|
||||
|
||||
for title in titles.iter_mut() {
|
||||
for stream in &mut title.streams {
|
||||
match stream {
|
||||
Stream::Audio(a) if a.label.is_empty() => {
|
||||
a.label = generate_audio_label(&a.codec, &a.channels, a.secondary);
|
||||
}
|
||||
Stream::Video(v) if v.label.is_empty() => {
|
||||
v.label = generate_video_label(&v.codec, v.resolution.pixels(), &v.hdr, v.secondary);
|
||||
}
|
||||
Stream::Subtitle(s) if s.forced => {
|
||||
// Ensure forced subs are labeled even if BD-J didn't set a name
|
||||
// (subtitle labels are generally not set — this just marks forced)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_video_label(
|
||||
codec: &crate::disc::Codec,
|
||||
pixels: (u32, u32),
|
||||
hdr: &crate::disc::HdrFormat,
|
||||
secondary: bool,
|
||||
) -> String {
|
||||
use crate::disc::HdrFormat;
|
||||
|
||||
if secondary {
|
||||
return match hdr {
|
||||
HdrFormat::DolbyVision => "Dolby Vision EL".to_string(),
|
||||
_ => "Secondary Video".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let mut parts = Vec::new();
|
||||
|
||||
// Codec
|
||||
parts.push(codec.name().to_string());
|
||||
|
||||
// Resolution
|
||||
let (w, h) = pixels;
|
||||
let res = if w >= 7680 {
|
||||
"8K"
|
||||
} else if w >= 3840 {
|
||||
"4K"
|
||||
} else if w >= 1920 {
|
||||
"1080p"
|
||||
} else if w >= 1280 {
|
||||
"720p"
|
||||
} else if h >= 576 {
|
||||
"576p"
|
||||
} else if h >= 480 {
|
||||
"480p"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if !res.is_empty() {
|
||||
parts.push(res.into());
|
||||
}
|
||||
|
||||
// HDR
|
||||
match hdr {
|
||||
HdrFormat::Sdr => {}
|
||||
_ => parts.push(hdr.name().to_string()),
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
fn generate_audio_label(
|
||||
codec: &crate::disc::Codec,
|
||||
channels: &crate::disc::AudioChannels,
|
||||
secondary: bool,
|
||||
) -> String {
|
||||
use crate::disc::{AudioChannels, Codec};
|
||||
|
||||
// Full marketing names for disc audio codecs
|
||||
let codec_name = match codec {
|
||||
Codec::TrueHd => "Dolby TrueHD",
|
||||
Codec::Ac3 => "Dolby Digital",
|
||||
Codec::Ac3Plus => "Dolby Digital Plus",
|
||||
Codec::DtsHdMa => "DTS-HD Master Audio",
|
||||
Codec::DtsHdHr => "DTS-HD High Resolution",
|
||||
Codec::Dts => "DTS",
|
||||
Codec::Lpcm => "LPCM",
|
||||
Codec::Aac => "AAC",
|
||||
Codec::Mp2 => "MPEG Audio",
|
||||
Codec::Mp3 => "MP3",
|
||||
Codec::Flac => "FLAC",
|
||||
Codec::Opus => "Opus",
|
||||
_ => return String::new(),
|
||||
};
|
||||
|
||||
// Channel layout
|
||||
let channel_str = match channels {
|
||||
AudioChannels::Mono => "1.0",
|
||||
AudioChannels::Stereo => "2.0",
|
||||
AudioChannels::Stereo21 => "2.1",
|
||||
AudioChannels::Quad => "4.0",
|
||||
AudioChannels::Surround50 => "5.0",
|
||||
AudioChannels::Surround51 => "5.1",
|
||||
AudioChannels::Surround61 => "6.1",
|
||||
AudioChannels::Surround71 => "7.1",
|
||||
AudioChannels::Unknown => "",
|
||||
};
|
||||
|
||||
let suffix = if secondary { " (Secondary)" } else { "" };
|
||||
|
||||
if channel_str.is_empty() {
|
||||
format!("{}{}", codec_name, suffix)
|
||||
} else {
|
||||
format!("{} {}{}", codec_name, channel_str, suffix)
|
||||
}
|
||||
}
|
||||
|
||||
fn extract(reader: &mut dyn SectorReader, udf: &UdfFs) -> Vec<StreamLabel> {
|
||||
for (_name, detect, parse) in PARSERS {
|
||||
if detect(udf) {
|
||||
|
||||
@@ -90,6 +90,7 @@ pub(crate) mod platform;
|
||||
pub mod profile;
|
||||
pub mod scsi;
|
||||
pub mod sector;
|
||||
pub mod verify;
|
||||
pub(crate) mod speed;
|
||||
pub(crate) mod udf;
|
||||
|
||||
|
||||
+124
-22
@@ -1,17 +1,24 @@
|
||||
//! Dolby TrueHD / Atmos elementary stream parser.
|
||||
//!
|
||||
//! TrueHD access units are 2560-byte fixed-size units (40 per major sync).
|
||||
//! Each unit starts with a 4-byte header: [length_hi, length_lo, timestamp_hi, timestamp_lo].
|
||||
//! Major sync: 0xF8726FBA appears within a unit.
|
||||
//! Buffers across PES boundaries for complete unit delivery.
|
||||
//! BD-TS TrueHD PES packets contain interleaved AC-3 + TrueHD access units.
|
||||
//! Access units span PES boundaries — must buffer and reassemble.
|
||||
//!
|
||||
//! TrueHD access unit header (4 bytes):
|
||||
//! [0..1] upper 4 bits = parity, lower 12 bits = length in 2-byte words
|
||||
//! [2..3] timing value
|
||||
//! [4..] substream data (major sync 0xF8726FBA may appear at offset 4)
|
||||
//!
|
||||
//! AC-3 frames (interleaved, same PID): start with sync word 0x0B77.
|
||||
//! We skip AC-3 frames and only emit TrueHD access units.
|
||||
|
||||
use super::{pts_to_ns, CodecParser, Frame, PesPacket};
|
||||
|
||||
/// TrueHD access unit size (fixed).
|
||||
const TRUEHD_UNIT_SIZE: usize = 2560;
|
||||
/// Duration of one TrueHD access unit in nanoseconds (1/1200 second).
|
||||
const AU_DURATION_NS: i64 = 833_333;
|
||||
|
||||
pub struct TrueHdParser {
|
||||
buf: Vec<u8>,
|
||||
next_pts_ns: i64,
|
||||
}
|
||||
|
||||
impl Default for TrueHdParser {
|
||||
@@ -23,9 +30,61 @@ impl Default for TrueHdParser {
|
||||
impl TrueHdParser {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buf: Vec::with_capacity(TRUEHD_UNIT_SIZE * 4),
|
||||
buf: Vec::with_capacity(32768),
|
||||
next_pts_ns: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip an AC-3 frame starting at the current buffer position.
|
||||
/// Returns number of bytes consumed, or 0 if not enough data.
|
||||
fn skip_ac3_frame(&self) -> usize {
|
||||
if self.buf.len() < 6 {
|
||||
return 0;
|
||||
}
|
||||
// AC-3 frame size from frmsizcod + fscod
|
||||
// Byte 4: [fscod:2][frmsizecod:6]
|
||||
let fscod = (self.buf[4] >> 6) & 0x03;
|
||||
let frmsizecod = (self.buf[4] & 0x3F) as usize;
|
||||
// Frame size in 16-bit words per fscod (simplified table for common rates)
|
||||
let frame_words = match fscod {
|
||||
0 => {
|
||||
// 48 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
64, 64, 80, 80, 96, 96, 112, 112, 128, 128,
|
||||
160, 160, 192, 192, 224, 224, 256, 256, 320, 320,
|
||||
384, 384, 448, 448, 512, 512, 640, 640, 768, 768,
|
||||
896, 896, 1024, 1024, 1152, 1152, 1280, 1280,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
1 => {
|
||||
// 44.1 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
69, 70, 87, 88, 104, 105, 121, 122, 139, 140,
|
||||
174, 175, 208, 209, 243, 244, 278, 279, 348, 349,
|
||||
417, 418, 487, 488, 557, 558, 696, 697, 835, 836,
|
||||
975, 976, 1114, 1115, 1253, 1254, 1393, 1394,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
2 => {
|
||||
// 32 kHz
|
||||
static SIZES: [usize; 38] = [
|
||||
96, 96, 120, 120, 144, 144, 168, 168, 192, 192,
|
||||
240, 240, 288, 288, 336, 336, 384, 384, 480, 480,
|
||||
576, 576, 672, 672, 768, 768, 960, 960, 1152, 1152,
|
||||
1344, 1344, 1536, 1536, 1728, 1728, 1920, 1920,
|
||||
];
|
||||
SIZES.get(frmsizecod).copied().unwrap_or(0)
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
let frame_bytes = frame_words * 2;
|
||||
if frame_bytes == 0 || self.buf.len() < frame_bytes {
|
||||
return 0;
|
||||
}
|
||||
frame_bytes
|
||||
}
|
||||
}
|
||||
|
||||
impl CodecParser for TrueHdParser {
|
||||
@@ -33,39 +92,59 @@ impl CodecParser for TrueHdParser {
|
||||
if pes.data.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let pts_ns = pes.pts.map(pts_to_ns).unwrap_or(0);
|
||||
|
||||
if let Some(pts) = pes.pts {
|
||||
self.next_pts_ns = pts_to_ns(pts);
|
||||
}
|
||||
|
||||
self.buf.extend_from_slice(&pes.data);
|
||||
|
||||
let mut frames = Vec::new();
|
||||
|
||||
// TrueHD units are variable-length but each starts with a 2-byte
|
||||
// big-endian length field (in 16-bit words, includes the 4-byte header).
|
||||
// Extract complete units from the buffer.
|
||||
while self.buf.len() >= 4 {
|
||||
let unit_words = ((self.buf[0] as usize) << 8) | self.buf[1] as usize;
|
||||
loop {
|
||||
if self.buf.len() < 4 {
|
||||
break;
|
||||
}
|
||||
|
||||
// AC-3 frame (interleaved): starts with sync word 0x0B77
|
||||
if self.buf[0] == 0x0B && self.buf[1] == 0x77 {
|
||||
let skip = self.skip_ac3_frame();
|
||||
if skip == 0 {
|
||||
break; // incomplete AC-3 frame, wait for more data
|
||||
}
|
||||
self.buf.drain(..skip);
|
||||
continue;
|
||||
}
|
||||
|
||||
// TrueHD access unit: lower 12 bits of first 2 bytes = length in words
|
||||
let unit_words = (((self.buf[0] as usize) << 8) | self.buf[1] as usize) & 0xFFF;
|
||||
if unit_words == 0 {
|
||||
// Padding — skip 2 bytes
|
||||
self.buf.drain(..2);
|
||||
continue;
|
||||
}
|
||||
let unit_bytes = unit_words * 2;
|
||||
if unit_bytes > 65536 {
|
||||
// Invalid — skip 2 bytes to resync
|
||||
if unit_bytes > 32768 {
|
||||
// Likely misaligned — try to resync by scanning for AC-3 sync or
|
||||
// a valid TrueHD length
|
||||
self.buf.drain(..2);
|
||||
continue;
|
||||
}
|
||||
if self.buf.len() < unit_bytes {
|
||||
// Incomplete unit — wait for more data
|
||||
break;
|
||||
break; // incomplete access unit, wait for more data
|
||||
}
|
||||
|
||||
let is_major_sync = unit_bytes >= 8
|
||||
&& (u32::from_be_bytes([self.buf[4], self.buf[5], self.buf[6], self.buf[7]])
|
||||
& 0xFFFF_FFFE)
|
||||
== 0xF872_6FBA;
|
||||
|
||||
frames.push(Frame {
|
||||
pts_ns,
|
||||
keyframe: true,
|
||||
pts_ns: self.next_pts_ns,
|
||||
keyframe: is_major_sync,
|
||||
data: self.buf[..unit_bytes].to_vec(),
|
||||
});
|
||||
self.buf.drain(..unit_bytes);
|
||||
self.next_pts_ns += AU_DURATION_NS;
|
||||
}
|
||||
|
||||
frames
|
||||
@@ -93,11 +172,20 @@ mod tests {
|
||||
fn make_truehd_unit(size_bytes: usize) -> Vec<u8> {
|
||||
let words = size_bytes / 2;
|
||||
let mut data = vec![0u8; size_bytes];
|
||||
data[0] = (words >> 8) as u8;
|
||||
data[0] = ((words >> 8) & 0x0F) as u8;
|
||||
data[1] = (words & 0xFF) as u8;
|
||||
data
|
||||
}
|
||||
|
||||
fn make_ac3_frame() -> Vec<u8> {
|
||||
// Minimal AC-3 frame: sync 0x0B77, fscod=0 (48kHz), frmsizecod=0 (64 words = 128 bytes)
|
||||
let mut data = vec![0u8; 128];
|
||||
data[0] = 0x0B;
|
||||
data[1] = 0x77;
|
||||
data[4] = 0x00; // fscod=0, frmsizecod=0
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
@@ -131,7 +219,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multiple_units_in_one_pes() {
|
||||
fn parse_multiple_units_incrementing_pts() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
let mut data = make_truehd_unit(100);
|
||||
data.extend_from_slice(&make_truehd_unit(120));
|
||||
@@ -140,6 +228,20 @@ mod tests {
|
||||
assert_eq!(frames.len(), 2);
|
||||
assert_eq!(frames[0].data.len(), 100);
|
||||
assert_eq!(frames[1].data.len(), 120);
|
||||
assert_eq!(frames[1].pts_ns - frames[0].pts_ns, AU_DURATION_NS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skip_interleaved_ac3() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
let ac3 = make_ac3_frame();
|
||||
let truehd = make_truehd_unit(200);
|
||||
let mut data = ac3;
|
||||
data.extend_from_slice(&truehd);
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data.len(), 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-1
@@ -95,11 +95,14 @@ impl MkvTrack {
|
||||
};
|
||||
let sr = a.sample_rate.hz();
|
||||
let ch = a.channels.count();
|
||||
|
||||
let name = a.label.clone();
|
||||
|
||||
Self {
|
||||
track_type: ebml::TRACK_TYPE_AUDIO,
|
||||
codec_id,
|
||||
language: a.language.clone(),
|
||||
name: a.label.clone(),
|
||||
name,
|
||||
codec_private: None,
|
||||
is_default: !a.secondary,
|
||||
is_forced: false,
|
||||
|
||||
@@ -12,6 +12,12 @@ pub trait SectorReader: Send {
|
||||
/// `buf` must be at least `count * 2048` bytes.
|
||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
|
||||
|
||||
/// Fast single-attempt read — no recovery, short timeout.
|
||||
/// Default implementation calls read_sectors (ISO files don't need fast mode).
|
||||
fn read_sectors_fast(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
self.read_sectors(lba, count, buf)
|
||||
}
|
||||
|
||||
/// Total capacity in sectors, if known.
|
||||
fn capacity(&self) -> u32 {
|
||||
0
|
||||
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
//! Disc sector verification — read every sector and classify health.
|
||||
|
||||
use crate::disc::{Chapter, DiscTitle, Extent};
|
||||
use crate::sector::SectorReader;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Health status of a single sector read.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SectorStatus {
|
||||
/// Read succeeded on first attempt, under 500ms
|
||||
Good,
|
||||
/// Read succeeded but took longer than expected (>500ms per sector)
|
||||
Slow,
|
||||
/// Read failed initially but succeeded on retry
|
||||
Recovered,
|
||||
/// Read failed all attempts — unrecoverable
|
||||
Bad,
|
||||
}
|
||||
|
||||
/// A contiguous range of bad or slow sectors.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SectorRange {
|
||||
pub start_lba: u32,
|
||||
pub count: u32,
|
||||
pub status: SectorStatus,
|
||||
/// Byte offset from start of title
|
||||
pub byte_offset: u64,
|
||||
}
|
||||
|
||||
/// Result of a disc verification scan.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VerifyResult {
|
||||
pub total_sectors: u64,
|
||||
pub good: u64,
|
||||
pub slow: u64,
|
||||
pub recovered: u64,
|
||||
pub bad: u64,
|
||||
/// Contiguous ranges of non-good sectors
|
||||
pub ranges: Vec<SectorRange>,
|
||||
pub elapsed_secs: f64,
|
||||
}
|
||||
|
||||
impl VerifyResult {
|
||||
/// Percentage of sectors that are fully readable (good + slow + recovered).
|
||||
pub fn readable_pct(&self) -> f64 {
|
||||
if self.total_sectors == 0 {
|
||||
return 100.0;
|
||||
}
|
||||
(self.total_sectors - self.bad) as f64 / self.total_sectors as f64 * 100.0
|
||||
}
|
||||
|
||||
/// True if every sector read successfully.
|
||||
pub fn is_perfect(&self) -> bool {
|
||||
self.bad == 0 && self.recovered == 0 && self.slow == 0
|
||||
}
|
||||
|
||||
/// Map a bad sector range to a chapter timestamp.
|
||||
pub fn chapter_at_offset(chapters: &[Chapter], byte_offset: u64, duration_secs: f64, total_bytes: u64) -> Option<(usize, f64)> {
|
||||
if total_bytes == 0 || chapters.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let time_secs = byte_offset as f64 / total_bytes as f64 * duration_secs;
|
||||
let mut chapter_idx = 0;
|
||||
for (i, ch) in chapters.iter().enumerate() {
|
||||
if ch.time_secs <= time_secs {
|
||||
chapter_idx = i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some((chapter_idx + 1, time_secs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress callback: (sectors_done, total_sectors, current_status)
|
||||
pub type ProgressFn = Box<dyn FnMut(u64, u64, SectorStatus)>;
|
||||
|
||||
/// Verify all sectors in a title's extents.
|
||||
/// Reads in batches for speed, falls back to single-sector on failure.
|
||||
pub fn verify_title(
|
||||
reader: &mut dyn SectorReader,
|
||||
title: &DiscTitle,
|
||||
batch_sectors: u16,
|
||||
mut on_progress: Option<ProgressFn>,
|
||||
) -> VerifyResult {
|
||||
let start = Instant::now();
|
||||
let mut good: u64 = 0;
|
||||
let mut slow: u64 = 0;
|
||||
let mut recovered: u64 = 0;
|
||||
let mut bad: u64 = 0;
|
||||
let mut ranges: Vec<SectorRange> = Vec::new();
|
||||
let mut sectors_done: u64 = 0;
|
||||
let mut byte_offset: u64 = 0;
|
||||
|
||||
let total_sectors: u64 = title.extents.iter().map(|e| e.sector_count as u64).sum();
|
||||
let mut buf = vec![0u8; batch_sectors as usize * 2048];
|
||||
|
||||
for ext in &title.extents {
|
||||
let mut offset: u32 = 0;
|
||||
while offset < ext.sector_count {
|
||||
let remaining = ext.sector_count - offset;
|
||||
let count = remaining.min(batch_sectors as u32) as u16;
|
||||
let lba = ext.start_lba + offset;
|
||||
let bytes = count as usize * 2048;
|
||||
|
||||
let batch_start = Instant::now();
|
||||
let batch_ok = reader
|
||||
.read_sectors_fast(lba, count, &mut buf[..bytes])
|
||||
.is_ok();
|
||||
let batch_ms = batch_start.elapsed().as_millis();
|
||||
|
||||
if batch_ok {
|
||||
// Batch succeeded — classify by speed
|
||||
let status = if batch_ms > count as u128 * 500 {
|
||||
SectorStatus::Slow
|
||||
} else {
|
||||
SectorStatus::Good
|
||||
};
|
||||
|
||||
match status {
|
||||
SectorStatus::Good => good += count as u64,
|
||||
SectorStatus::Slow => {
|
||||
slow += count as u64;
|
||||
ranges.push(SectorRange {
|
||||
start_lba: lba,
|
||||
count: count as u32,
|
||||
status: SectorStatus::Slow,
|
||||
byte_offset,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
sectors_done += count as u64;
|
||||
if let Some(ref mut cb) = on_progress {
|
||||
cb(sectors_done, total_sectors, status);
|
||||
}
|
||||
} else {
|
||||
// Batch failed — test each sector individually
|
||||
for i in 0..count {
|
||||
let sector_lba = lba + i as u32;
|
||||
let sector_offset = i as usize * 2048;
|
||||
let sector_byte_offset = byte_offset + i as u64 * 2048;
|
||||
|
||||
let s1 = Instant::now();
|
||||
let first_ok = reader
|
||||
.read_sectors_fast(sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048])
|
||||
.is_ok();
|
||||
let s1_ms = s1.elapsed().as_millis();
|
||||
|
||||
let status = if first_ok && s1_ms <= 500 {
|
||||
good += 1;
|
||||
SectorStatus::Good
|
||||
} else if first_ok {
|
||||
slow += 1;
|
||||
SectorStatus::Slow
|
||||
} else {
|
||||
// Retry once more after brief pause
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
if reader
|
||||
.read_sectors_fast(sector_lba, 1, &mut buf[sector_offset..sector_offset + 2048])
|
||||
.is_ok()
|
||||
{
|
||||
recovered += 1;
|
||||
SectorStatus::Recovered
|
||||
} else {
|
||||
bad += 1;
|
||||
SectorStatus::Bad
|
||||
}
|
||||
};
|
||||
|
||||
if status != SectorStatus::Good {
|
||||
// Merge with previous range if contiguous and same status
|
||||
if let Some(last) = ranges.last_mut() {
|
||||
if last.status == status
|
||||
&& last.start_lba + last.count == sector_lba
|
||||
{
|
||||
last.count += 1;
|
||||
} else {
|
||||
ranges.push(SectorRange {
|
||||
start_lba: sector_lba,
|
||||
count: 1,
|
||||
status,
|
||||
byte_offset: sector_byte_offset,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
ranges.push(SectorRange {
|
||||
start_lba: sector_lba,
|
||||
count: 1,
|
||||
status,
|
||||
byte_offset: sector_byte_offset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sectors_done += 1;
|
||||
if let Some(ref mut cb) = on_progress {
|
||||
cb(sectors_done, total_sectors, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += count as u32;
|
||||
byte_offset += bytes as u64;
|
||||
}
|
||||
}
|
||||
|
||||
VerifyResult {
|
||||
total_sectors,
|
||||
good,
|
||||
slow,
|
||||
recovered,
|
||||
bad,
|
||||
ranges,
|
||||
elapsed_secs: start.elapsed().as_secs_f64(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user