use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::super::{Read, Seek, SeekFrom, Write};
use hadris_common::types::extent::{Extent, FileType};
use hadris_common::types::layout::{AllocationMap, DirectoryLayout, FileLayout};
use hadris_io as io;
use super::descriptor::AnchorVolumeDescriptorPointer;
use crate::{AVDP_LOCATION, SECTOR_SIZE, UdfError, UdfRevision};
#[derive(Debug, Clone)]
pub enum ModifyOp {
AppendFile {
path: String,
data: FileData,
},
CreateDir {
path: String,
},
Delete {
path: String,
},
Replace {
path: String,
data: FileData,
},
}
#[derive(Debug, Clone)]
pub enum FileData {
Buffer(Vec<u8>),
#[cfg(feature = "std")]
Path(std::path::PathBuf),
}
impl From<Vec<u8>> for FileData {
fn from(data: Vec<u8>) -> Self {
FileData::Buffer(data)
}
}
impl From<&[u8]> for FileData {
fn from(data: &[u8]) -> Self {
FileData::Buffer(data.to_vec())
}
}
#[cfg(feature = "std")]
impl From<std::path::PathBuf> for FileData {
fn from(path: std::path::PathBuf) -> Self {
FileData::Path(path)
}
}
impl FileData {
pub fn size(&self) -> io::Result<u64> {
match self {
FileData::Buffer(data) => Ok(data.len() as u64),
#[cfg(feature = "std")]
FileData::Path(path) => {
let metadata = std::fs::metadata(path)?;
Ok(metadata.len())
}
}
}
pub fn read_all(&self) -> io::Result<Vec<u8>> {
match self {
FileData::Buffer(data) => Ok(data.clone()),
#[cfg(feature = "std")]
FileData::Path(path) => std::fs::read(path),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum UdfModifyError {
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Udf(#[from] UdfError),
#[error("file not found: {0}")]
FileNotFound(String),
#[error("path already exists: {0}")]
PathExists(String),
#[error("not enough space to allocate {0} bytes")]
NotEnoughSpace(u64),
#[error("invalid path: {0}")]
InvalidPath(String),
}
pub type UdfModifyResult<T> = Result<T, UdfModifyError>;
#[derive(Debug, Clone, Default)]
pub struct UdfModifyOptions {
pub volume_name: Option<String>,
}
pub struct UdfModifier<RW: Read + Write + Seek> {
inner: RW,
existing_layout: DirectoryLayout,
#[allow(dead_code)]
allocation_map: AllocationMap,
pending_ops: Vec<ModifyOp>,
#[allow(dead_code)]
options: UdfModifyOptions,
#[allow(dead_code)]
revision: UdfRevision,
#[allow(dead_code)]
partition_start: u32,
#[allow(dead_code)]
partition_length: u32,
#[allow(dead_code)]
next_unique_id: u64,
end_sector: u32,
}
impl<RW: Read + Write + Seek> UdfModifier<RW> {
pub fn open(inner: RW) -> UdfModifyResult<Self> {
Self::open_with_options(inner, UdfModifyOptions::default())
}
pub fn open_with_options(mut inner: RW, options: UdfModifyOptions) -> UdfModifyResult<Self> {
inner.seek(SeekFrom::Start(AVDP_LOCATION as u64 * SECTOR_SIZE as u64))?;
let mut avdp_buf = [0u8; SECTOR_SIZE];
inner.read_exact(&mut avdp_buf)?;
let avdp: &AnchorVolumeDescriptorPointer = bytemuck::from_bytes(&avdp_buf[..512]);
let _vds_location = avdp.main_vds_extent.location;
let partition_start = 270u32;
let partition_length = 1000u32;
let existing_layout = DirectoryLayout::root();
let total_sectors = partition_start + partition_length;
let allocation_map = AllocationMap::new(total_sectors);
let end_sector = partition_start + partition_length;
Ok(Self {
inner,
existing_layout,
allocation_map,
pending_ops: Vec::new(),
options,
revision: UdfRevision::V1_02,
partition_start,
partition_length,
next_unique_id: 16,
end_sector,
})
}
pub fn queue(&mut self, op: ModifyOp) {
self.pending_ops.push(op);
}
pub fn append_file(&mut self, path: &str, data: impl Into<FileData>) {
self.queue(ModifyOp::AppendFile {
path: path.to_string(),
data: data.into(),
});
}
pub fn create_dir(&mut self, path: &str) {
self.queue(ModifyOp::CreateDir {
path: path.to_string(),
});
}
pub fn delete(&mut self, path: &str) {
self.queue(ModifyOp::Delete {
path: path.to_string(),
});
}
pub fn replace(&mut self, path: &str, data: impl Into<FileData>) {
self.queue(ModifyOp::Replace {
path: path.to_string(),
data: data.into(),
});
}
pub fn layout(&self) -> &DirectoryLayout {
&self.existing_layout
}
pub fn commit(mut self) -> UdfModifyResult<()> {
if self.pending_ops.is_empty() {
return Ok(());
}
let new_layout = self.apply_ops()?;
let file_extents = self.write_new_data(&new_layout)?;
self.write_new_metadata(&new_layout, file_extents)?;
Ok(())
}
fn apply_ops(&mut self) -> UdfModifyResult<DirectoryLayout> {
let mut layout = self.existing_layout.clone();
for op in &self.pending_ops {
match op {
ModifyOp::AppendFile { path, data } => {
if layout.find_file(path).is_some() {
return Err(UdfModifyError::PathExists(path.clone()));
}
let (dir_path, filename) = Self::split_path(path)?;
let dir = if dir_path.is_empty() {
&mut layout
} else {
layout.get_or_create_dir(&dir_path)
};
let size = data.size()?;
let file = FileLayout::new(filename, Extent::new(0, size))
.with_type(FileType::RegularFile);
dir.add_file(file);
}
ModifyOp::CreateDir { path } => {
layout.get_or_create_dir(path);
}
ModifyOp::Delete { path } => {
if layout.remove_file(path).is_none() {
return Err(UdfModifyError::FileNotFound(path.clone()));
}
}
ModifyOp::Replace { path, data } => {
let file = layout
.find_file_mut(path)
.ok_or_else(|| UdfModifyError::FileNotFound(path.clone()))?;
let size = data.size()?;
file.extent = Extent::new(0, size);
}
}
}
Ok(layout)
}
fn write_new_data(
&mut self,
_layout: &DirectoryLayout,
) -> UdfModifyResult<BTreeMap<String, Extent>> {
let mut file_extents = BTreeMap::new();
let mut current_sector = self.end_sector;
for op in &self.pending_ops {
match op {
ModifyOp::AppendFile { path, data } | ModifyOp::Replace { path, data } => {
let size = data.size()?;
if size == 0 {
file_extents.insert(path.clone(), Extent::new(0, 0));
continue;
}
let extent = Extent::new(current_sector, size);
file_extents.insert(path.clone(), extent);
self.inner
.seek(SeekFrom::Start(current_sector as u64 * SECTOR_SIZE as u64))?;
let content = data.read_all()?;
self.inner.write_all(&content)?;
current_sector += extent.sector_count(SECTOR_SIZE as u32);
}
_ => {}
}
}
let pos = self.inner.stream_position()?;
let remainder = pos % SECTOR_SIZE as u64;
if remainder != 0 {
let padding = SECTOR_SIZE as u64 - remainder;
let zeros = alloc::vec![0u8; padding as usize];
self.inner.write_all(&zeros)?;
}
self.end_sector = current_sector;
Ok(file_extents)
}
fn write_new_metadata(
&mut self,
_layout: &DirectoryLayout,
_file_extents: BTreeMap<String, Extent>,
) -> UdfModifyResult<()> {
Ok(())
}
fn split_path(path: &str) -> UdfModifyResult<(String, String)> {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return Err(UdfModifyError::InvalidPath(path.to_string()));
}
let filename = parts.last().unwrap().to_string();
let dir_path = if parts.len() > 1 {
parts[..parts.len() - 1].join("/")
} else {
String::new()
};
Ok((dir_path, filename))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_split_path() {
let (dir, file) = UdfModifier::<std::io::Cursor<Vec<u8>>>::split_path("test.txt").unwrap();
assert_eq!(dir, "");
assert_eq!(file, "test.txt");
let (dir, file) =
UdfModifier::<std::io::Cursor<Vec<u8>>>::split_path("docs/readme.txt").unwrap();
assert_eq!(dir, "docs");
assert_eq!(file, "readme.txt");
}
#[test]
fn test_file_data() {
let data = FileData::from(vec![1, 2, 3, 4]);
assert_eq!(data.size().unwrap(), 4);
assert_eq!(data.read_all().unwrap(), vec![1, 2, 3, 4]);
}
}