mirror of
https://github.com/freemkv/libfreemkv.git
synced 2026-10-07 18:13:10 +00:00
dirimage: narrow io-error mapping, round-trip volume id, fix dir-size comment order
This commit is contained in:
@@ -136,7 +136,7 @@ fn put_domain_id(buf: &mut [u8]) {
|
||||
/// OSTA CS0 d-string: a compression-ID byte, the characters, then the used
|
||||
/// length in the FIELD'S LAST byte (ECMA-167 1/7.2.12 + UDF 2.1.3). An
|
||||
/// all-zero field is the empty string.
|
||||
fn put_dstring(buf: &mut [u8], s: &str) {
|
||||
pub(super) fn put_dstring(buf: &mut [u8], s: &str) {
|
||||
if s.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
+29
-21
@@ -245,9 +245,6 @@ fn walk(dir: &Path, disc_path: &str, depth: u32, entries: &mut usize) -> Result<
|
||||
}
|
||||
}
|
||||
|
||||
// `find_dir`/`read_file` match path components case-insensitively, so two
|
||||
// entries differing only in case are indistinguishable to every consumer —
|
||||
// the second would silently shadow the first. Only on a case-sensitive host.
|
||||
let dir_len = dir_bytes(&dirs, &files);
|
||||
if dir_len.div_ceil(SECTOR) > (crate::udf::MAX_DIR_BYTES as usize) / SECTOR {
|
||||
// The UDF reader rejects a larger directory; refuse it here, naming it.
|
||||
@@ -255,6 +252,9 @@ fn walk(dir: &Path, disc_path: &str, depth: u32, entries: &mut usize) -> Result<
|
||||
path: disc_path.to_string(),
|
||||
});
|
||||
}
|
||||
// `find_dir`/`read_file` match path components case-insensitively, so two
|
||||
// entries differing only in case are indistinguishable to every consumer —
|
||||
// the second would silently shadow the first. Only on a case-sensitive host.
|
||||
names.sort();
|
||||
for pair in names.windows(2) {
|
||||
if pair[0] == pair[1] {
|
||||
@@ -349,11 +349,14 @@ fn read_head(path: &Path, disc_path: &str, n: usize) -> Result<Vec<u8>> {
|
||||
// `read_exact`, not `read`: a single `read` may legally return fewer bytes on
|
||||
// a network/FUSE mount (a NAS-hosted backup is normal here). A short buffer
|
||||
// records NO constraint, so the rip reads the wrong sectors at exit 0.
|
||||
let unreadable = |_| Error::DirImagePlacement {
|
||||
path: disc_path.to_string(),
|
||||
};
|
||||
let mut f = std::fs::File::open(path).map_err(unreadable)?;
|
||||
f.read_exact(&mut buf).map_err(unreadable)?;
|
||||
let mut f = std::fs::File::open(path).map_err(Error::from)?;
|
||||
f.read_exact(&mut buf).map_err(|e| match e.kind() {
|
||||
// A short IFO cannot resolve placement; other errors stay retryable IoError.
|
||||
std::io::ErrorKind::UnexpectedEof => Error::DirImagePlacement {
|
||||
path: disc_path.to_string(),
|
||||
},
|
||||
_ => Error::from(e),
|
||||
})?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
@@ -621,19 +624,24 @@ pub(super) fn plan(root: &Path) -> Result<Layout> {
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| "FREEMKV".to_string());
|
||||
// A 32-byte d-string holds 30 ASCII chars, but only 15 UTF-16 units once
|
||||
// non-ASCII forces CS0 ID 16. Trim here so `put_dstring` never cuts a char.
|
||||
let mut volume_id: String = volume_id.chars().take(30).collect();
|
||||
if !volume_id.is_ascii() {
|
||||
let mut units = 0;
|
||||
volume_id = volume_id
|
||||
.chars()
|
||||
.take_while(|c| {
|
||||
units += c.len_utf16();
|
||||
units <= 15
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
// A 32-byte d-string holds 30 ASCII chars, or 15 UTF-16 units once non-ASCII
|
||||
// forces CS0 ID 16. Round-trip through the encoder and the reader so `volume_id`
|
||||
// is exactly what the reader decodes (it drops surrogates and trims).
|
||||
let mut units = 0;
|
||||
let cut: String = volume_id
|
||||
.chars()
|
||||
.take_while(|c| {
|
||||
units += if volume_id.is_ascii() {
|
||||
1
|
||||
} else {
|
||||
c.len_utf16()
|
||||
};
|
||||
units <= if volume_id.is_ascii() { 30 } else { 15 }
|
||||
})
|
||||
.collect();
|
||||
let mut field = [0u8; 32];
|
||||
super::encode::put_dstring(&mut field, &cut);
|
||||
let volume_id = crate::udf::parse_dstring(&field);
|
||||
|
||||
Ok(Layout {
|
||||
part_start,
|
||||
|
||||
+6
-3
@@ -183,9 +183,12 @@ impl DirImage {
|
||||
self.open.insert(0, entry);
|
||||
return Ok(&mut self.open[0].1);
|
||||
}
|
||||
// A file that vanished or became unreadable is a change; name it.
|
||||
let changed = |_| Error::DirImageFileChanged {
|
||||
path: self.files[file].disc_path.clone(),
|
||||
// A vanished file is a change; name it. Other io errors stay retryable IoError.
|
||||
let changed = |e: std::io::Error| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => Error::DirImageFileChanged {
|
||||
path: self.files[file].disc_path.clone(),
|
||||
},
|
||||
_ => Error::from(e),
|
||||
};
|
||||
let f = File::open(&self.files[file].host).map_err(changed)?;
|
||||
let md = f.metadata().map_err(changed)?;
|
||||
|
||||
@@ -1211,6 +1211,68 @@ fn a_non_ascii_volume_id_matches_what_the_reader_sees() {
|
||||
assert_eq!(fs.volume_id, img.volume_id());
|
||||
}
|
||||
|
||||
/// A folder name with an emoji and a trailing space INSIDE the 15-unit cut still matches the reader.
|
||||
#[test]
|
||||
fn a_volume_id_cut_inside_emoji_or_space_matches_the_reader() {
|
||||
for (tag, name) in [
|
||||
("volemoji", "Film \u{1F600} x"),
|
||||
("volspace", "Cr\u{e8}me br\u{fb}l\u{e9}e 1 x"),
|
||||
] {
|
||||
let base = Scratch::new(tag);
|
||||
base.file(&format!("{name}/BDMV/index.bdmv"), &pattern(1, 16));
|
||||
base.file(&format!("{name}/BDMV/STREAM/00000.m2ts"), &pattern(2, 4096));
|
||||
let mut img = DirImage::open(&base.path().join(name)).unwrap();
|
||||
let fs = udf::read_filesystem(&mut img).unwrap();
|
||||
assert_eq!(fs.volume_id, img.volume_id(), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An unreadable (not vanished) file keeps its IoError so engine retry logic still sees it.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn an_unreadable_file_is_an_io_error_not_file_changed() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let (s, _, _) = bdmv_scratch();
|
||||
let mut img = DirImage::open(s.path()).unwrap();
|
||||
let fs = udf::read_filesystem(&mut img).unwrap();
|
||||
let (lba, sectors) = fs
|
||||
.file_extents(&mut img, "/BDMV/STREAM/00000.m2ts")
|
||||
.unwrap()[0];
|
||||
let p = s.path().join("BDMV/STREAM/00000.m2ts");
|
||||
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
if std::fs::File::open(&p).is_ok() {
|
||||
return; // running as root: permissions not enforced
|
||||
}
|
||||
let mut buf = vec![0u8; sectors as usize * SECTOR];
|
||||
let err = img
|
||||
.read_sectors(lba, sectors as u16, &mut buf, false)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, crate::error::Error::IoError { .. }),
|
||||
"expected IoError, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// An unreadable IFO is an IoError; only a short one is a placement error.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn an_unreadable_ifo_is_an_io_error_not_placement() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let s = Scratch::new("dvdperm");
|
||||
s.file("VIDEO_TS/VIDEO_TS.IFO", &vec![0u8; SECTOR]);
|
||||
s.file("VIDEO_TS/VTS_01_0.IFO", &vec![0u8; SECTOR]);
|
||||
let p = s.path().join("VIDEO_TS/VTS_01_0.IFO");
|
||||
std::fs::set_permissions(&p, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
if std::fs::File::open(&p).is_ok() {
|
||||
return; // running as root: permissions not enforced
|
||||
}
|
||||
let err = DirImage::open(s.path()).unwrap_err();
|
||||
assert!(
|
||||
matches!(err, crate::error::Error::IoError { .. }),
|
||||
"expected IoError, got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
// Two IFOs that parse to the same title-set group (VTS_01_0 vs VTS_1_0) must be refused, or the
|
||||
// second's constraint silently overwrites the first's.
|
||||
#[test]
|
||||
|
||||
+2
-2
@@ -197,7 +197,7 @@ pub const E_DIR_IMAGE_SSIF_UNSUPPORTED: u16 = 9061;
|
||||
/// A `dir://` SOURCE `VIDEO_TS` folder's IFO-declared VOB offsets cannot be
|
||||
/// satisfied by any placement: the required start sector of a VOB lies BELOW
|
||||
/// the end of the file that must precede it. Carries the offending file's disc
|
||||
/// path. A silently misplaced VOB would rip the wrong sectors.
|
||||
/// path. Also raised for an IFO too short to resolve its placement offsets.
|
||||
pub const E_DIR_IMAGE_PLACEMENT: u16 = 9062;
|
||||
/// A `dir://` SOURCE folder still carries live AACS-encrypted content: it has
|
||||
/// an `AACS/` directory AND the sampled content units are genuinely scrambled.
|
||||
@@ -217,7 +217,7 @@ pub const E_DIR_IMAGE_TOO_LARGE: u16 = 9066;
|
||||
/// File Identifier Descriptor stores the encoded length in one byte.
|
||||
pub const E_DIR_NAME_TOO_LONG: u16 = 9067;
|
||||
/// One directory in the folder holds more subdirectories than a UDF link count
|
||||
/// can express (it is 16 bits, one per child plus one for its own entry).
|
||||
/// can express (16 bits), or more entries than the reader's directory size cap.
|
||||
pub const E_DIR_IMAGE_FANOUT: u16 = 9068;
|
||||
/// A title's clip marks excluded more frames than they kept.
|
||||
pub const E_SEAM_PLAN_DROPPED_MOST: u16 = 9069;
|
||||
|
||||
+1
-1
@@ -1573,7 +1573,7 @@ pub(crate) fn merge_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> {
|
||||
/// Parse a UDF d-string (fixed-length field with length byte at the end).
|
||||
/// Used for Volume Identifier and other UDF descriptor strings.
|
||||
/// The first byte of content is a compression ID: 8 = ASCII, 16 = UTF-16BE.
|
||||
fn parse_dstring(data: &[u8]) -> String {
|
||||
pub(crate) fn parse_dstring(data: &[u8]) -> String {
|
||||
let Some(&len) = data.last() else {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user