use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::error::{OpticaldiscsError, Result};
use crate::sector_reader::{SectorReader, SECTOR_SIZE};
pub struct PhysicalDisc {
device: File,
device_path: PathBuf,
size_bytes: Option<u64>,
}
impl PhysicalDisc {
pub fn open(device_path: impl AsRef<Path>) -> Result<Self> {
let requested = device_path.as_ref();
let resolved = resolve_device_path(requested);
let device = File::open(&resolved).map_err(OpticaldiscsError::Io)?;
let size_bytes = probe_size(&device);
Ok(Self {
device,
device_path: resolved,
size_bytes,
})
}
pub fn device_path(&self) -> &Path {
&self.device_path
}
pub fn size_bytes(&self) -> Option<u64> {
self.size_bytes
}
pub fn sector_count(&self) -> Option<u64> {
self.size_bytes.map(|b| b / SECTOR_SIZE)
}
pub fn read_sectors(&mut self, lba: u64, count: u32) -> Result<Vec<u8>> {
if count == 0 {
return Ok(Vec::new());
}
let offset = lba
.checked_mul(SECTOR_SIZE)
.ok_or_else(|| OpticaldiscsError::InvalidData(format!("sector {lba} out of range")))?;
self.device
.seek(SeekFrom::Start(offset))
.map_err(OpticaldiscsError::Io)?;
let len = (count as usize) * (SECTOR_SIZE as usize);
let mut buf = vec![0u8; len];
self.device
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
impl SectorReader for PhysicalDisc {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
self.read_sectors(lba, 1)
}
fn read_bytes(&mut self, byte_offset: u64, length: usize) -> Result<Vec<u8>> {
if length == 0 {
return Ok(Vec::new());
}
let first = byte_offset / SECTOR_SIZE;
let skip = (byte_offset % SECTOR_SIZE) as usize;
let span = skip + length;
let sectors = span.div_ceil(SECTOR_SIZE as usize);
let count = u32::try_from(sectors).map_err(|_| {
OpticaldiscsError::InvalidData(format!("{length}-byte read is too large"))
})?;
let mut buf = self.read_sectors(first, count)?;
buf.drain(..skip);
buf.truncate(length);
Ok(buf)
}
}
fn resolve_device_path(path: &Path) -> PathBuf {
#[cfg(target_os = "macos")]
{
if let Some(s) = path.to_str() {
if let Some(rest) = s.strip_prefix("/dev/disk") {
return PathBuf::from(format!("/dev/rdisk{rest}"));
}
}
}
path.to_path_buf()
}
fn probe_size(device: &File) -> Option<u64> {
if let Ok(m) = device.metadata() {
if m.len() > 0 {
return Some(m.len());
}
}
if let Some(size) = platform_size(device) {
return Some(size);
}
let mut probe = device.try_clone().ok()?;
let end = probe.seek(SeekFrom::End(0)).ok()?;
(end > 0).then_some(end)
}
#[cfg(target_os = "macos")]
fn platform_size(device: &File) -> Option<u64> {
use std::os::unix::io::AsRawFd;
const DKIOCGETBLOCKSIZE: libc::c_ulong = 0x4004_6418;
const DKIOCGETBLOCKCOUNT: libc::c_ulong = 0x4008_6419;
let fd = device.as_raw_fd();
let mut block_size: u32 = 0;
let mut block_count: u64 = 0;
unsafe {
if libc::ioctl(fd, DKIOCGETBLOCKSIZE, &mut block_size) < 0 {
return None;
}
if libc::ioctl(fd, DKIOCGETBLOCKCOUNT, &mut block_count) < 0 {
return None;
}
}
block_count.checked_mul(u64::from(block_size))
}
#[cfg(target_os = "linux")]
fn platform_size(device: &File) -> Option<u64> {
use std::os::unix::io::AsRawFd;
const BLKGETSIZE64: libc::c_ulong = 0x8008_1272;
let fd = device.as_raw_fd();
let mut size: u64 = 0;
unsafe {
if libc::ioctl(fd, BLKGETSIZE64, &mut size) < 0 {
return None;
}
}
(size > 0).then_some(size)
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn platform_size(_device: &File) -> Option<u64> {
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opening_a_missing_device_is_an_error_not_a_panic() {
assert!(PhysicalDisc::open("/dev/definitely-not-a-drive").is_err());
}
#[cfg(target_os = "macos")]
#[test]
fn macos_maps_block_device_to_raw_node() {
assert_eq!(
resolve_device_path(Path::new("/dev/disk6")),
PathBuf::from("/dev/rdisk6")
);
assert_eq!(
resolve_device_path(Path::new("/dev/rdisk6")),
PathBuf::from("/dev/rdisk6")
);
}
#[cfg(not(target_os = "macos"))]
#[test]
fn other_platforms_open_the_path_as_given() {
assert_eq!(
resolve_device_path(Path::new("/dev/sr0")),
PathBuf::from("/dev/sr0")
);
}
#[test]
fn reads_a_real_disc_when_one_is_present() {
let Some(drive) = crate::drives::list_drives()
.into_iter()
.find(|d| d.is_loaded && !d.device_path.as_os_str().is_empty())
else {
eprintln!("no loaded optical drive present — skipping");
return;
};
let mut disc = match PhysicalDisc::open(&drive.device_path) {
Ok(d) => d,
Err(e) => {
eprintln!(
"could not open {}: {e} — skipping",
drive.device_path.display()
);
return;
}
};
let pvd = disc.read_sector(16).expect("read PVD sector");
assert_eq!(pvd.len(), SECTOR_SIZE as usize);
eprintln!(
"{}: size={:?} PVD id={:?}",
drive.display_name,
disc.size_bytes(),
String::from_utf8_lossy(&pvd[1..6])
);
}
#[test]
fn probes_disc_info_on_a_real_drive() {
let Some(drive) = crate::drives::list_drives()
.into_iter()
.find(|d| d.is_loaded && !d.device_path.as_os_str().is_empty())
else {
eprintln!("no loaded optical drive present — skipping");
return;
};
match crate::detect::DiscImageInfo::open_physical(&drive.device_path) {
Ok(info) => eprintln!(
"{}: format={:?} fs={:?} volume={:?}",
drive.display_name, info.format, info.filesystem, info.volume_label
),
Err(e) => panic!("open_physical failed on {}: {e}", drive.display_name),
}
}
#[test]
fn browses_a_real_disc_when_one_is_present() {
use crate::browse::open_physical_filesystem;
let Some(drive) = crate::drives::list_drives()
.into_iter()
.find(|d| d.is_loaded && !d.device_path.as_os_str().is_empty())
else {
eprintln!("no loaded optical drive present — skipping");
return;
};
let mut fs = match open_physical_filesystem(&drive.device_path) {
Ok(fs) => fs,
Err(e) => {
eprintln!("could not browse {}: {e} — skipping", drive.display_name);
return;
}
};
let root = fs.root().expect("read disc root");
let entries = fs.list_directory(&root).expect("list disc root");
eprintln!(
"{}: volume={:?}, root name={:?}, {} entries",
drive.display_name,
fs.volume_name(),
root.name,
entries.len()
);
for e in entries.iter().take(10) {
eprintln!(" {} dir={} size={}", e.name, e.is_directory(), e.size);
}
for dir in entries.iter().filter(|e| e.is_directory()) {
match fs.list_directory(dir) {
Ok(children) => {
eprintln!(" {}/ -> {} entries", dir.name, children.len());
for c in children.iter().take(6) {
eprintln!(" {} dir={} size={}", c.name, c.is_directory(), c.size);
}
}
Err(e) => eprintln!(" {}/ -> ERROR: {e}", dir.name),
}
}
}
}