Fix cargo fmt formatting
This commit is contained in:
+15
-4
@@ -34,8 +34,10 @@ fn main() {
|
|||||||
let file = std::fs::File::create("/dev/null").unwrap();
|
let file = std::fs::File::create("/dev/null").unwrap();
|
||||||
let mut writer = std::io::BufWriter::with_capacity(4 * 1024 * 1024, file);
|
let mut writer = std::io::BufWriter::with_capacity(4 * 1024 * 1024, file);
|
||||||
|
|
||||||
eprintln!("Reading 1000 batches ({:.1} MB) with write + progress...",
|
eprintln!(
|
||||||
1000.0 * batch as f64 * 2048.0 / 1_048_576.0);
|
"Reading 1000 batches ({:.1} MB) with write + progress...",
|
||||||
|
1000.0 * batch as f64 * 2048.0 / 1_048_576.0
|
||||||
|
);
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let mut ok = 0u32;
|
let mut ok = 0u32;
|
||||||
@@ -51,7 +53,9 @@ fn main() {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail += 1;
|
fail += 1;
|
||||||
if fail <= 5 { eprintln!(" FAIL LBA {}: {}", lba, e); }
|
if fail <= 5 {
|
||||||
|
eprintln!(" FAIL LBA {}: {}", lba, e);
|
||||||
|
}
|
||||||
buf.fill(0);
|
buf.fill(0);
|
||||||
writer.write_all(&buf).unwrap();
|
writer.write_all(&buf).unwrap();
|
||||||
}
|
}
|
||||||
@@ -67,5 +71,12 @@ fn main() {
|
|||||||
|
|
||||||
let elapsed = start.elapsed().as_secs_f64();
|
let elapsed = start.elapsed().as_secs_f64();
|
||||||
let mb = ok as f64 * batch as f64 * 2048.0 / 1_048_576.0;
|
let mb = ok as f64 * batch as f64 * 2048.0 / 1_048_576.0;
|
||||||
eprintln!("\n{} ok, {} fail, {:.1} MB in {:.1}s = {:.1} MB/s", ok, fail, mb, elapsed, mb / elapsed);
|
eprintln!(
|
||||||
|
"\n{} ok, {} fail, {:.1} MB in {:.1}s = {:.1} MB/s",
|
||||||
|
ok,
|
||||||
|
fail,
|
||||||
|
mb,
|
||||||
|
elapsed,
|
||||||
|
mb / elapsed
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-3
@@ -50,7 +50,9 @@ fn main() {
|
|||||||
eprintln!("\n SLOW READ: LBA {} took {}ms (ok={})", lba, read_ms, ok);
|
eprintln!("\n SLOW READ: LBA {} took {}ms (ok={})", lba, read_ms, ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ok { buf[..n].fill(0); }
|
if !ok {
|
||||||
|
buf[..n].fill(0);
|
||||||
|
}
|
||||||
w.write_all(&buf[..n]).unwrap();
|
w.write_all(&buf[..n]).unwrap();
|
||||||
lba += count as u32;
|
lba += count as u32;
|
||||||
bytes += n as u64;
|
bytes += n as u64;
|
||||||
@@ -60,11 +62,22 @@ fn main() {
|
|||||||
let speed = delta as f64 / last.elapsed().as_secs_f64() / 1_048_576.0;
|
let speed = delta as f64 / last.elapsed().as_secs_f64() / 1_048_576.0;
|
||||||
let avg = bytes as f64 / start.elapsed().as_secs_f64() / 1_048_576.0;
|
let avg = bytes as f64 / start.elapsed().as_secs_f64() / 1_048_576.0;
|
||||||
let pct = bytes as f64 / (cap as f64 * 2048.0) * 100.0;
|
let pct = bytes as f64 / (cap as f64 * 2048.0) * 100.0;
|
||||||
eprint!("\r {:.1}% LBA {} | {:.0} MB/s (avg {:.0}) | {:.1} GB ", pct, lba, speed, avg, bytes as f64 / 1e9);
|
eprint!(
|
||||||
|
"\r {:.1}% LBA {} | {:.0} MB/s (avg {:.0}) | {:.1} GB ",
|
||||||
|
pct,
|
||||||
|
lba,
|
||||||
|
speed,
|
||||||
|
avg,
|
||||||
|
bytes as f64 / 1e9
|
||||||
|
);
|
||||||
last_bytes = bytes;
|
last_bytes = bytes;
|
||||||
last = Instant::now();
|
last = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.flush().unwrap();
|
w.flush().unwrap();
|
||||||
eprintln!("\nDone: {:.1} GB in {:.0}s", bytes as f64 / 1e9, start.elapsed().as_secs_f64());
|
eprintln!(
|
||||||
|
"\nDone: {:.1} GB in {:.0}s",
|
||||||
|
bytes as f64 / 1e9,
|
||||||
|
start.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -1119,7 +1119,11 @@ impl Disc {
|
|||||||
use std::io::{Seek, SeekFrom, Write};
|
use std::io::{Seek, SeekFrom, Write};
|
||||||
|
|
||||||
let total_bytes = self.capacity_sectors as u64 * 2048;
|
let total_bytes = self.capacity_sectors as u64 * 2048;
|
||||||
let keys = if decrypt { self.decrypt_keys() } else { crate::decrypt::DecryptKeys::None };
|
let keys = if decrypt {
|
||||||
|
self.decrypt_keys()
|
||||||
|
} else {
|
||||||
|
crate::decrypt::DecryptKeys::None
|
||||||
|
};
|
||||||
|
|
||||||
// Resume: check existing file
|
// Resume: check existing file
|
||||||
let (start_lba, file) = if resume {
|
let (start_lba, file) = if resume {
|
||||||
@@ -1138,14 +1142,13 @@ impl Disc {
|
|||||||
(safe_sectors, f)
|
(safe_sectors, f)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let f = std::fs::File::create(path)
|
let f =
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
std::fs::File::create(path).map_err(|e| Error::IoError { source: e })?;
|
||||||
(0u32, f)
|
(0u32, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let f = std::fs::File::create(path)
|
let f = std::fs::File::create(path).map_err(|e| Error::IoError { source: e })?;
|
||||||
.map_err(|e| Error::IoError { source: e })?;
|
|
||||||
(0u32, f)
|
(0u32, f)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1160,7 +1163,6 @@ impl Disc {
|
|||||||
let count = remaining.min(batch as u32) as u16;
|
let count = remaining.min(batch as u32) as u16;
|
||||||
let bytes = count as usize * 2048;
|
let bytes = count as usize * 2048;
|
||||||
|
|
||||||
|
|
||||||
reader
|
reader
|
||||||
.read_sectors(lba, count, &mut buf[..bytes])
|
.read_sectors(lba, count, &mut buf[..bytes])
|
||||||
.map_err(|e| Error::IoError {
|
.map_err(|e| Error::IoError {
|
||||||
|
|||||||
+69
-9
@@ -150,7 +150,18 @@ impl Drive {
|
|||||||
/// Uses GET EVENT STATUS NOTIFICATION which works regardless of firmware state.
|
/// Uses GET EVENT STATUS NOTIFICATION which works regardless of firmware state.
|
||||||
pub fn drive_status(&mut self) -> DriveStatus {
|
pub fn drive_status(&mut self) -> DriveStatus {
|
||||||
// GET EVENT STATUS NOTIFICATION: polled, media event class (0x10)
|
// GET EVENT STATUS NOTIFICATION: polled, media event class (0x10)
|
||||||
let cdb = [SCSI_GET_EVENT_STATUS, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x08, 0x00];
|
let cdb = [
|
||||||
|
SCSI_GET_EVENT_STATUS,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x10,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x08,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
let mut buf = [0u8; 8];
|
let mut buf = [0u8; 8];
|
||||||
match self.scsi.as_mut().execute(
|
match self.scsi.as_mut().execute(
|
||||||
&cdb,
|
&cdb,
|
||||||
@@ -343,7 +354,18 @@ impl Drive {
|
|||||||
/// Read REPORT KEY RPC state (region playback control).
|
/// Read REPORT KEY RPC state (region playback control).
|
||||||
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
|
pub fn report_key_rpc_state(&mut self) -> Option<Vec<u8>> {
|
||||||
let cdb = [
|
let cdb = [
|
||||||
SCSI_REPORT_KEY, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x08, 0x00,
|
SCSI_REPORT_KEY,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x08,
|
||||||
|
0x08,
|
||||||
|
0x00,
|
||||||
];
|
];
|
||||||
let mut buf = vec![0u8; 8];
|
let mut buf = vec![0u8; 8];
|
||||||
let r = self
|
let r = self
|
||||||
@@ -365,7 +387,18 @@ impl Drive {
|
|||||||
|
|
||||||
/// Read MODE SENSE page data.
|
/// Read MODE SENSE page data.
|
||||||
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
|
pub fn mode_sense_page(&mut self, page: u8) -> Option<Vec<u8>> {
|
||||||
let cdb = [SCSI_MODE_SENSE, 0x00, page, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00];
|
let cdb = [
|
||||||
|
SCSI_MODE_SENSE,
|
||||||
|
0x00,
|
||||||
|
page,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0xFC,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
let mut buf = vec![0u8; 252];
|
let mut buf = vec![0u8; 252];
|
||||||
let r = self
|
let r = self
|
||||||
.scsi
|
.scsi
|
||||||
@@ -421,7 +454,11 @@ impl Drive {
|
|||||||
/// Returns Err only after all attempts exhausted — user should clean
|
/// Returns Err only after all attempts exhausted — user should clean
|
||||||
/// the disc and resume.
|
/// the disc and resume.
|
||||||
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
pub fn read(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||||
let timeout_ms = if self.recovery_bytes_remaining > 0 { 30_000 } else { 10_000 };
|
let timeout_ms = if self.recovery_bytes_remaining > 0 {
|
||||||
|
30_000
|
||||||
|
} else {
|
||||||
|
10_000
|
||||||
|
};
|
||||||
let cdb = [
|
let cdb = [
|
||||||
crate::scsi::SCSI_READ_10,
|
crate::scsi::SCSI_READ_10,
|
||||||
0x00,
|
0x00,
|
||||||
@@ -437,7 +474,10 @@ impl Drive {
|
|||||||
|
|
||||||
// Normal read
|
// Normal read
|
||||||
if let Ok(result) = self.scsi.as_mut().execute(
|
if let Ok(result) = self.scsi.as_mut().execute(
|
||||||
&cdb, crate::scsi::DataDirection::FromDevice, buf, timeout_ms,
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
buf,
|
||||||
|
timeout_ms,
|
||||||
) {
|
) {
|
||||||
if self.recovery_bytes_remaining > 0 {
|
if self.recovery_bytes_remaining > 0 {
|
||||||
let bytes_read = count as u64 * 2048;
|
let bytes_read = count as u64 * 2048;
|
||||||
@@ -457,7 +497,10 @@ impl Drive {
|
|||||||
std::thread::sleep(std::time::Duration::from_secs(30));
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
if let Ok(result) = self.scsi.as_mut().execute(
|
if let Ok(result) = self.scsi.as_mut().execute(
|
||||||
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
buf,
|
||||||
|
30_000,
|
||||||
) {
|
) {
|
||||||
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
return Ok(result.bytes_transferred);
|
return Ok(result.bytes_transferred);
|
||||||
@@ -479,7 +522,10 @@ impl Drive {
|
|||||||
std::thread::sleep(std::time::Duration::from_secs(30));
|
std::thread::sleep(std::time::Duration::from_secs(30));
|
||||||
|
|
||||||
if let Ok(result) = self.scsi.as_mut().execute(
|
if let Ok(result) = self.scsi.as_mut().execute(
|
||||||
&cdb, crate::scsi::DataDirection::FromDevice, buf, 30_000,
|
&cdb,
|
||||||
|
crate::scsi::DataDirection::FromDevice,
|
||||||
|
buf,
|
||||||
|
30_000,
|
||||||
) {
|
) {
|
||||||
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
self.recovery_bytes_remaining = RECOVERY_WINDOW;
|
||||||
return Ok(result.bytes_transferred);
|
return Ok(result.bytes_transferred);
|
||||||
@@ -524,7 +570,14 @@ impl Drive {
|
|||||||
|
|
||||||
/// Lock the tray so the disc cannot be ejected during a rip.
|
/// Lock the tray so the disc cannot be ejected during a rip.
|
||||||
pub fn lock_tray(&mut self) {
|
pub fn lock_tray(&mut self) {
|
||||||
let prevent = [SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL, 0x00, 0x00, 0x00, 0x01, 0x00];
|
let prevent = [
|
||||||
|
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
let _ =
|
let _ =
|
||||||
self.scsi
|
self.scsi
|
||||||
@@ -534,7 +587,14 @@ impl Drive {
|
|||||||
|
|
||||||
/// Unlock the tray so the user can manually eject the disc.
|
/// Unlock the tray so the user can manually eject the disc.
|
||||||
pub fn unlock_tray(&mut self) {
|
pub fn unlock_tray(&mut self) {
|
||||||
let allow = [SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL, 0x00, 0x00, 0x00, 0x00, 0x00];
|
let allow = [
|
||||||
|
SCSI_PREVENT_ALLOW_MEDIUM_REMOVAL,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
];
|
||||||
let mut buf = [0u8; 0];
|
let mut buf = [0u8; 0];
|
||||||
let _ =
|
let _ =
|
||||||
self.scsi
|
self.scsi
|
||||||
|
|||||||
+88
-26
@@ -89,10 +89,18 @@ pub const E_NO_METADATA: u16 = 9008;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
// Device (1xxx)
|
// Device (1xxx)
|
||||||
DeviceNotFound { path: String },
|
DeviceNotFound {
|
||||||
DevicePermission { path: String },
|
path: String,
|
||||||
DeviceNotReady { path: String },
|
},
|
||||||
DeviceResetFailed { path: String },
|
DevicePermission {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
DeviceNotReady {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
DeviceResetFailed {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
|
||||||
// Profile (2xxx)
|
// Profile (2xxx)
|
||||||
UnsupportedDrive {
|
UnsupportedDrive {
|
||||||
@@ -104,7 +112,10 @@ pub enum Error {
|
|||||||
|
|
||||||
// Unlock (3xxx)
|
// Unlock (3xxx)
|
||||||
UnlockFailed,
|
UnlockFailed,
|
||||||
SignatureMismatch { expected: [u8; 4], got: [u8; 4] },
|
SignatureMismatch {
|
||||||
|
expected: [u8; 4],
|
||||||
|
got: [u8; 4],
|
||||||
|
},
|
||||||
|
|
||||||
// SCSI (4xxx)
|
// SCSI (4xxx)
|
||||||
ScsiError {
|
ScsiError {
|
||||||
@@ -114,14 +125,23 @@ pub enum Error {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// I/O (5xxx)
|
// I/O (5xxx)
|
||||||
IoError { source: std::io::Error },
|
IoError {
|
||||||
|
source: std::io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
// Disc format (6xxx)
|
// Disc format (6xxx)
|
||||||
DiscRead { sector: u64 },
|
DiscRead {
|
||||||
|
sector: u64,
|
||||||
|
},
|
||||||
MplsParse,
|
MplsParse,
|
||||||
ClpiParse,
|
ClpiParse,
|
||||||
UdfNotFound { path: String },
|
UdfNotFound {
|
||||||
DiscTitleRange { index: usize, count: usize },
|
path: String,
|
||||||
|
},
|
||||||
|
DiscTitleRange {
|
||||||
|
index: usize,
|
||||||
|
count: usize,
|
||||||
|
},
|
||||||
IfoParse,
|
IfoParse,
|
||||||
MkvInvalid,
|
MkvInvalid,
|
||||||
NoStreams,
|
NoStreams,
|
||||||
@@ -142,22 +162,40 @@ pub enum Error {
|
|||||||
DecryptFailed,
|
DecryptFailed,
|
||||||
|
|
||||||
// Keydb (8xxx)
|
// Keydb (8xxx)
|
||||||
KeydbConnect { host: String },
|
KeydbConnect {
|
||||||
KeydbHttp { status: u16 },
|
host: String,
|
||||||
|
},
|
||||||
|
KeydbHttp {
|
||||||
|
status: u16,
|
||||||
|
},
|
||||||
KeydbInvalid,
|
KeydbInvalid,
|
||||||
KeydbWrite { path: String },
|
KeydbWrite {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
KeydbParse,
|
KeydbParse,
|
||||||
KeydbLoad { path: String },
|
KeydbLoad {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
|
|
||||||
// Stream/mux (9xxx)
|
// Stream/mux (9xxx)
|
||||||
StreamReadOnly,
|
StreamReadOnly,
|
||||||
StreamWriteOnly,
|
StreamWriteOnly,
|
||||||
StreamUrlInvalid { url: String },
|
StreamUrlInvalid {
|
||||||
StreamUrlMissingPath { scheme: String },
|
url: String,
|
||||||
StreamUrlMissingPort { addr: String },
|
},
|
||||||
PesFrameTooLarge { size: usize },
|
StreamUrlMissingPath {
|
||||||
|
scheme: String,
|
||||||
|
},
|
||||||
|
StreamUrlMissingPort {
|
||||||
|
addr: String,
|
||||||
|
},
|
||||||
|
PesFrameTooLarge {
|
||||||
|
size: usize,
|
||||||
|
},
|
||||||
PesInvalidMagic,
|
PesInvalidMagic,
|
||||||
IsoTooLarge { path: String },
|
IsoTooLarge {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
NoMetadata,
|
NoMetadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,22 +265,46 @@ impl std::fmt::Display for Error {
|
|||||||
product_id,
|
product_id,
|
||||||
product_revision,
|
product_revision,
|
||||||
} => write!(
|
} => write!(
|
||||||
f, "E{}: {} {} {}",
|
f,
|
||||||
self.code(), vendor_id.trim(), product_id.trim(), product_revision.trim()
|
"E{}: {} {} {}",
|
||||||
|
self.code(),
|
||||||
|
vendor_id.trim(),
|
||||||
|
product_id.trim(),
|
||||||
|
product_revision.trim()
|
||||||
),
|
),
|
||||||
Error::SignatureMismatch { expected, got } => write!(
|
Error::SignatureMismatch { expected, got } => write!(
|
||||||
f, "E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
|
f,
|
||||||
|
"E{}: {:02x}{:02x}{:02x}{:02x}!={:02x}{:02x}{:02x}{:02x}",
|
||||||
self.code(),
|
self.code(),
|
||||||
expected[0], expected[1], expected[2], expected[3],
|
expected[0],
|
||||||
got[0], got[1], got[2], got[3]
|
expected[1],
|
||||||
|
expected[2],
|
||||||
|
expected[3],
|
||||||
|
got[0],
|
||||||
|
got[1],
|
||||||
|
got[2],
|
||||||
|
got[3]
|
||||||
),
|
),
|
||||||
Error::ScsiError { opcode, status, sense_key } => {
|
Error::ScsiError {
|
||||||
write!(f, "E{}: 0x{:02x}/0x{:02x}/0x{:02x}", self.code(), opcode, status, sense_key)
|
opcode,
|
||||||
|
status,
|
||||||
|
sense_key,
|
||||||
|
} => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"E{}: 0x{:02x}/0x{:02x}/0x{:02x}",
|
||||||
|
self.code(),
|
||||||
|
opcode,
|
||||||
|
status,
|
||||||
|
sense_key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Error::IoError { source } => write!(f, "E{}: {}", self.code(), source),
|
Error::IoError { source } => write!(f, "E{}: {}", self.code(), source),
|
||||||
Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector),
|
Error::DiscRead { sector } => write!(f, "E{}: {}", self.code(), sector),
|
||||||
Error::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path),
|
Error::UdfNotFound { path } => write!(f, "E{}: {}", self.code(), path),
|
||||||
Error::DiscTitleRange { index, count } => write!(f, "E{}: {}/{}", self.code(), index, count),
|
Error::DiscTitleRange { index, count } => {
|
||||||
|
write!(f, "E{}: {}/{}", self.code(), index, count)
|
||||||
|
}
|
||||||
Error::KeydbConnect { host } => write!(f, "E{}: {}", self.code(), host),
|
Error::KeydbConnect { host } => write!(f, "E{}: {}", self.code(), host),
|
||||||
Error::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status),
|
Error::KeydbHttp { status } => write!(f, "E{}: {}", self.code(), status),
|
||||||
Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path),
|
Error::KeydbWrite { path } => write!(f, "E{}: {}", self.code(), path),
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ pub struct Event {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum EventKind {
|
pub enum EventKind {
|
||||||
// ── Init sequence events ────────────────────────────────────────
|
// ── Init sequence events ────────────────────────────────────────
|
||||||
|
|
||||||
/// Drive opened successfully.
|
/// Drive opened successfully.
|
||||||
DriveOpened { device: String },
|
DriveOpened { device: String },
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ pub enum EventKind {
|
|||||||
ScanComplete { titles: usize },
|
ScanComplete { titles: usize },
|
||||||
|
|
||||||
// ── Read events ─────────────────────────────────────────────────
|
// ── Read events ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Bytes successfully read and written to output.
|
/// Bytes successfully read and written to output.
|
||||||
BytesRead {
|
BytesRead {
|
||||||
/// Bytes written so far.
|
/// Bytes written so far.
|
||||||
|
|||||||
+21
-6
@@ -710,17 +710,32 @@ mod tests {
|
|||||||
pgc.resize(pgc.len() + 48, 0);
|
pgc.resize(pgc.len() + 48, 0);
|
||||||
// Cell 0: sectors 100-200
|
// Cell 0: sectors 100-200
|
||||||
let co = 0xEA;
|
let co = 0xEA;
|
||||||
pgc[co + 8] = 0; pgc[co + 9] = 0; pgc[co + 10] = 0; pgc[co + 11] = 100; // first sector
|
pgc[co + 8] = 0;
|
||||||
pgc[co + 20] = 0; pgc[co + 21] = 0; pgc[co + 22] = 0; pgc[co + 23] = 200; // last sector
|
pgc[co + 9] = 0;
|
||||||
|
pgc[co + 10] = 0;
|
||||||
|
pgc[co + 11] = 100; // first sector
|
||||||
|
pgc[co + 20] = 0;
|
||||||
|
pgc[co + 21] = 0;
|
||||||
|
pgc[co + 22] = 0;
|
||||||
|
pgc[co + 23] = 200; // last sector
|
||||||
// Cell 1: sectors 300-400
|
// Cell 1: sectors 300-400
|
||||||
let co = 0xEA + 24;
|
let co = 0xEA + 24;
|
||||||
pgc[co + 8] = 0; pgc[co + 9] = 0; pgc[co + 10] = 1; pgc[co + 11] = 44; // first sector = 300
|
pgc[co + 8] = 0;
|
||||||
pgc[co + 20] = 0; pgc[co + 21] = 0; pgc[co + 22] = 1; pgc[co + 23] = 144; // last sector = 400
|
pgc[co + 9] = 0;
|
||||||
|
pgc[co + 10] = 1;
|
||||||
|
pgc[co + 11] = 44; // first sector = 300
|
||||||
|
pgc[co + 20] = 0;
|
||||||
|
pgc[co + 21] = 0;
|
||||||
|
pgc[co + 22] = 1;
|
||||||
|
pgc[co + 23] = 144; // last sector = 400
|
||||||
|
|
||||||
let title = parse_pgc(&pgc, 0, 5).unwrap();
|
let title = parse_pgc(&pgc, 0, 5).unwrap();
|
||||||
let expected = 1.0 * 3600.0 + 59.0 * 60.0 + 30.0;
|
let expected = 1.0 * 3600.0 + 59.0 * 60.0 + 30.0;
|
||||||
assert!((title.duration_secs - expected).abs() < 0.1,
|
assert!(
|
||||||
"expected ~{expected}s, got {}s", title.duration_secs);
|
(title.duration_secs - expected).abs() < 0.1,
|
||||||
|
"expected ~{expected}s, got {}s",
|
||||||
|
title.duration_secs
|
||||||
|
);
|
||||||
assert_eq!(title.chapters, 5);
|
assert_eq!(title.chapters, 5);
|
||||||
assert_eq!(title.cells.len(), 2);
|
assert_eq!(title.cells.len(), 2);
|
||||||
assert_eq!(title.cells[0].first_sector, 100);
|
assert_eq!(title.cells[0].first_sector, 100);
|
||||||
|
|||||||
+2
-2
@@ -104,8 +104,8 @@ pub use profile::DriveProfile;
|
|||||||
// Platform trait is pub(crate) -- callers use Drive, not Platform directly
|
// Platform trait is pub(crate) -- callers use Drive, not Platform directly
|
||||||
pub use decrypt::{decrypt_sectors, DecryptKeys};
|
pub use decrypt::{decrypt_sectors, DecryptKeys};
|
||||||
pub use disc::{
|
pub use disc::{
|
||||||
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat,
|
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, Disc,
|
||||||
Disc, DiscFormat, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate,
|
DiscFormat, DiscTitle, Extent, FrameRate, HdrFormat, KeySource, Resolution, SampleRate,
|
||||||
ScanOptions, Stream, SubtitleStream, VideoStream,
|
ScanOptions, Stream, SubtitleStream, VideoStream,
|
||||||
};
|
};
|
||||||
pub use mux::DiscStream;
|
pub use mux::DiscStream;
|
||||||
|
|||||||
@@ -69,9 +69,15 @@ impl CodecParser for HevcParser {
|
|||||||
let nal_type = (data[nal_start] >> 1) & 0x3F;
|
let nal_type = (data[nal_start] >> 1) & 0x3F;
|
||||||
|
|
||||||
match nal_type {
|
match nal_type {
|
||||||
NAL_VPS => { self.vps = Some(data[nal_start..end].to_vec()); }
|
NAL_VPS => {
|
||||||
NAL_SPS => { self.sps = Some(data[nal_start..end].to_vec()); }
|
self.vps = Some(data[nal_start..end].to_vec());
|
||||||
NAL_PPS => { self.pps = Some(data[nal_start..end].to_vec()); }
|
}
|
||||||
|
NAL_SPS => {
|
||||||
|
self.sps = Some(data[nal_start..end].to_vec());
|
||||||
|
}
|
||||||
|
NAL_PPS => {
|
||||||
|
self.pps = Some(data[nal_start..end].to_vec());
|
||||||
|
}
|
||||||
NAL_AUD => {} // Skip access unit delimiters
|
NAL_AUD => {} // Skip access unit delimiters
|
||||||
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
|
t if (NAL_BLA_W_LP..=NAL_RSV_IRAP_VCL23).contains(&t) => {
|
||||||
keyframe = true;
|
keyframe = true;
|
||||||
|
|||||||
+53
-26
@@ -5,9 +5,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
|
//! Read-only. For disc→ISO (raw sector copy), use `Disc::copy()`.
|
||||||
|
|
||||||
use crate::disc::{
|
use crate::disc::{detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions};
|
||||||
detect_max_batch_sectors, Disc, DiscTitle, Extent, ScanOptions,
|
|
||||||
};
|
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
@@ -99,8 +97,8 @@ impl DiscStream {
|
|||||||
let mut reader = super::iso::IsoSectorReader::open(path)?;
|
let mut reader = super::iso::IsoSectorReader::open(path)?;
|
||||||
let capacity = reader.capacity();
|
let capacity = reader.capacity();
|
||||||
|
|
||||||
let disc = Disc::scan_image(&mut reader, capacity, opts)
|
let disc =
|
||||||
.map_err(|e| -> io::Error { e.into() })?;
|
Disc::scan_image(&mut reader, capacity, opts).map_err(|e| -> io::Error { e.into() })?;
|
||||||
|
|
||||||
if disc.titles.is_empty() {
|
if disc.titles.is_empty() {
|
||||||
return Err(crate::error::Error::NoStreams.into());
|
return Err(crate::error::Error::NoStreams.into());
|
||||||
@@ -110,7 +108,8 @@ impl DiscStream {
|
|||||||
return Err(crate::error::Error::DiscTitleRange {
|
return Err(crate::error::Error::DiscTitleRange {
|
||||||
index: idx,
|
index: idx,
|
||||||
count: disc.titles.len(),
|
count: disc.titles.len(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = disc.titles[idx].clone();
|
let title = disc.titles[idx].clone();
|
||||||
@@ -210,7 +209,10 @@ impl DiscStream {
|
|||||||
let bytes = sectors as usize * 2048;
|
let bytes = sectors as usize * 2048;
|
||||||
self.read_buf.resize(bytes, 0);
|
self.read_buf.resize(bytes, 0);
|
||||||
|
|
||||||
match self.reader.read_sectors(lba, sectors, &mut self.read_buf[..bytes]) {
|
match self
|
||||||
|
.reader
|
||||||
|
.read_sectors(lba, sectors, &mut self.read_buf[..bytes])
|
||||||
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.buf_valid = bytes;
|
self.buf_valid = bytes;
|
||||||
self.current_offset += sectors as u32;
|
self.current_offset += sectors as u32;
|
||||||
@@ -241,11 +243,15 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
// Flush demuxer — last PES packet may still be in the assembler
|
// Flush demuxer — last PES packet may still be in the assembler
|
||||||
if let Some(ref mut demuxer) = self.ts_demuxer {
|
if let Some(ref mut demuxer) = self.ts_demuxer {
|
||||||
for pes in &demuxer.flush() {
|
for pes in &demuxer.flush() {
|
||||||
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
if let Some((_, track)) =
|
||||||
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
|
if let Some((_, parser)) =
|
||||||
|
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
for frame in parser.parse(pes) {
|
for frame in parser.parse(pes) {
|
||||||
self.pending_frames.push_back(
|
self.pending_frames.push_back(
|
||||||
crate::pes::PesFrame::from_codec_frame(*track, frame)
|
crate::pes::PesFrame::from_codec_frame(*track, frame),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,11 +264,17 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
let track = match ps.stream_id {
|
let track = match ps.stream_id {
|
||||||
0xE0..=0xEF => 0,
|
0xE0..=0xEF => 0,
|
||||||
0xC0..=0xDF => 1,
|
0xC0..=0xDF => 1,
|
||||||
0xBD => ps.sub_stream_id.map(|s| (s & 0x1F) as usize + 1).unwrap_or(1),
|
0xBD => ps
|
||||||
|
.sub_stream_id
|
||||||
|
.map(|s| (s & 0x1F) as usize + 1)
|
||||||
|
.unwrap_or(1),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
if track < self.title.streams.len() {
|
if track < self.title.streams.len() {
|
||||||
let pts_ns = ps.pts.map(|p| (p as i64) * 1_000_000_000 / 90_000).unwrap_or(0);
|
let pts_ns = ps
|
||||||
|
.pts
|
||||||
|
.map(|p| (p as i64) * 1_000_000_000 / 90_000)
|
||||||
|
.unwrap_or(0);
|
||||||
self.pending_frames.push_back(crate::pes::PesFrame {
|
self.pending_frames.push_back(crate::pes::PesFrame {
|
||||||
track,
|
track,
|
||||||
pts: pts_ns,
|
pts: pts_ns,
|
||||||
@@ -276,22 +288,24 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let bytes = self.buf_valid;
|
let bytes = self.buf_valid;
|
||||||
if let Err(e) = crate::decrypt::decrypt_sectors(
|
if let Err(e) =
|
||||||
&mut self.read_buf[..bytes],
|
crate::decrypt::decrypt_sectors(&mut self.read_buf[..bytes], &self.decrypt_keys, 0)
|
||||||
&self.decrypt_keys,
|
{
|
||||||
0,
|
|
||||||
) {
|
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(ref mut demuxer) = self.ts_demuxer {
|
if let Some(ref mut demuxer) = self.ts_demuxer {
|
||||||
let packets = demuxer.feed(&self.read_buf[..bytes]);
|
let packets = demuxer.feed(&self.read_buf[..bytes]);
|
||||||
for pes in &packets {
|
for pes in &packets {
|
||||||
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
if let Some((_, track)) =
|
||||||
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
|
if let Some((_, parser)) =
|
||||||
|
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
for frame in parser.parse(pes) {
|
for frame in parser.parse(pes) {
|
||||||
self.pending_frames.push_back(
|
self.pending_frames.push_back(
|
||||||
crate::pes::PesFrame::from_codec_frame(*track, frame)
|
crate::pes::PesFrame::from_codec_frame(*track, frame),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,11 +317,17 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
let track = match ps.stream_id {
|
let track = match ps.stream_id {
|
||||||
0xE0..=0xEF => 0,
|
0xE0..=0xEF => 0,
|
||||||
0xC0..=0xDF => 1,
|
0xC0..=0xDF => 1,
|
||||||
0xBD => ps.sub_stream_id.map(|s| (s & 0x1F) as usize + 1).unwrap_or(1),
|
0xBD => ps
|
||||||
|
.sub_stream_id
|
||||||
|
.map(|s| (s & 0x1F) as usize + 1)
|
||||||
|
.unwrap_or(1),
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
if track < self.title.streams.len() {
|
if track < self.title.streams.len() {
|
||||||
let pts_ns = ps.pts.map(|p| (p as i64) * 1_000_000_000 / 90_000).unwrap_or(0);
|
let pts_ns = ps
|
||||||
|
.pts
|
||||||
|
.map(|p| (p as i64) * 1_000_000_000 / 90_000)
|
||||||
|
.unwrap_or(0);
|
||||||
self.pending_frames.push_back(crate::pes::PesFrame {
|
self.pending_frames.push_back(crate::pes::PesFrame {
|
||||||
track,
|
track,
|
||||||
pts: pts_ns,
|
pts: pts_ns,
|
||||||
@@ -330,15 +350,22 @@ impl crate::pes::Stream for DiscStream {
|
|||||||
Err(crate::error::Error::StreamReadOnly.into())
|
Err(crate::error::Error::StreamReadOnly.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(&mut self) -> io::Result<()> { Ok(()) }
|
fn finish(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn info(&self) -> &DiscTitle { &self.title }
|
fn info(&self) -> &DiscTitle {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
let pid = self.pid_to_track.iter()
|
let pid = self
|
||||||
|
.pid_to_track
|
||||||
|
.iter()
|
||||||
.find(|(_, idx)| *idx == track)
|
.find(|(_, idx)| *idx == track)
|
||||||
.map(|(pid, _)| *pid)?;
|
.map(|(pid, _)| *pid)?;
|
||||||
self.parsers.iter()
|
self.parsers
|
||||||
|
.iter()
|
||||||
.find(|(p, _)| *p == pid)
|
.find(|(p, _)| *p == pid)
|
||||||
.and_then(|(_, parser)| parser.codec_private())
|
.and_then(|(_, parser)| parser.codec_private())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -26,7 +26,8 @@ impl IsoSectorReader {
|
|||||||
if sectors > u32::MAX as u64 {
|
if sectors > u32::MAX as u64 {
|
||||||
return Err(crate::error::Error::IsoTooLarge {
|
return Err(crate::error::Error::IsoTooLarge {
|
||||||
path: path.to_string(),
|
path: path.to_string(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
let capacity = sectors as u32;
|
let capacity = sectors as u32;
|
||||||
Ok(Self { file, capacity })
|
Ok(Self { file, capacity })
|
||||||
|
|||||||
+48
-17
@@ -7,7 +7,11 @@ use super::{meta, ts};
|
|||||||
use crate::disc::{DiscTitle, Stream as DiscStream};
|
use crate::disc::{DiscTitle, Stream as DiscStream};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
type PesSetup = (Vec<u16>, Vec<(u16, Box<dyn super::codec::CodecParser>)>, Vec<(u16, usize)>);
|
type PesSetup = (
|
||||||
|
Vec<u16>,
|
||||||
|
Vec<(u16, Box<dyn super::codec::CodecParser>)>,
|
||||||
|
Vec<(u16, usize)>,
|
||||||
|
);
|
||||||
|
|
||||||
/// Size of initial scan buffer for PMT/stream detection.
|
/// Size of initial scan buffer for PMT/stream detection.
|
||||||
const SCAN_SIZE: usize = 1024 * 1024;
|
const SCAN_SIZE: usize = 1024 * 1024;
|
||||||
@@ -59,11 +63,15 @@ impl M2tsStream {
|
|||||||
let m = meta::M2tsMeta::from_title(title);
|
let m = meta::M2tsMeta::from_title(title);
|
||||||
meta::write_header(&mut writer, &m)?;
|
meta::write_header(&mut writer, &m)?;
|
||||||
}
|
}
|
||||||
let pids: Vec<u16> = title.streams.iter().map(|s| match s {
|
let pids: Vec<u16> = title
|
||||||
|
.streams
|
||||||
|
.iter()
|
||||||
|
.map(|s| match s {
|
||||||
DiscStream::Video(v) => v.pid,
|
DiscStream::Video(v) => v.pid,
|
||||||
DiscStream::Audio(a) => a.pid,
|
DiscStream::Audio(a) => a.pid,
|
||||||
DiscStream::Subtitle(s) => s.pid,
|
DiscStream::Subtitle(s) => s.pid,
|
||||||
}).collect();
|
})
|
||||||
|
.collect();
|
||||||
let boxed: Box<dyn Write> = Box::new(writer);
|
let boxed: Box<dyn Write> = Box::new(writer);
|
||||||
let mut muxer = super::tsmux::TsMuxer::new(boxed, &pids);
|
let mut muxer = super::tsmux::TsMuxer::new(boxed, &pids);
|
||||||
for (i, cp) in title.codec_privates.iter().enumerate() {
|
for (i, cp) in title.codec_privates.iter().enumerate() {
|
||||||
@@ -117,11 +125,16 @@ impl M2tsStream {
|
|||||||
let (pids, parsers, pid_to_track) = Self::setup_pes(&title.streams);
|
let (pids, parsers, pid_to_track) = Self::setup_pes(&title.streams);
|
||||||
// Chain: remaining head bytes + rest of reader
|
// Chain: remaining head bytes + rest of reader
|
||||||
let remaining_head = &head[header_end..];
|
let remaining_head = &head[header_end..];
|
||||||
let chain: Box<dyn Read> = Box::new(io::Cursor::new(remaining_head.to_vec()).chain(reader));
|
let chain: Box<dyn Read> =
|
||||||
|
Box::new(io::Cursor::new(remaining_head.to_vec()).chain(reader));
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
disc_title: title.clone(),
|
disc_title: title.clone(),
|
||||||
mode: Mode::Read { reader: chain },
|
mode: Mode::Read { reader: chain },
|
||||||
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
|
demuxer: if pids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ts::TsDemuxer::new(&pids))
|
||||||
|
},
|
||||||
parsers,
|
parsers,
|
||||||
pending_frames: std::collections::VecDeque::new(),
|
pending_frames: std::collections::VecDeque::new(),
|
||||||
pid_to_track,
|
pid_to_track,
|
||||||
@@ -146,7 +159,11 @@ impl M2tsStream {
|
|||||||
..DiscTitle::empty()
|
..DiscTitle::empty()
|
||||||
},
|
},
|
||||||
mode: Mode::Read { reader: chain },
|
mode: Mode::Read { reader: chain },
|
||||||
demuxer: if pids.is_empty() { None } else { Some(ts::TsDemuxer::new(&pids)) },
|
demuxer: if pids.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(ts::TsDemuxer::new(&pids))
|
||||||
|
},
|
||||||
parsers,
|
parsers,
|
||||||
pending_frames: std::collections::VecDeque::new(),
|
pending_frames: std::collections::VecDeque::new(),
|
||||||
pid_to_track,
|
pid_to_track,
|
||||||
@@ -161,7 +178,9 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
if let Some(frame) = self.pending_frames.pop_front() {
|
if let Some(frame) = self.pending_frames.pop_front() {
|
||||||
return Ok(Some(frame));
|
return Ok(Some(frame));
|
||||||
}
|
}
|
||||||
if self.pes_eof { return Ok(None); }
|
if self.pes_eof {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let reader = match &mut self.mode {
|
let reader = match &mut self.mode {
|
||||||
@@ -175,11 +194,15 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
// Flush demuxer — last PES packet may still be in the assembler
|
// Flush demuxer — last PES packet may still be in the assembler
|
||||||
if let Some(ref mut demuxer) = self.demuxer {
|
if let Some(ref mut demuxer) = self.demuxer {
|
||||||
for pes in &demuxer.flush() {
|
for pes in &demuxer.flush() {
|
||||||
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
if let Some((_, track)) =
|
||||||
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
|
if let Some((_, parser)) =
|
||||||
|
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
for frame in parser.parse(pes) {
|
for frame in parser.parse(pes) {
|
||||||
self.pending_frames.push_back(
|
self.pending_frames.push_back(
|
||||||
crate::pes::PesFrame::from_codec_frame(*track, frame)
|
crate::pes::PesFrame::from_codec_frame(*track, frame),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,11 +215,15 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
if let Some(ref mut demuxer) = self.demuxer {
|
if let Some(ref mut demuxer) = self.demuxer {
|
||||||
let packets = demuxer.feed(&buf[..n]);
|
let packets = demuxer.feed(&buf[..n]);
|
||||||
for pes in &packets {
|
for pes in &packets {
|
||||||
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
if let Some((_, track)) =
|
||||||
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
|
if let Some((_, parser)) =
|
||||||
|
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
for frame in parser.parse(pes) {
|
for frame in parser.parse(pes) {
|
||||||
self.pending_frames.push_back(
|
self.pending_frames.push_back(
|
||||||
crate::pes::PesFrame::from_codec_frame(*track, frame)
|
crate::pes::PesFrame::from_codec_frame(*track, frame),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,7 +251,9 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
|
fn info(&self) -> &crate::disc::DiscTitle {
|
||||||
|
&self.disc_title
|
||||||
|
}
|
||||||
|
|
||||||
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
// First check stored codec_privates from FMKV header
|
// First check stored codec_privates from FMKV header
|
||||||
@@ -232,10 +261,13 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
return Some(cp.clone());
|
return Some(cp.clone());
|
||||||
}
|
}
|
||||||
// Fall back to parser-extracted codec_private
|
// Fall back to parser-extracted codec_private
|
||||||
let pid = self.pid_to_track.iter()
|
let pid = self
|
||||||
|
.pid_to_track
|
||||||
|
.iter()
|
||||||
.find(|(_, idx)| *idx == track)
|
.find(|(_, idx)| *idx == track)
|
||||||
.map(|(pid, _)| *pid)?;
|
.map(|(pid, _)| *pid)?;
|
||||||
self.parsers.iter()
|
self.parsers
|
||||||
|
.iter()
|
||||||
.find(|(p, _)| *p == pid)
|
.find(|(p, _)| *p == pid)
|
||||||
.and_then(|(_, parser)| parser.codec_private())
|
.and_then(|(_, parser)| parser.codec_private())
|
||||||
}
|
}
|
||||||
@@ -251,4 +283,3 @@ impl crate::pes::Stream for M2tsStream {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-9
@@ -93,7 +93,9 @@ impl M2tsMeta {
|
|||||||
hdr: v.hdr.id().into(),
|
hdr: v.hdr.id().into(),
|
||||||
label: v.label.clone(),
|
label: v.label.clone(),
|
||||||
secondary: v.secondary,
|
secondary: v.secondary,
|
||||||
codec_private: title.codec_privates.get(i)
|
codec_private: title
|
||||||
|
.codec_privates
|
||||||
|
.get(i)
|
||||||
.and_then(|cp| cp.as_ref())
|
.and_then(|cp| cp.as_ref())
|
||||||
.map(|cp| base64::engine::general_purpose::STANDARD.encode(cp)),
|
.map(|cp| base64::engine::general_purpose::STANDARD.encode(cp)),
|
||||||
},
|
},
|
||||||
@@ -141,8 +143,12 @@ impl M2tsMeta {
|
|||||||
} => Stream::Video(VideoStream {
|
} => Stream::Video(VideoStream {
|
||||||
pid: *pid,
|
pid: *pid,
|
||||||
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
|
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
|
||||||
resolution: resolution.parse().unwrap_or(crate::disc::Resolution::Unknown),
|
resolution: resolution
|
||||||
frame_rate: frame_rate.parse().unwrap_or(crate::disc::FrameRate::Unknown),
|
.parse()
|
||||||
|
.unwrap_or(crate::disc::Resolution::Unknown),
|
||||||
|
frame_rate: frame_rate
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(crate::disc::FrameRate::Unknown),
|
||||||
hdr: hdr.parse().unwrap_or(crate::disc::HdrFormat::Sdr),
|
hdr: hdr.parse().unwrap_or(crate::disc::HdrFormat::Sdr),
|
||||||
color_space: ColorSpace::Bt709,
|
color_space: ColorSpace::Bt709,
|
||||||
secondary: *secondary,
|
secondary: *secondary,
|
||||||
@@ -159,9 +165,13 @@ impl M2tsMeta {
|
|||||||
} => Stream::Audio(AudioStream {
|
} => Stream::Audio(AudioStream {
|
||||||
pid: *pid,
|
pid: *pid,
|
||||||
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
|
codec: codec.parse().unwrap_or(crate::disc::Codec::Unknown(0)),
|
||||||
channels: channels.parse().unwrap_or(crate::disc::AudioChannels::Unknown),
|
channels: channels
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(crate::disc::AudioChannels::Unknown),
|
||||||
language: language.clone(),
|
language: language.clone(),
|
||||||
sample_rate: sample_rate.parse().unwrap_or(crate::disc::SampleRate::Unknown),
|
sample_rate: sample_rate
|
||||||
|
.parse()
|
||||||
|
.unwrap_or(crate::disc::SampleRate::Unknown),
|
||||||
secondary: *secondary,
|
secondary: *secondary,
|
||||||
label: label.clone(),
|
label: label.clone(),
|
||||||
}),
|
}),
|
||||||
@@ -197,16 +207,23 @@ impl M2tsMeta {
|
|||||||
/// Extract codec_private data per stream (from FMKV header).
|
/// Extract codec_private data per stream (from FMKV header).
|
||||||
/// Returns a Vec matching stream order — None for streams without codec_private.
|
/// Returns a Vec matching stream order — None for streams without codec_private.
|
||||||
pub fn codec_privates(&self) -> Vec<Option<Vec<u8>>> {
|
pub fn codec_privates(&self) -> Vec<Option<Vec<u8>>> {
|
||||||
self.streams.iter().map(|s| {
|
self.streams
|
||||||
if let MetaStream::Video { codec_private: Some(ref b64), .. } = s {
|
.iter()
|
||||||
{
|
.map(|s| {
|
||||||
|
if let MetaStream::Video {
|
||||||
|
codec_private: Some(ref b64),
|
||||||
|
..
|
||||||
|
} = s
|
||||||
|
{
|
||||||
|
{
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
base64::engine::general_purpose::STANDARD.decode(b64).ok()
|
base64::engine::general_purpose::STANDARD.decode(b64).ok()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}).collect()
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-17
@@ -40,10 +40,7 @@ pub struct MkvStream {
|
|||||||
impl MkvStream {
|
impl MkvStream {
|
||||||
/// Create for writing PES frames → MKV container.
|
/// Create for writing PES frames → MKV container.
|
||||||
/// Codec privates come from title.codec_privates (populated by input stream).
|
/// Codec privates come from title.codec_privates (populated by input stream).
|
||||||
pub fn create(
|
pub fn create(writer: Box<dyn WriteSeek>, title: &DiscTitle) -> io::Result<Self> {
|
||||||
writer: Box<dyn WriteSeek>,
|
|
||||||
title: &DiscTitle,
|
|
||||||
) -> io::Result<Self> {
|
|
||||||
let mut tracks = Vec::new();
|
let mut tracks = Vec::new();
|
||||||
for (idx, s) in title.streams.iter().enumerate() {
|
for (idx, s) in title.streams.iter().enumerate() {
|
||||||
let mut track = match s {
|
let mut track = match s {
|
||||||
@@ -106,10 +103,14 @@ impl crate::pes::Stream for MkvStream {
|
|||||||
}
|
}
|
||||||
ebml::SIMPLE_BLOCK => {
|
ebml::SIMPLE_BLOCK => {
|
||||||
let block = ebml::read_binary_val(&mut rs.reader, size as usize)?;
|
let block = ebml::read_binary_val(&mut rs.reader, size as usize)?;
|
||||||
if block.len() < 4 { continue; }
|
if block.len() < 4 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let (track, vl) = block_vint(&block);
|
let (track, vl) = block_vint(&block);
|
||||||
if vl + 3 > block.len() { continue; }
|
if vl + 3 > block.len() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
|
let rel_ts = i16::from_be_bytes([block[vl], block[vl + 1]]);
|
||||||
let keyframe = block[vl + 2] & 0x80 != 0;
|
let keyframe = block[vl + 2] & 0x80 != 0;
|
||||||
@@ -139,7 +140,9 @@ impl crate::pes::Stream for MkvStream {
|
|||||||
|
|
||||||
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
Mode::Write { muxer: Some(ref mut m) } => m.write_frame(frame.track, frame.pts, frame.keyframe, &frame.data),
|
Mode::Write {
|
||||||
|
muxer: Some(ref mut m),
|
||||||
|
} => m.write_frame(frame.track, frame.pts, frame.keyframe, &frame.data),
|
||||||
Mode::Write { muxer: None } => Ok(()),
|
Mode::Write { muxer: None } => Ok(()),
|
||||||
Mode::Read(_) => Err(crate::error::Error::StreamReadOnly.into()),
|
Mode::Read(_) => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
}
|
}
|
||||||
@@ -154,7 +157,9 @@ impl crate::pes::Stream for MkvStream {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn info(&self) -> &crate::disc::DiscTitle { &self.disc_title }
|
fn info(&self) -> &crate::disc::DiscTitle {
|
||||||
|
&self.disc_title
|
||||||
|
}
|
||||||
|
|
||||||
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
let track_num = (track + 1) as u16; // MKV tracks are 1-based
|
let track_num = (track + 1) as u16; // MKV tracks are 1-based
|
||||||
@@ -176,9 +181,7 @@ impl crate::pes::Stream for MkvStream {
|
|||||||
// ── MKV header parsing (read side) ────────────────────────────
|
// ── MKV header parsing (read side) ────────────────────────────
|
||||||
|
|
||||||
/// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
|
/// Returns (DiscTitle, codec_privates: Vec<(track_number, codec_private_bytes)>)
|
||||||
fn parse_mkv_header(
|
fn parse_mkv_header(r: &mut impl Read) -> MkvHeaderResult {
|
||||||
r: &mut impl Read,
|
|
||||||
) -> MkvHeaderResult {
|
|
||||||
let mut title = String::new();
|
let mut title = String::new();
|
||||||
let mut duration_ms = 0.0f64;
|
let mut duration_ms = 0.0f64;
|
||||||
let mut ts_scale: u64 = 1_000_000;
|
let mut ts_scale: u64 = 1_000_000;
|
||||||
@@ -220,7 +223,9 @@ fn parse_mkv_header(
|
|||||||
ebml::TIMESTAMP_SCALE => ts_scale = ebml::read_uint_val(r, cs as usize)?,
|
ebml::TIMESTAMP_SCALE => ts_scale = ebml::read_uint_val(r, cs as usize)?,
|
||||||
ebml::DURATION => duration_ms = ebml::read_float_val(r, cs as usize)?,
|
ebml::DURATION => duration_ms = ebml::read_float_val(r, cs as usize)?,
|
||||||
ebml::TITLE => title = ebml::read_string_val(r, cs as usize)?,
|
ebml::TITLE => title = ebml::read_string_val(r, cs as usize)?,
|
||||||
_ => { skip_bytes(r, cs)?; }
|
_ => {
|
||||||
|
skip_bytes(r, cs)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
got_info = true;
|
got_info = true;
|
||||||
@@ -303,11 +308,15 @@ fn parse_track(
|
|||||||
match aid {
|
match aid {
|
||||||
ebml::SAMPLING_FREQUENCY => sr = ebml::read_float_val(r, as_ as usize)?,
|
ebml::SAMPLING_FREQUENCY => sr = ebml::read_float_val(r, as_ as usize)?,
|
||||||
ebml::CHANNELS => ch = ebml::read_uint_val(r, as_ as usize)? as u8,
|
ebml::CHANNELS => ch = ebml::read_uint_val(r, as_ as usize)? as u8,
|
||||||
_ => { skip_bytes(r, as_)?; }
|
_ => {
|
||||||
|
skip_bytes(r, as_)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => { skip_bytes(r, cs)?; }
|
}
|
||||||
|
_ => {
|
||||||
|
skip_bytes(r, cs)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,7 +343,11 @@ fn parse_track(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Map MKV track numbers to BD-TS PIDs
|
// Map MKV track numbers to BD-TS PIDs
|
||||||
let ts_pid = if tnum == 1 { 0x1011 } else { 0x1100 + (tnum - 2) };
|
let ts_pid = if tnum == 1 {
|
||||||
|
0x1011
|
||||||
|
} else {
|
||||||
|
0x1100 + (tnum - 2)
|
||||||
|
};
|
||||||
|
|
||||||
let stream = match ttype {
|
let stream = match ttype {
|
||||||
1 => {
|
1 => {
|
||||||
@@ -382,10 +395,19 @@ fn block_vint(d: &[u8]) -> (u64, usize) {
|
|||||||
return ((((d[0] & 0x3F) as u64) << 8) | d[1] as u64, 2);
|
return ((((d[0] & 0x3F) as u64) << 8) | d[1] as u64, 2);
|
||||||
}
|
}
|
||||||
if d[0] & 0x20 != 0 && d.len() >= 3 {
|
if d[0] & 0x20 != 0 && d.len() >= 3 {
|
||||||
return ((((d[0] & 0x1F) as u64) << 16) | ((d[1] as u64) << 8) | d[2] as u64, 3);
|
return (
|
||||||
|
(((d[0] & 0x1F) as u64) << 16) | ((d[1] as u64) << 8) | d[2] as u64,
|
||||||
|
3,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if d[0] & 0x10 != 0 && d.len() >= 4 {
|
if d[0] & 0x10 != 0 && d.len() >= 4 {
|
||||||
return ((((d[0] & 0x0F) as u64) << 24) | ((d[1] as u64) << 16) | ((d[2] as u64) << 8) | d[3] as u64, 4);
|
return (
|
||||||
|
(((d[0] & 0x0F) as u64) << 24)
|
||||||
|
| ((d[1] as u64) << 16)
|
||||||
|
| ((d[2] as u64) << 8)
|
||||||
|
| d[3] as u64,
|
||||||
|
4,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
(0, 1) // Unsupported 5+ byte VINT — treat as track 0
|
(0, 1) // Unsupported 5+ byte VINT — treat as track 0
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -19,11 +19,9 @@ pub mod codec;
|
|||||||
pub mod disc;
|
pub mod disc;
|
||||||
pub mod ebml;
|
pub mod ebml;
|
||||||
pub mod iso;
|
pub mod iso;
|
||||||
pub mod mkv;
|
|
||||||
pub mod tsmux;
|
|
||||||
pub mod tsreader;
|
|
||||||
mod m2ts;
|
mod m2ts;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
pub mod mkv;
|
||||||
mod mkvstream;
|
mod mkvstream;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod null;
|
pub mod null;
|
||||||
@@ -31,6 +29,8 @@ pub mod ps;
|
|||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod stdio;
|
pub mod stdio;
|
||||||
pub mod ts;
|
pub mod ts;
|
||||||
|
pub mod tsmux;
|
||||||
|
pub mod tsreader;
|
||||||
|
|
||||||
pub use disc::DiscStream;
|
pub use disc::DiscStream;
|
||||||
pub use iso::IsoSectorReader;
|
pub use iso::IsoSectorReader;
|
||||||
|
|||||||
+14
-3
@@ -79,7 +79,11 @@ impl crate::pes::Stream for NetworkStream {
|
|||||||
}
|
}
|
||||||
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
fn write(&mut self, frame: &crate::pes::PesFrame) -> io::Result<()> {
|
||||||
match &mut self.mode {
|
match &mut self.mode {
|
||||||
Mode::Write { writer, ref mut header_written, .. } => {
|
Mode::Write {
|
||||||
|
writer,
|
||||||
|
ref mut header_written,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
if !*header_written {
|
if !*header_written {
|
||||||
if !self.disc_title.streams.is_empty() {
|
if !self.disc_title.streams.is_empty() {
|
||||||
let m = meta::M2tsMeta::from_title(&self.disc_title);
|
let m = meta::M2tsMeta::from_title(&self.disc_title);
|
||||||
@@ -99,7 +103,9 @@ impl crate::pes::Stream for NetworkStream {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
fn info(&self) -> &DiscTitle {
|
||||||
|
&self.disc_title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NetworkStream is PES-only — no IOStream/Read/Write byte interface.
|
// NetworkStream is PES-only — no IOStream/Read/Write byte interface.
|
||||||
@@ -174,7 +180,12 @@ mod tests {
|
|||||||
|
|
||||||
let dt = sample_title();
|
let dt = sample_title();
|
||||||
let mut writer = NetworkStream::connect(&addr).unwrap().meta(&dt);
|
let mut writer = NetworkStream::connect(&addr).unwrap().meta(&dt);
|
||||||
let frame = pes::PesFrame { track: 0, pts: 90000, keyframe: true, data: vec![0x47; 192] };
|
let frame = pes::PesFrame {
|
||||||
|
track: 0,
|
||||||
|
pts: 90000,
|
||||||
|
keyframe: true,
|
||||||
|
data: vec![0x47; 192],
|
||||||
|
};
|
||||||
pes::Stream::write(&mut writer, &frame).unwrap();
|
pes::Stream::write(&mut writer, &frame).unwrap();
|
||||||
pes::Stream::finish(&mut writer).unwrap();
|
pes::Stream::finish(&mut writer).unwrap();
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -17,8 +17,16 @@ impl NullStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl crate::pes::Stream for NullStream {
|
impl crate::pes::Stream for NullStream {
|
||||||
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> { Ok(None) }
|
fn read(&mut self) -> io::Result<Option<crate::pes::PesFrame>> {
|
||||||
fn write(&mut self, _: &crate::pes::PesFrame) -> io::Result<()> { Ok(()) }
|
Ok(None)
|
||||||
fn finish(&mut self) -> io::Result<()> { Ok(()) }
|
}
|
||||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
fn write(&mut self, _: &crate::pes::PesFrame) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn finish(&mut self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn info(&self) -> &DiscTitle {
|
||||||
|
&self.disc_title
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ impl PsDemuxer {
|
|||||||
|
|
||||||
packets
|
packets
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether a start code byte is a valid PES stream ID that carries payload.
|
/// Check whether a start code byte is a valid PES stream ID that carries payload.
|
||||||
|
|||||||
+41
-42
@@ -128,12 +128,14 @@ fn validate_file_path(path: &Path, scheme: &str) -> io::Result<()> {
|
|||||||
if path.as_os_str().is_empty() {
|
if path.as_os_str().is_empty() {
|
||||||
return Err(crate::error::Error::StreamUrlMissingPath {
|
return Err(crate::error::Error::StreamUrlMissingPath {
|
||||||
scheme: scheme.to_string(),
|
scheme: scheme.to_string(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
if path.file_name().is_none() {
|
if path.file_name().is_none() {
|
||||||
return Err(crate::error::Error::StreamUrlInvalid {
|
return Err(crate::error::Error::StreamUrlInvalid {
|
||||||
url: format!("{scheme}://{}", path.display()),
|
url: format!("{scheme}://{}", path.display()),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -143,12 +145,14 @@ fn validate_network_addr(addr: &str) -> io::Result<()> {
|
|||||||
if addr.is_empty() {
|
if addr.is_empty() {
|
||||||
return Err(crate::error::Error::StreamUrlMissingPath {
|
return Err(crate::error::Error::StreamUrlMissingPath {
|
||||||
scheme: "network".to_string(),
|
scheme: "network".to_string(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
if !addr.contains(':') {
|
if !addr.contains(':') {
|
||||||
return Err(crate::error::Error::StreamUrlMissingPort {
|
return Err(crate::error::Error::StreamUrlMissingPort {
|
||||||
addr: addr.to_string(),
|
addr: addr.to_string(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -169,10 +173,15 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
StreamUrl::Disc { device } => {
|
StreamUrl::Disc { device } => {
|
||||||
// Open drive, init, scan — caller manages the drive
|
// Open drive, init, scan — caller manages the drive
|
||||||
let mut drive = match device {
|
let mut drive = match device {
|
||||||
Some(ref d) => crate::drive::Drive::open(d)
|
Some(ref d) => {
|
||||||
.map_err(|e| -> io::Error { e.into() })?,
|
crate::drive::Drive::open(d).map_err(|e| -> io::Error { e.into() })?
|
||||||
None => crate::drive::find_drive()
|
}
|
||||||
.ok_or_else(|| -> io::Error { crate::error::Error::DeviceNotFound { path: String::new() }.into() })?,
|
None => crate::drive::find_drive().ok_or_else(|| -> io::Error {
|
||||||
|
crate::error::Error::DeviceNotFound {
|
||||||
|
path: String::new(),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})?,
|
||||||
};
|
};
|
||||||
let _ = drive.wait_ready();
|
let _ = drive.wait_ready();
|
||||||
let _ = drive.init();
|
let _ = drive.init();
|
||||||
@@ -181,7 +190,8 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
drive,
|
drive,
|
||||||
opts.keydb_path.as_deref(),
|
opts.keydb_path.as_deref(),
|
||||||
opts.title_index.unwrap_or(0),
|
opts.title_index.unwrap_or(0),
|
||||||
).map_err(|e| -> io::Error { e.into() })?;
|
)
|
||||||
|
.map_err(|e| -> io::Error { e.into() })?;
|
||||||
if opts.raw {
|
if opts.raw {
|
||||||
stream.set_raw();
|
stream.set_raw();
|
||||||
}
|
}
|
||||||
@@ -193,7 +203,8 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
Some(p) => crate::disc::ScanOptions::with_keydb(p),
|
Some(p) => crate::disc::ScanOptions::with_keydb(p),
|
||||||
None => crate::disc::ScanOptions::default(),
|
None => crate::disc::ScanOptions::default(),
|
||||||
};
|
};
|
||||||
let mut stream = DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
|
let mut stream =
|
||||||
|
DiscStream::open_iso(&path.to_string_lossy(), opts.title_index, &scan_opts)?;
|
||||||
if opts.raw {
|
if opts.raw {
|
||||||
stream.set_raw();
|
stream.set_raw();
|
||||||
}
|
}
|
||||||
@@ -201,15 +212,17 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
}
|
}
|
||||||
StreamUrl::M2ts { ref path } => {
|
StreamUrl::M2ts { ref path } => {
|
||||||
validate_file_path(path, "m2ts")?;
|
validate_file_path(path, "m2ts")?;
|
||||||
let file = std::fs::File::open(path)
|
let file = std::fs::File::open(path).map_err(|e| {
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e)))?;
|
io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e))
|
||||||
|
})?;
|
||||||
let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
|
let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
|
||||||
Ok(Box::new(M2tsStream::open(reader)?))
|
Ok(Box::new(M2tsStream::open(reader)?))
|
||||||
}
|
}
|
||||||
StreamUrl::Mkv { ref path } => {
|
StreamUrl::Mkv { ref path } => {
|
||||||
validate_file_path(path, "mkv")?;
|
validate_file_path(path, "mkv")?;
|
||||||
let file = std::fs::File::open(path)
|
let file = std::fs::File::open(path).map_err(|e| {
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e)))?;
|
io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e))
|
||||||
|
})?;
|
||||||
let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
|
let reader = std::io::BufReader::with_capacity(IO_BUF_SIZE, file);
|
||||||
Ok(Box::new(MkvStream::open(reader)?))
|
Ok(Box::new(MkvStream::open(reader)?))
|
||||||
}
|
}
|
||||||
@@ -217,16 +230,10 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
validate_network_addr(addr)?;
|
validate_network_addr(addr)?;
|
||||||
Ok(Box::new(NetworkStream::listen(addr)?))
|
Ok(Box::new(NetworkStream::listen(addr)?))
|
||||||
}
|
}
|
||||||
StreamUrl::Stdio => {
|
StreamUrl::Stdio => Ok(Box::new(StdioStream::input())),
|
||||||
Ok(Box::new(StdioStream::input()))
|
StreamUrl::Null => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||||
}
|
|
||||||
StreamUrl::Null => {
|
|
||||||
Err(crate::error::Error::StreamWriteOnly.into())
|
|
||||||
}
|
|
||||||
StreamUrl::Unknown { ref raw } => {
|
StreamUrl::Unknown { ref raw } => {
|
||||||
Err(crate::error::Error::StreamUrlInvalid {
|
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||||
url: raw.clone(),
|
|
||||||
}.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,16 +247,18 @@ pub fn output(
|
|||||||
match parsed {
|
match parsed {
|
||||||
StreamUrl::Mkv { ref path } => {
|
StreamUrl::Mkv { ref path } => {
|
||||||
validate_file_path(path, "mkv")?;
|
validate_file_path(path, "mkv")?;
|
||||||
let file = std::fs::File::create(path)
|
let file = std::fs::File::create(path).map_err(|e| {
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e)))?;
|
io::Error::new(e.kind(), format!("mkv://{}: {}", path.display(), e))
|
||||||
|
})?;
|
||||||
let writer: Box<dyn super::WriteSeek> =
|
let writer: Box<dyn super::WriteSeek> =
|
||||||
Box::new(std::io::BufWriter::with_capacity(IO_BUF_SIZE, file));
|
Box::new(std::io::BufWriter::with_capacity(IO_BUF_SIZE, file));
|
||||||
Ok(Box::new(MkvStream::create(writer, title)?))
|
Ok(Box::new(MkvStream::create(writer, title)?))
|
||||||
}
|
}
|
||||||
StreamUrl::M2ts { ref path } => {
|
StreamUrl::M2ts { ref path } => {
|
||||||
validate_file_path(path, "m2ts")?;
|
validate_file_path(path, "m2ts")?;
|
||||||
let file = std::fs::File::create(path)
|
let file = std::fs::File::create(path).map_err(|e| {
|
||||||
.map_err(|e| io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e)))?;
|
io::Error::new(e.kind(), format!("m2ts://{}: {}", path.display(), e))
|
||||||
|
})?;
|
||||||
let writer = std::io::BufWriter::with_capacity(IO_BUF_SIZE, file);
|
let writer = std::io::BufWriter::with_capacity(IO_BUF_SIZE, file);
|
||||||
Ok(Box::new(M2tsStream::create(writer, title)?))
|
Ok(Box::new(M2tsStream::create(writer, title)?))
|
||||||
}
|
}
|
||||||
@@ -257,22 +266,12 @@ pub fn output(
|
|||||||
validate_network_addr(addr)?;
|
validate_network_addr(addr)?;
|
||||||
Ok(Box::new(NetworkStream::connect(addr)?.meta(title)))
|
Ok(Box::new(NetworkStream::connect(addr)?.meta(title)))
|
||||||
}
|
}
|
||||||
StreamUrl::Stdio => {
|
StreamUrl::Stdio => Ok(Box::new(StdioStream::output(title))),
|
||||||
Ok(Box::new(StdioStream::output(title)))
|
StreamUrl::Null => Ok(Box::new(NullStream::new(title))),
|
||||||
}
|
StreamUrl::Disc { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
StreamUrl::Null => {
|
StreamUrl::Iso { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
Ok(Box::new(NullStream::new(title)))
|
|
||||||
}
|
|
||||||
StreamUrl::Disc { .. } => {
|
|
||||||
Err(crate::error::Error::StreamReadOnly.into())
|
|
||||||
}
|
|
||||||
StreamUrl::Iso { .. } => {
|
|
||||||
Err(crate::error::Error::StreamReadOnly.into())
|
|
||||||
}
|
|
||||||
StreamUrl::Unknown { ref raw } => {
|
StreamUrl::Unknown { ref raw } => {
|
||||||
Err(crate::error::Error::StreamUrlInvalid {
|
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||||
url: raw.clone(),
|
|
||||||
}.into())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -84,13 +84,19 @@ impl crate::pes::Stream for StdioStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn finish(&mut self) -> io::Result<()> {
|
fn finish(&mut self) -> io::Result<()> {
|
||||||
if let Some(w) = &mut self.writer { w.flush()?; }
|
if let Some(w) = &mut self.writer {
|
||||||
|
w.flush()?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
fn info(&self) -> &DiscTitle {
|
||||||
|
&self.disc_title
|
||||||
|
}
|
||||||
|
|
||||||
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
self.stored_codec_privates.get(track).and_then(|c| c.clone())
|
self.stored_codec_privates
|
||||||
|
.get(track)
|
||||||
|
.and_then(|c| c.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn headers_ready(&self) -> bool {
|
fn headers_ready(&self) -> bool {
|
||||||
|
|||||||
+4
-1
@@ -329,7 +329,10 @@ pub fn scan_streams(data: &[u8]) -> Option<Vec<crate::disc::Stream>> {
|
|||||||
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
|
let section_len = (((data[pat_start + 1] & 0x0F) as usize) << 8)
|
||||||
| data[pat_start + 2] as usize;
|
| data[pat_start + 2] as usize;
|
||||||
let entries_start = pat_start + 8;
|
let entries_start = pat_start + 8;
|
||||||
if section_len < 4 { offset += BD_TS_PACKET_SIZE; continue; }
|
if section_len < 4 {
|
||||||
|
offset += BD_TS_PACKET_SIZE;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let entries_end = pat_start + 3 + section_len - 4;
|
let entries_end = pat_start + 3 + section_len - 4;
|
||||||
let mut e = entries_start;
|
let mut e = entries_start;
|
||||||
while e + 4 <= data.len() && e < entries_end {
|
while e + 4 <= data.len() && e < entries_end {
|
||||||
|
|||||||
+19
-12
@@ -40,12 +40,7 @@ impl<W: Write> TsMuxer<W> {
|
|||||||
/// Write a PES frame as BD-TS packets.
|
/// Write a PES frame as BD-TS packets.
|
||||||
/// Video frame data is expected as length-prefixed NALUs (MKV/PES format)
|
/// Video frame data is expected as length-prefixed NALUs (MKV/PES format)
|
||||||
/// and is converted to Annex B for transport stream.
|
/// and is converted to Annex B for transport stream.
|
||||||
pub fn write_frame(
|
pub fn write_frame(&mut self, track: usize, pts_ns: i64, data: &[u8]) -> io::Result<()> {
|
||||||
&mut self,
|
|
||||||
track: usize,
|
|
||||||
pts_ns: i64,
|
|
||||||
data: &[u8],
|
|
||||||
) -> io::Result<()> {
|
|
||||||
if track >= self.pids.len() {
|
if track >= self.pids.len() {
|
||||||
return Ok(()); // unknown track, skip
|
return Ok(()); // unknown track, skip
|
||||||
}
|
}
|
||||||
@@ -125,11 +120,13 @@ impl<W: Write> TsMuxer<W> {
|
|||||||
self.writer.write_all(&STUFF_FF[..stuff_len - 2])?;
|
self.writer.write_all(&STUFF_FF[..stuff_len - 2])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.writer.write_all(&pes_packet[offset..offset + payload_len])?;
|
self.writer
|
||||||
|
.write_all(&pes_packet[offset..offset + payload_len])?;
|
||||||
} else {
|
} else {
|
||||||
self.writer.write_all(&tp_extra)?;
|
self.writer.write_all(&tp_extra)?;
|
||||||
self.writer.write_all(&ts_header)?;
|
self.writer.write_all(&ts_header)?;
|
||||||
self.writer.write_all(&pes_packet[offset..offset + payload_len])?;
|
self.writer
|
||||||
|
.write_all(&pes_packet[offset..offset + payload_len])?;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += payload_len;
|
offset += payload_len;
|
||||||
@@ -202,24 +199,34 @@ fn hvcc_to_annex_b(hvcc: &[u8]) -> Option<Vec<u8>> {
|
|||||||
let mut offset = 23;
|
let mut offset = 23;
|
||||||
|
|
||||||
for _ in 0..num_arrays {
|
for _ in 0..num_arrays {
|
||||||
if offset + 3 > hvcc.len() { break; }
|
if offset + 3 > hvcc.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
// array: 1 byte (completeness + NAL type), 2 bytes (numNalus)
|
// array: 1 byte (completeness + NAL type), 2 bytes (numNalus)
|
||||||
let _nal_type = hvcc[offset] & 0x3F;
|
let _nal_type = hvcc[offset] & 0x3F;
|
||||||
let num_nalus = u16::from_be_bytes([hvcc[offset + 1], hvcc[offset + 2]]) as usize;
|
let num_nalus = u16::from_be_bytes([hvcc[offset + 1], hvcc[offset + 2]]) as usize;
|
||||||
offset += 3;
|
offset += 3;
|
||||||
|
|
||||||
for _ in 0..num_nalus {
|
for _ in 0..num_nalus {
|
||||||
if offset + 2 > hvcc.len() { break; }
|
if offset + 2 > hvcc.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let nal_len = u16::from_be_bytes([hvcc[offset], hvcc[offset + 1]]) as usize;
|
let nal_len = u16::from_be_bytes([hvcc[offset], hvcc[offset + 1]]) as usize;
|
||||||
offset += 2;
|
offset += 2;
|
||||||
if offset + nal_len > hvcc.len() { break; }
|
if offset + nal_len > hvcc.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
out.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
|
||||||
out.extend_from_slice(&hvcc[offset..offset + nal_len]);
|
out.extend_from_slice(&hvcc[offset..offset + nal_len]);
|
||||||
offset += nal_len;
|
offset += nal_len;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.is_empty() { None } else { Some(out) }
|
if out.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert length-prefixed NALUs (4-byte BE length + NAL) to Annex B
|
/// Convert length-prefixed NALUs (4-byte BE length + NAL) to Annex B
|
||||||
|
|||||||
+12
-5
@@ -69,10 +69,14 @@ impl<R: Read> TsDemuxReader<R> {
|
|||||||
|
|
||||||
let packets = self.demuxer.feed(&self.buf[..n]);
|
let packets = self.demuxer.feed(&self.buf[..n]);
|
||||||
for pes in &packets {
|
for pes in &packets {
|
||||||
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid) {
|
if let Some((_, track)) = self.pid_to_track.iter().find(|(pid, _)| *pid == pes.pid)
|
||||||
if let Some((_, parser)) = self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid) {
|
{
|
||||||
|
if let Some((_, parser)) =
|
||||||
|
self.parsers.iter_mut().find(|(pid, _)| *pid == pes.pid)
|
||||||
|
{
|
||||||
for frame in parser.parse(pes) {
|
for frame in parser.parse(pes) {
|
||||||
self.pending.push_back(PesFrame::from_codec_frame(*track, frame));
|
self.pending
|
||||||
|
.push_back(PesFrame::from_codec_frame(*track, frame));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,10 +90,13 @@ impl<R: Read> TsDemuxReader<R> {
|
|||||||
|
|
||||||
/// Codec private data for a track.
|
/// Codec private data for a track.
|
||||||
pub fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
pub fn codec_private(&self, track: usize) -> Option<Vec<u8>> {
|
||||||
let pid = self.pid_to_track.iter()
|
let pid = self
|
||||||
|
.pid_to_track
|
||||||
|
.iter()
|
||||||
.find(|(_, idx)| *idx == track)
|
.find(|(_, idx)| *idx == track)
|
||||||
.map(|(pid, _)| *pid)?;
|
.map(|(pid, _)| *pid)?;
|
||||||
self.parsers.iter()
|
self.parsers
|
||||||
|
.iter()
|
||||||
.find(|(p, _)| *p == pid)
|
.find(|(p, _)| *p == pid)
|
||||||
.and_then(|(_, parser)| parser.codec_private())
|
.and_then(|(_, parser)| parser.codec_private())
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-6
@@ -28,7 +28,8 @@ impl PesFrame {
|
|||||||
if self.data.len() > u32::MAX as usize {
|
if self.data.len() > u32::MAX as usize {
|
||||||
return Err(crate::error::Error::PesFrameTooLarge {
|
return Err(crate::error::Error::PesFrameTooLarge {
|
||||||
size: self.data.len(),
|
size: self.data.len(),
|
||||||
}.into());
|
}
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
w.write_all(&[self.track as u8])?;
|
w.write_all(&[self.track as u8])?;
|
||||||
w.write_all(&self.pts.to_le_bytes())?;
|
w.write_all(&self.pts.to_le_bytes())?;
|
||||||
@@ -49,8 +50,7 @@ impl PesFrame {
|
|||||||
}
|
}
|
||||||
let track = header[0] as usize;
|
let track = header[0] as usize;
|
||||||
let pts = i64::from_le_bytes([
|
let pts = i64::from_le_bytes([
|
||||||
header[1], header[2], header[3], header[4],
|
header[1], header[2], header[3], header[4], header[5], header[6], header[7], header[8],
|
||||||
header[5], header[6], header[7], header[8],
|
|
||||||
]);
|
]);
|
||||||
let keyframe = header[9] != 0;
|
let keyframe = header[9] != 0;
|
||||||
let len = u32::from_le_bytes([header[10], header[11], header[12], header[13]]) as usize;
|
let len = u32::from_le_bytes([header[10], header[11], header[12], header[13]]) as usize;
|
||||||
@@ -59,7 +59,12 @@ impl PesFrame {
|
|||||||
}
|
}
|
||||||
let mut data = vec![0u8; len];
|
let mut data = vec![0u8; len];
|
||||||
r.read_exact(&mut data)?;
|
r.read_exact(&mut data)?;
|
||||||
Ok(Some(Self { track, pts, keyframe, data }))
|
Ok(Some(Self {
|
||||||
|
track,
|
||||||
|
pts,
|
||||||
|
keyframe,
|
||||||
|
data,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create from a codec::Frame with a track index.
|
/// Create from a codec::Frame with a track index.
|
||||||
@@ -88,10 +93,14 @@ pub trait Stream {
|
|||||||
fn info(&self) -> &crate::disc::DiscTitle;
|
fn info(&self) -> &crate::disc::DiscTitle;
|
||||||
|
|
||||||
/// Codec initialization data for a track (SPS/PPS, etc).
|
/// Codec initialization data for a track (SPS/PPS, etc).
|
||||||
fn codec_private(&self, _track: usize) -> Option<Vec<u8>> { None }
|
fn codec_private(&self, _track: usize) -> Option<Vec<u8>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// True when codec_private is available for all video tracks.
|
/// True when codec_private is available for all video tracks.
|
||||||
fn headers_ready(&self) -> bool { true }
|
fn headers_ready(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wraps any output stream and counts bytes written.
|
/// Wraps any output stream and counts bytes written.
|
||||||
|
|||||||
+5
-5
@@ -166,7 +166,10 @@ impl SgIoTransport {
|
|||||||
let err = std::io::Error::last_os_error();
|
let err = std::io::Error::last_os_error();
|
||||||
Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
Err(if err.kind() == std::io::ErrorKind::PermissionDenied {
|
||||||
Error::DevicePermission {
|
Error::DevicePermission {
|
||||||
path: format!("{}: permission denied (try running as root)", device.display()),
|
path: format!(
|
||||||
|
"{}: permission denied (try running as root)",
|
||||||
|
device.display()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Error::DeviceNotFound {
|
Error::DeviceNotFound {
|
||||||
@@ -225,10 +228,7 @@ impl SgIoTransport {
|
|||||||
if let Ok(mut entries) = std::fs::read_dir(&sg_dir) {
|
if let Ok(mut entries) = std::fs::read_dir(&sg_dir) {
|
||||||
if let Some(Ok(entry)) = entries.next() {
|
if let Some(Ok(entry)) = entries.next() {
|
||||||
let sg_name = entry.file_name();
|
let sg_name = entry.file_name();
|
||||||
return std::path::PathBuf::from(format!(
|
return std::path::PathBuf::from(format!("/dev/{}", sg_name.to_string_lossy()));
|
||||||
"/dev/{}",
|
|
||||||
sg_name.to_string_lossy()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -13,7 +13,9 @@ pub trait SectorReader: Send {
|
|||||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
|
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize>;
|
||||||
|
|
||||||
/// Total capacity in sectors, if known.
|
/// Total capacity in sectors, if known.
|
||||||
fn capacity(&self) -> u32 { 0 }
|
fn capacity(&self) -> u32 {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SectorReader backed by a file (ISO image).
|
/// SectorReader backed by a file (ISO image).
|
||||||
@@ -47,9 +49,11 @@ impl SectorReader for FileSectorReader {
|
|||||||
use std::io::{Read, Seek, SeekFrom};
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
let offset = lba as u64 * 2048;
|
let offset = lba as u64 * 2048;
|
||||||
let bytes = count as usize * 2048;
|
let bytes = count as usize * 2048;
|
||||||
self.file.seek(SeekFrom::Start(offset))
|
self.file
|
||||||
|
.seek(SeekFrom::Start(offset))
|
||||||
.map_err(|e| crate::error::Error::IoError { source: e })?;
|
.map_err(|e| crate::error::Error::IoError { source: e })?;
|
||||||
self.file.read_exact(&mut buf[..bytes])
|
self.file
|
||||||
|
.read_exact(&mut buf[..bytes])
|
||||||
.map_err(|e| crate::error::Error::IoError { source: e })?;
|
.map_err(|e| crate::error::Error::IoError { source: e })?;
|
||||||
Ok(bytes)
|
Ok(bytes)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -416,7 +416,11 @@ fn batch_count_u16_overflow_regression() {
|
|||||||
// 47513600 = 725 * 65536, lower 16 bits = 0
|
// 47513600 = 725 * 65536, lower 16 bits = 0
|
||||||
let remaining: u32 = 47533152 - 19552; // = 47513600
|
let remaining: u32 = 47533152 - 19552; // = 47513600
|
||||||
assert_eq!(remaining, 47513600);
|
assert_eq!(remaining, 47513600);
|
||||||
assert_eq!(remaining % 65536, 0, "remaining should be multiple of 65536");
|
assert_eq!(
|
||||||
|
remaining % 65536,
|
||||||
|
0,
|
||||||
|
"remaining should be multiple of 65536"
|
||||||
|
);
|
||||||
|
|
||||||
// Buggy version produces 0 → infinite loop
|
// Buggy version produces 0 → infinite loop
|
||||||
assert_eq!(buggy_batch_count(remaining, 60), 0);
|
assert_eq!(buggy_batch_count(remaining, 60), 0);
|
||||||
|
|||||||
+30
-6
@@ -121,7 +121,11 @@ fn open_input_bare_path_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("not a valid stream URL") || msg.contains("E9002"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("not a valid stream URL") || msg.contains("E9002"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -133,7 +137,11 @@ fn open_output_bare_path_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("not a valid stream URL") || msg.contains("E9002"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("not a valid stream URL") || msg.contains("E9002"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -144,7 +152,11 @@ fn open_input_m2ts_empty_path_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("requires a file path") || msg.contains("E9003"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("requires a file path") || msg.contains("E9003"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -155,7 +167,11 @@ fn open_output_null_input_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("write-only") || msg.contains("E9001"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("write-only") || msg.contains("E9001"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -167,7 +183,11 @@ fn open_output_disc_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("read-only") || msg.contains("E9000"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("read-only") || msg.contains("E9000"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -178,7 +198,11 @@ fn open_input_network_no_port_errors() {
|
|||||||
Err(e) => e.to_string(),
|
Err(e) => e.to_string(),
|
||||||
Ok(_) => panic!("expected error"),
|
Ok(_) => panic!("expected error"),
|
||||||
};
|
};
|
||||||
assert!(msg.contains("PES pipeline") || msg.contains("missing port") || msg.contains("E9004"), "got: {}", msg);
|
assert!(
|
||||||
|
msg.contains("PES pipeline") || msg.contains("missing port") || msg.contains("E9004"),
|
||||||
|
"got: {}",
|
||||||
|
msg
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user