libfreemkv: dir:// — decrypted file-tree extraction (Disc::extract_tree)
Sibling of Disc::copy specialized to write per-file instead of a whole ISO image, decrypting on the way out: walk the UDF tree, read each file's extents through the shared DecryptingSectorSource (AACS unit-aligned, CSS per-VTS), strip AACS/, sanitize host paths per component, .partial+rename, 1-shot with per-file loss accounting (no mapfile; recovery stays the iso:// multipass path). Reuses UdfFs + the decrypt seam; only the per-file orchestration is new.
This commit is contained in:
+1323
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ mod bluray;
|
|||||||
mod dvd;
|
mod dvd;
|
||||||
pub mod dvd_audio_probe;
|
pub mod dvd_audio_probe;
|
||||||
mod encrypt;
|
mod encrypt;
|
||||||
|
mod extract;
|
||||||
pub mod mapfile;
|
pub mod mapfile;
|
||||||
mod patch;
|
mod patch;
|
||||||
pub mod read_error;
|
pub mod read_error;
|
||||||
@@ -28,6 +29,7 @@ use encrypt::HandshakeResult;
|
|||||||
// so the public surface keeps the structured metadata together. Callers map
|
// so the public surface keeps the structured metadata together. Callers map
|
||||||
// these to display text in their own locale.
|
// these to display text in their own locale.
|
||||||
pub use crate::labels::{LabelPurpose, LabelQualifier};
|
pub use crate::labels::{LabelPurpose, LabelQualifier};
|
||||||
|
pub use extract::{ExtractOptions, ExtractResult, FileResult};
|
||||||
|
|
||||||
// ─── Public types ───────────────────────────────────────────────────────────
|
// ─── Public types ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ pub const E_PES_INVALID_MAGIC: u16 = 9006;
|
|||||||
pub const E_ISO_TOO_LARGE: u16 = 9007;
|
pub const E_ISO_TOO_LARGE: u16 = 9007;
|
||||||
pub const E_NO_METADATA: u16 = 9008;
|
pub const E_NO_METADATA: u16 = 9008;
|
||||||
pub const E_DISC_URL_NOT_DIRECT: u16 = 9009;
|
pub const E_DISC_URL_NOT_DIRECT: u16 = 9009;
|
||||||
|
/// `--raw` given with a `dir://` destination (raw + decrypted-tree is
|
||||||
|
/// a contradiction; raw bytes go to `iso://`).
|
||||||
|
pub const E_DIR_RAW_REJECTED: u16 = 9019;
|
||||||
pub const E_HEVC_PARAM_PARSE: u16 = 9010;
|
pub const E_HEVC_PARAM_PARSE: u16 = 9010;
|
||||||
pub const E_MUX_TRACK_RANGE: u16 = 9011;
|
pub const E_MUX_TRACK_RANGE: u16 = 9011;
|
||||||
pub const E_FMP4_UNIMPLEMENTED: u16 = 9012;
|
pub const E_FMP4_UNIMPLEMENTED: u16 = 9012;
|
||||||
@@ -119,6 +122,21 @@ pub const E_SWEEP_CONSUMER_GONE: u16 = 9016;
|
|||||||
pub const E_PES_TRACK_TOO_LARGE: u16 = 9017;
|
pub const E_PES_TRACK_TOO_LARGE: u16 = 9017;
|
||||||
pub const E_PIPELINE_CONSUMER_GONE: u16 = 9018;
|
pub const E_PIPELINE_CONSUMER_GONE: u16 = 9018;
|
||||||
pub const E_DISC_CAPACITY_OVERFLOW: u16 = 9020;
|
pub const E_DISC_CAPACITY_OVERFLOW: u16 = 9020;
|
||||||
|
/// `--multipass` given with a `dir://` destination (`dir://` is 1-shot;
|
||||||
|
/// recovery is the `iso://` path's job).
|
||||||
|
pub const E_DIR_MULTIPASS_REJECTED: u16 = 9024;
|
||||||
|
/// A non-disc (byte-stream) source was routed into `dir://`, which needs a
|
||||||
|
/// filesystem (only `disc://` / `iso://` qualify).
|
||||||
|
pub const E_DIR_SOURCE_UNSUPPORTED: u16 = 9025;
|
||||||
|
/// `dir://` target directory is non-empty and `--force` was not given.
|
||||||
|
pub const E_DIR_NOT_EMPTY: u16 = 9026;
|
||||||
|
/// `dir://` target filesystem free space is below the sum of file extents.
|
||||||
|
pub const E_DIR_INSUFFICIENT_SPACE: u16 = 9027;
|
||||||
|
/// Two distinct disc paths sanitize to the same host path (would silently
|
||||||
|
/// overwrite — surfaced as a hard error instead).
|
||||||
|
pub const E_DIR_NAME_COLLISION: u16 = 9028;
|
||||||
|
/// A `dir://` create_dir_all / file write / rename failed.
|
||||||
|
pub const E_DIR_WRITE_FAILED: u16 = 9029;
|
||||||
pub const E_M2TS_PACKET_MALFORMED: u16 = 9021;
|
pub const E_M2TS_PACKET_MALFORMED: u16 = 9021;
|
||||||
/// A `network://` output target resolved to no address that is safe to
|
/// A `network://` output target resolved to no address that is safe to
|
||||||
/// connect to (every resolved IP was loopback / private / link-local /
|
/// connect to (every resolved IP was loopback / private / link-local /
|
||||||
@@ -457,6 +475,35 @@ pub enum Error {
|
|||||||
/// last-LBA + 1 overflowed `u32`. Either case means the capacity
|
/// last-LBA + 1 overflowed `u32`. Either case means the capacity
|
||||||
/// response is unusable; no English commentary.
|
/// response is unusable; no English commentary.
|
||||||
DiscCapacityMalformed,
|
DiscCapacityMalformed,
|
||||||
|
/// `--raw` was given with a `dir://` destination. An encrypted file
|
||||||
|
/// tree is useless; raw bytes belong in `iso://`.
|
||||||
|
DirRawRejected,
|
||||||
|
/// `--multipass` was given with a `dir://` destination. `dir://` is
|
||||||
|
/// 1-shot; recovery is the `iso://` multipass path's job.
|
||||||
|
DirMultipassRejected,
|
||||||
|
/// A non-disc (byte-stream) source was routed into `dir://`, which
|
||||||
|
/// requires a filesystem (only `disc://` / `iso://` qualify).
|
||||||
|
DirSourceUnsupported,
|
||||||
|
/// The `dir://` target directory is non-empty and `--force` was not
|
||||||
|
/// given. Mixing two discs' trees is refused by default.
|
||||||
|
DirNotEmpty,
|
||||||
|
/// The `dir://` target filesystem's free space is below the sum of
|
||||||
|
/// the file extents to extract. Carries required / available bytes.
|
||||||
|
DirInsufficientSpace {
|
||||||
|
required: u64,
|
||||||
|
available: u64,
|
||||||
|
},
|
||||||
|
/// Two distinct disc paths sanitize to the same host path. Surfaced
|
||||||
|
/// as a hard error rather than a silent overwrite. Carries the
|
||||||
|
/// colliding host component.
|
||||||
|
DirNameCollision {
|
||||||
|
host: String,
|
||||||
|
},
|
||||||
|
/// A `dir://` create_dir_all / file write / rename failed. Carries
|
||||||
|
/// the underlying errno when present.
|
||||||
|
DirWriteFailed {
|
||||||
|
errno: Option<i32>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
@@ -546,6 +593,13 @@ impl Error {
|
|||||||
Error::ExtentNotUnitAligned => E_EXTENT_NOT_UNIT_ALIGNED,
|
Error::ExtentNotUnitAligned => E_EXTENT_NOT_UNIT_ALIGNED,
|
||||||
Error::M2tsPacketMalformed => E_M2TS_PACKET_MALFORMED,
|
Error::M2tsPacketMalformed => E_M2TS_PACKET_MALFORMED,
|
||||||
Error::DiscCapacityMalformed => E_DISC_CAPACITY_MALFORMED,
|
Error::DiscCapacityMalformed => E_DISC_CAPACITY_MALFORMED,
|
||||||
|
Error::DirRawRejected => E_DIR_RAW_REJECTED,
|
||||||
|
Error::DirMultipassRejected => E_DIR_MULTIPASS_REJECTED,
|
||||||
|
Error::DirSourceUnsupported => E_DIR_SOURCE_UNSUPPORTED,
|
||||||
|
Error::DirNotEmpty => E_DIR_NOT_EMPTY,
|
||||||
|
Error::DirInsufficientSpace { .. } => E_DIR_INSUFFICIENT_SPACE,
|
||||||
|
Error::DirNameCollision { .. } => E_DIR_NAME_COLLISION,
|
||||||
|
Error::DirWriteFailed { .. } => E_DIR_WRITE_FAILED,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -761,6 +815,16 @@ impl From<Error> for std::io::Error {
|
|||||||
// 9047 DiscCapacityMalformed: the drive returned an unusable
|
// 9047 DiscCapacityMalformed: the drive returned an unusable
|
||||||
// READ CAPACITY response (short transfer / overflow).
|
// READ CAPACITY response (short transfer / overflow).
|
||||||
9047 => std::io::ErrorKind::InvalidData,
|
9047 => std::io::ErrorKind::InvalidData,
|
||||||
|
// dir:// usage / footgun gates (9019, 9024–9026, 9028): the caller
|
||||||
|
// gave an invalid flag/source/name combination — InvalidInput.
|
||||||
|
E_DIR_RAW_REJECTED
|
||||||
|
| E_DIR_MULTIPASS_REJECTED
|
||||||
|
| E_DIR_SOURCE_UNSUPPORTED
|
||||||
|
| E_DIR_NOT_EMPTY
|
||||||
|
| E_DIR_NAME_COLLISION => std::io::ErrorKind::InvalidInput,
|
||||||
|
// 9027 insufficient space / 9029 write failed: a filesystem-level
|
||||||
|
// failure, not bad input.
|
||||||
|
E_DIR_INSUFFICIENT_SPACE | E_DIR_WRITE_FAILED => std::io::ErrorKind::Other,
|
||||||
_ => std::io::ErrorKind::Other,
|
_ => std::io::ErrorKind::Other,
|
||||||
};
|
};
|
||||||
std::io::Error::new(kind, msg)
|
std::io::Error::new(kind, msg)
|
||||||
@@ -879,6 +943,17 @@ mod tests {
|
|||||||
Error::ExtentNotUnitAligned.code(),
|
Error::ExtentNotUnitAligned.code(),
|
||||||
Error::M2tsPacketMalformed.code(),
|
Error::M2tsPacketMalformed.code(),
|
||||||
Error::DiscCapacityMalformed.code(),
|
Error::DiscCapacityMalformed.code(),
|
||||||
|
Error::DirRawRejected.code(),
|
||||||
|
Error::DirMultipassRejected.code(),
|
||||||
|
Error::DirSourceUnsupported.code(),
|
||||||
|
Error::DirNotEmpty.code(),
|
||||||
|
Error::DirInsufficientSpace {
|
||||||
|
required: 1,
|
||||||
|
available: 0,
|
||||||
|
}
|
||||||
|
.code(),
|
||||||
|
Error::DirNameCollision { host: "x".into() }.code(),
|
||||||
|
Error::DirWriteFailed { errno: Some(28) }.code(),
|
||||||
];
|
];
|
||||||
let mut sorted = codes.to_vec();
|
let mut sorted = codes.to_vec();
|
||||||
sorted.sort();
|
sorted.sort();
|
||||||
|
|||||||
+4
-3
@@ -190,9 +190,10 @@ pub use decrypt::{DecryptKeys, decrypt_sectors, decrypt_threads, set_decrypt_thr
|
|||||||
// prefix at the crate root to keep both addressable.
|
// prefix at the crate root to keep both addressable.
|
||||||
pub use disc::{
|
pub use disc::{
|
||||||
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, DamageSeverity,
|
AacsState, AudioChannels, AudioStream, Clip, Codec, ColorSpace, ContentFormat, DamageSeverity,
|
||||||
Disc, DiscFormat, DiscId, DiscTitle, DriveCredentials, Extent, FrameRate, HdrFormat, Key,
|
Disc, DiscFormat, DiscId, DiscTitle, DriveCredentials, Extent, ExtractOptions, ExtractResult,
|
||||||
KeyOrigin, LabelPurpose, LabelQualifier, PatchOptions, PatchOutcome, Resolution, SampleRate,
|
FileResult, FrameRate, HdrFormat, Key, KeyOrigin, LabelPurpose, LabelQualifier, PatchOptions,
|
||||||
ScanOptions, Stream, SubtitleStream, SweepOptions, VideoStream, classify_damage,
|
PatchOutcome, Resolution, SampleRate, ScanOptions, Stream, SubtitleStream, SweepOptions,
|
||||||
|
VideoStream, classify_damage,
|
||||||
};
|
};
|
||||||
pub use keysource::{DiscInputs, KeySource, read_encrypted_units, resolve_and_apply};
|
pub use keysource::{DiscInputs, KeySource, read_encrypted_units, resolve_and_apply};
|
||||||
|
|
||||||
|
|||||||
+57
-3
@@ -49,6 +49,10 @@ pub enum StreamUrl {
|
|||||||
Stdio,
|
Stdio,
|
||||||
/// ISO disc image file.
|
/// ISO disc image file.
|
||||||
Iso { path: PathBuf },
|
Iso { path: PathBuf },
|
||||||
|
/// Decrypted file-tree output directory (`dir://`). A sink that writes
|
||||||
|
/// per-file decrypted bytes (not muxed PES frames), so it never flows
|
||||||
|
/// through `output()`; the CLI routes a `Dir` dest to `Disc::extract_tree`.
|
||||||
|
Dir { path: PathBuf },
|
||||||
/// Null sink (write-only, discards data).
|
/// Null sink (write-only, discards data).
|
||||||
Null,
|
Null,
|
||||||
/// Unrecognized URL.
|
/// Unrecognized URL.
|
||||||
@@ -65,6 +69,7 @@ impl StreamUrl {
|
|||||||
StreamUrl::Network { .. } => "network",
|
StreamUrl::Network { .. } => "network",
|
||||||
StreamUrl::Stdio => "stdio",
|
StreamUrl::Stdio => "stdio",
|
||||||
StreamUrl::Iso { .. } => "iso",
|
StreamUrl::Iso { .. } => "iso",
|
||||||
|
StreamUrl::Dir { .. } => "dir",
|
||||||
StreamUrl::Null => "null",
|
StreamUrl::Null => "null",
|
||||||
StreamUrl::Unknown { .. } => "unknown",
|
StreamUrl::Unknown { .. } => "unknown",
|
||||||
}
|
}
|
||||||
@@ -75,9 +80,10 @@ impl StreamUrl {
|
|||||||
match self {
|
match self {
|
||||||
StreamUrl::Disc { device: Some(p) } => p.to_str().unwrap_or(""),
|
StreamUrl::Disc { device: Some(p) } => p.to_str().unwrap_or(""),
|
||||||
StreamUrl::Disc { device: None } => "",
|
StreamUrl::Disc { device: None } => "",
|
||||||
StreamUrl::M2ts { path } | StreamUrl::Mkv { path } | StreamUrl::Iso { path } => {
|
StreamUrl::M2ts { path }
|
||||||
path.to_str().unwrap_or("")
|
| StreamUrl::Mkv { path }
|
||||||
}
|
| StreamUrl::Iso { path }
|
||||||
|
| StreamUrl::Dir { path } => path.to_str().unwrap_or(""),
|
||||||
StreamUrl::Network { addr } => addr,
|
StreamUrl::Network { addr } => addr,
|
||||||
StreamUrl::Stdio | StreamUrl::Null => "",
|
StreamUrl::Stdio | StreamUrl::Null => "",
|
||||||
StreamUrl::Unknown { raw } => raw,
|
StreamUrl::Unknown { raw } => raw,
|
||||||
@@ -140,6 +146,11 @@ pub fn parse_url(url: &str) -> StreamUrl {
|
|||||||
path: PathBuf::from(rest),
|
path: PathBuf::from(rest),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = url.strip_prefix("dir://") {
|
||||||
|
return StreamUrl::Dir {
|
||||||
|
path: PathBuf::from(rest),
|
||||||
|
};
|
||||||
|
}
|
||||||
StreamUrl::Unknown {
|
StreamUrl::Unknown {
|
||||||
raw: url.to_string(),
|
raw: url.to_string(),
|
||||||
}
|
}
|
||||||
@@ -370,6 +381,9 @@ pub fn input(url: &str, opts: &InputOptions) -> io::Result<Box<dyn crate::pes::S
|
|||||||
Ok(Box::new(NetworkStream::listen(addr)?))
|
Ok(Box::new(NetworkStream::listen(addr)?))
|
||||||
}
|
}
|
||||||
StreamUrl::Stdio => Ok(Box::new(StdioStream::input())),
|
StreamUrl::Stdio => Ok(Box::new(StdioStream::input())),
|
||||||
|
// `dir://` is an output-only sink (decrypted file tree); it is never a
|
||||||
|
// PES source. Mirror `null://` → write-only.
|
||||||
|
StreamUrl::Dir { .. } => Err(crate::error::Error::StreamWriteOnly.into()),
|
||||||
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 { url: raw.clone() }.into())
|
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||||
@@ -423,6 +437,11 @@ pub fn output(
|
|||||||
StreamUrl::Null => Ok(Box::new(NullStream::new(title))),
|
StreamUrl::Null => Ok(Box::new(NullStream::new(title))),
|
||||||
StreamUrl::Disc { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
StreamUrl::Disc { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
StreamUrl::Iso { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
StreamUrl::Iso { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
|
// `dir://` is NOT a PES sink — it writes raw decrypted files, not muxed
|
||||||
|
// frames. A stray `dir://` routed into the mux/PES path fails loudly,
|
||||||
|
// exactly the category the crate already rejects for `iso://`. The CLI
|
||||||
|
// routes a `dir://` dest to `Disc::extract_tree` before reaching here.
|
||||||
|
StreamUrl::Dir { .. } => Err(crate::error::Error::StreamReadOnly.into()),
|
||||||
StreamUrl::Unknown { ref raw } => {
|
StreamUrl::Unknown { ref raw } => {
|
||||||
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
Err(crate::error::Error::StreamUrlInvalid { url: raw.clone() }.into())
|
||||||
}
|
}
|
||||||
@@ -814,6 +833,41 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `dir://PATH/` parses to `StreamUrl::Dir` with the raw remainder as the
|
||||||
|
/// path; it is a SINK (not a disc source), so `is_disc_source()` is false.
|
||||||
|
#[test]
|
||||||
|
fn parse_dir_url_is_sink_not_disc_source() {
|
||||||
|
match parse_url("dir://out/movie/") {
|
||||||
|
StreamUrl::Dir { path } => {
|
||||||
|
assert_eq!(path, PathBuf::from("out/movie/"));
|
||||||
|
}
|
||||||
|
other => panic!("dir:// must parse to Dir, got {other:?}"),
|
||||||
|
}
|
||||||
|
assert_eq!(parse_url("dir://x").scheme(), "dir");
|
||||||
|
assert_eq!(parse_url("dir://x/y").path_str(), "x/y");
|
||||||
|
assert!(
|
||||||
|
!parse_url("dir://x").is_disc_source(),
|
||||||
|
"dir:// is a sink, never a disc source"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `dir://` is output-only: `input()` rejects it (StreamWriteOnly →
|
||||||
|
/// Unsupported), and `output()` rejects it too (StreamReadOnly →
|
||||||
|
/// Unsupported) because it is NOT a PES sink — the CLI routes it to
|
||||||
|
/// `Disc::extract_tree` before the mux path.
|
||||||
|
#[test]
|
||||||
|
fn dir_url_is_not_a_pes_stream_either_direction() {
|
||||||
|
assert_eq!(
|
||||||
|
input_err_kind("dir://out/"),
|
||||||
|
std::io::ErrorKind::Unsupported
|
||||||
|
);
|
||||||
|
let t = DiscTitle::empty();
|
||||||
|
assert_eq!(
|
||||||
|
output_err_kind("dir://out/", &t),
|
||||||
|
std::io::ErrorKind::Unsupported
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// output() to network:// with no port must fail validation
|
/// output() to network:// with no port must fail validation
|
||||||
/// (StreamUrlMissingPort, E9004 → InvalidInput) before any TcpStream.
|
/// (StreamUrlMissingPort, E9004 → InvalidInput) before any TcpStream.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+40
@@ -652,6 +652,46 @@ impl UdfFs {
|
|||||||
Ok(extents)
|
Ok(extents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If the ICB at `meta_lba` stores its data inline (embedded, AD type 3),
|
||||||
|
/// return the embedded bytes; `Ok(None)` for the normal extent-backed case.
|
||||||
|
/// Public wrapper over [`read_inline_data`](Self::read_inline_data) so the
|
||||||
|
/// per-file tree extractor can honor inline nav files without re-walking a
|
||||||
|
/// path. The caller trims to the entry's declared `size`.
|
||||||
|
pub fn inline_data_at(
|
||||||
|
&self,
|
||||||
|
reader: &mut dyn SectorSource,
|
||||||
|
meta_lba: u32,
|
||||||
|
) -> Result<Option<Vec<u8>>> {
|
||||||
|
self.read_inline_data(reader, meta_lba)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absolute disc extents `(absolute_lba, byte_length)` for the ICB at
|
||||||
|
/// `meta_lba`. Like [`file_extents`](Self::file_extents) but keyed by ICB
|
||||||
|
/// LBA (so the tree extractor can resolve a `DirEntry` it already holds
|
||||||
|
/// without re-navigating a path) and preserving the per-extent byte length
|
||||||
|
/// (so the last sector can be trimmed to the file's real size). Resolves
|
||||||
|
/// multi-extent / Long-AD / continuation ICBs.
|
||||||
|
pub fn extents_abs_at(
|
||||||
|
&self,
|
||||||
|
reader: &mut dyn SectorSource,
|
||||||
|
meta_lba: u32,
|
||||||
|
) -> Result<Vec<(u32, u32)>> {
|
||||||
|
let alloc = self.read_icb_extents(reader, meta_lba)?;
|
||||||
|
let mut out = Vec::with_capacity(alloc.len());
|
||||||
|
for (lba, byte_len) in alloc {
|
||||||
|
let abs = self
|
||||||
|
.partition_start
|
||||||
|
.checked_add(lba)
|
||||||
|
.ok_or(Error::DiscRead {
|
||||||
|
sector: self.partition_start as u64,
|
||||||
|
status: None,
|
||||||
|
sense: None,
|
||||||
|
})?;
|
||||||
|
out.push((abs, byte_len));
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
/// Get all absolute disc sector extents for a file.
|
/// Get all absolute disc sector extents for a file.
|
||||||
/// Returns Vec of (absolute_lba, sector_count) covering the entire file.
|
/// Returns Vec of (absolute_lba, sector_count) covering the entire file.
|
||||||
pub fn file_extents(
|
pub fn file_extents(
|
||||||
|
|||||||
Reference in New Issue
Block a user