diff --git a/Cargo.toml b/Cargo.toml index 59afab8..b7a9315 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "dcb-tools/dcb-extractor", "dcb-tools/dcb-decompiler", "dcb-tools/drv-extractor", + "dcb-tools/drv-packer", "dcb-tools/pak-extractor", "dcb-tools/model-set-extractor", ] diff --git a/dcb-io/src/drv.rs b/dcb-io/src/drv.rs index cabb8ee..7e43f44 100644 --- a/dcb-io/src/drv.rs +++ b/dcb-io/src/drv.rs @@ -6,7 +6,7 @@ pub mod error; pub mod file; // Exports -pub use dir::{DirEntryReader, DirEntryWriter, DirReader, DirWriter}; +pub use dir::{DirEntryReader, DirEntryWriter, DirReader, DirWriter, DirWriterList}; pub use error::{FromReaderError, ToWriterError}; pub use file::{FileReader, FileWriter}; @@ -30,11 +30,11 @@ pub struct DrvFsWriter; impl DrvFsWriter { /// Creates a `.DRV` filesystem - pub fn write_fs, io::Error>>>( - writer: &mut W, root_entries: I, - ) -> Result<(), ToWriterError> { + pub fn write_fs( + writer: &mut W, root_entries: L, root_entries_len: u32, + ) -> Result<(), ToWriterError> { // Get the root and write it - let root = DirWriter::new(root_entries); + let root = DirWriter::new(root_entries, root_entries_len); root.write_entries(writer).map_err(ToWriterError::RootDir)?; Ok(()) diff --git a/dcb-io/src/drv/dir.rs b/dcb-io/src/drv/dir.rs index f2d6bed..3ff4ee8 100644 --- a/dcb-io/src/drv/dir.rs +++ b/dcb-io/src/drv/dir.rs @@ -68,72 +68,106 @@ impl DirReader { } } -/// Directory writer -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub struct DirWriter, io::Error>>> { - /// Iterator over all entries - entries: I, +/// Directory list +pub trait DirWriterList: Sized + std::fmt::Debug { + /// Reader used for the files in this directory + type FileReader: std::fmt::Debug + io::Read; + + /// Directory lister + type DirList: DirWriterList; + + /// Error type for each entry + type Error: std::error::Error + 'static; + + /// Iterator + type Iter: Iterator, Self::Error>>; + + /// Converts this list into an iterator + fn into_iter(self) -> Self::Iter; } -impl, io::Error>>> DirWriter { +/// Directory writer +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub struct DirWriter { + /// Writer list + entries: L, + + /// Number of entries + entries_len: u32, +} + +impl DirWriter { /// Creates a new directory writer. - pub fn new(entries: I) -> Self { - Self { entries } + pub fn new(entries: L, entries_len: u32) -> Self { + Self { entries, entries_len } } /// Returns the number of entries pub fn entries_len(&self) -> u32 { - u32::try_from(self.entries.len()).expect("Too many entries") + self.entries_len + } + + /// Returns this directory's size + pub fn size(&self) -> u32 { + // Note: `+1` for the terminator + (self.entries_len() + 1) * 0x20 } /// Writes all entries into a writer - pub fn write_entries(self, writer: &mut W) -> Result<(), WriteEntriesError> { + /// + /// Returns the number of sectors written by this directory + pub fn write_entries(self, writer: &mut W) -> Result> { // Get the sector we're currently on - let sector_pos = writer.stream_position().map_err(WriteEntriesError::GetPos)? / 2048; - let sector_pos = u32::try_from(sector_pos).expect("`.DRV` file is too big"); - - // Get the starting sector pos for each entry - // Note: We start right after this directory - let start_sector_pos = sector_pos + (self.entries_len() * 0x20 + 2047) / 2048; - - // Get all the entries with their sector positions - let entries = self - .entries - .scan(start_sector_pos, |cur_sector_pos, res| match res { - Ok(entry) => { - let sector_pos = *cur_sector_pos; - *cur_sector_pos += (entry.size() + 2047) / 2048; - Some(Ok((entry, sector_pos))) - }, - Err(err) => Some(Err(err)), - }) - .collect::, _>>() - .map_err(WriteEntriesError::GetEntry)?; - - // Write each entry in the directory - for (entry, sector_pos) in &entries { - // Write the bytes - let mut entry_bytes = [0; 0x20]; - entry.to_bytes(&mut entry_bytes, *sector_pos); - - // And write them - writer.write_all(&entry_bytes).map_err(WriteEntriesError::WriteEntryInDir)?; + let start_pos = writer.stream_position().map_err(WriteEntriesError::GetPos)?; + if start_pos % 2048 != 0 { + return Err(WriteEntriesError::WriterAtSectorStart); } + let start_sector_pos = u32::try_from(start_pos / 2048).map_err(|_err| WriteEntriesError::WriterSectorPastMax)?; + + // Get the starting sector position for the first entry. + // Note: We start right after this directory + // Note: `+2047` is to pad this directory to the next sector, if not empty. + let mut cur_sector_pos = start_sector_pos + (self.size() + 2047) / 2048; + + // Our directory to write after writing all entries + let mut dir_bytes = vec![]; + + // For each entry, write it and add it to our directory bytes + for entry in self.entries.into_iter() { + // Get the entry + let entry = entry.map_err(WriteEntriesError::GetEntry)?; + + // Write the entry on our directory + let mut entry_bytes = [0; 0x20]; + entry.to_bytes(&mut entry_bytes, cur_sector_pos); + dir_bytes.extend_from_slice(&entry_bytes); - // Then write each entry - for (entry, sector_pos) in entries { // Seek to the entry writer - .seek(SeekFrom::Start(u64::from(sector_pos) * 2048)) + .seek(SeekFrom::Start(u64::from(cur_sector_pos) * 2048)) .map_err(WriteEntriesError::SeekToEntry)?; - // Write the entry - match entry.into_kind() { - DirEntryWriterKind::File(file) => file.into_writer(writer).map_err(WriteEntriesError::WriteFile)?, + // Write the entry on the file + let sector_size = match entry.into_kind() { + DirEntryWriterKind::File(file) => { + let size = file.size(); + file.write(writer).map_err(WriteEntriesError::WriteFile)?; + (size + 2047) / 2048 + }, DirEntryWriterKind::Dir(dir) => dir.write_entries(writer).map_err(|err| WriteEntriesError::WriteDir(Box::new(err)))?, - } + }; + + // Update our sector pos + cur_sector_pos += sector_size; } - Ok(()) + // Then write our directory + writer + .seek(SeekFrom::Start(u64::from(start_sector_pos) * 2048)) + .map_err(WriteEntriesError::SeekToEntries)?; + + writer.write_all(&dir_bytes).map_err(WriteEntriesError::WriteEntries)?; + + Ok(cur_sector_pos - start_sector_pos) } } diff --git a/dcb-io/src/drv/dir/entry.rs b/dcb-io/src/drv/dir/entry.rs index 6ee2bd4..df06067 100644 --- a/dcb-io/src/drv/dir/entry.rs +++ b/dcb-io/src/drv/dir/entry.rs @@ -7,12 +7,12 @@ pub mod error; pub use error::FromBytesError; // Imports -use super::{DirReader, DirWriter}; +use super::{DirReader, DirWriter, DirWriterList}; use crate::drv::{FileReader, FileWriter}; use byteorder::{ByteOrder, LittleEndian}; use chrono::NaiveDateTime; use dcb_util::{array_split, array_split_mut, ascii_str_arr::AsciiChar, AsciiStrArr}; -use std::{convert::TryFrom, io}; +use std::convert::TryFrom; /// A directory entry kind #[derive(PartialEq, Eq, Clone, Debug)] @@ -102,18 +102,18 @@ impl DirEntryReader { } /// A directory entry kind -#[derive(PartialEq, Eq, Clone, Debug)] -pub enum DirEntryWriterKind, io::Error>>> { +#[derive(Debug)] +pub enum DirEntryWriterKind { /// A file - File(FileWriter), + File(FileWriter), /// Directory - Dir(DirWriter), + Dir(DirWriter), } /// A directory entry reader -#[derive(PartialEq, Eq, Clone, Debug)] -pub struct DirEntryWriter, io::Error>>> { +#[derive(Debug)] +pub struct DirEntryWriter { /// Entry name name: AsciiStrArr<0x10>, @@ -121,12 +121,12 @@ pub struct DirEntryWriter, + kind: DirEntryWriterKind, } -impl, io::Error>>> DirEntryWriter { +impl DirEntryWriter { /// Creates a new entry writer from it's name, date and kind - pub fn new(name: AsciiStrArr<0x10>, date: NaiveDateTime, kind: DirEntryWriterKind) -> Self { + pub fn new(name: AsciiStrArr<0x10>, date: NaiveDateTime, kind: DirEntryWriterKind) -> Self { Self { name, date, kind } } @@ -134,17 +134,17 @@ impl, io::E pub fn size(&self) -> u32 { match &self.kind { DirEntryWriterKind::File(file) => file.size(), - DirEntryWriterKind::Dir(dir) => dir.entries_len() * 0x20, + DirEntryWriterKind::Dir(dir) => dir.size(), } } /// Returns this entry's kind - pub fn kind(&self) -> &DirEntryWriterKind { + pub fn kind(&self) -> &DirEntryWriterKind { &self.kind } /// Returns this entry's kind - pub fn into_kind(self) -> DirEntryWriterKind { + pub fn into_kind(self) -> DirEntryWriterKind { self.kind } @@ -171,6 +171,8 @@ impl, io::E }, DirEntryWriterKind::Dir(_) => { *bytes.kind = 0x80; + + LittleEndian::write_u32(bytes.size, 0); }, }; diff --git a/dcb-io/src/drv/dir/error.rs b/dcb-io/src/drv/dir/error.rs index f577b44..dd51ce5 100644 --- a/dcb-io/src/drv/dir/error.rs +++ b/dcb-io/src/drv/dir/error.rs @@ -26,18 +26,22 @@ pub enum ReadEntryError { /// Error for [`DirWriter::to_writer`](super::DirWriter::to_writer) #[derive(Debug, thiserror::Error)] -pub enum WriteEntriesError { +pub enum WriteEntriesError { /// Unable to get position #[error("Unable to get position")] GetPos(#[source] io::Error), + /// Writer was not at sector star + #[error("Writer was not at sector start")] + WriterAtSectorStart, + + /// Writer current sector was past max + #[error("Writer current sector was past `u32::MAX`")] + WriterSectorPastMax, + /// Unable to get entry #[error("Unable to get entry")] - GetEntry(#[source] io::Error), - - /// Unable to write entry in directory - #[error("Unable to write entry in directory")] - WriteEntryInDir(#[source] io::Error), + GetEntry(#[source] E), /// Unable to seek to entry #[error("Unable to seek to entry")] @@ -50,4 +54,12 @@ pub enum WriteEntriesError { /// Unable to write directory #[error("Unable to write directory")] WriteDir(#[source] Box), + + /// Unable to seek to entries + #[error("Unable to seek to entries")] + SeekToEntries(#[source] io::Error), + + /// Unable to write all entries + #[error("Unable to write entries")] + WriteEntries(#[source] io::Error), } diff --git a/dcb-io/src/drv/error.rs b/dcb-io/src/drv/error.rs index 72ffdc1..890d5e6 100644 --- a/dcb-io/src/drv/error.rs +++ b/dcb-io/src/drv/error.rs @@ -13,8 +13,8 @@ pub enum FromReaderError { /// Error for [`DrvFsWriter::to_writer`](super::DrvFsWriter::to_writer) #[derive(Debug, thiserror::Error)] -pub enum ToWriterError { +pub enum ToWriterError { /// Unable to write root directory #[error("Unable to write root directory")] - RootDir(#[source] dir::WriteEntriesError), + RootDir(#[source] dir::WriteEntriesError), } diff --git a/dcb-io/src/drv/file.rs b/dcb-io/src/drv/file.rs index 96eaea1..ca3f54c 100644 --- a/dcb-io/src/drv/file.rs +++ b/dcb-io/src/drv/file.rs @@ -90,8 +90,8 @@ impl FileWriter { } /// Writes this file to a writer - pub fn into_writer(mut self, writer: &mut W) -> Result<(), io::Error> { - let written = std::io::copy(&mut self.reader, writer)?; + pub fn write(self, writer: &mut W) -> Result<(), io::Error> { + let written = std::io::copy(&mut self.reader.take(u64::from(self.size)), writer)?; assert_eq!(written, u64::from(self.size)); Ok(()) } diff --git a/dcb-tools/drv-packer/Cargo.toml b/dcb-tools/drv-packer/Cargo.toml new file mode 100644 index 0000000..76ef2d7 --- /dev/null +++ b/dcb-tools/drv-packer/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "drv-packer" +version = "0.1.0" +authors = ["Filipe Rodrigues "] +edition = "2018" + +[dependencies] +# Dcb +dcb-util = { path = "../../dcb-util" } +dcb-io = { path = "../../dcb-io" } + +# Util +filetime = "0.2.14" +chrono = "0.4.19" + +# Cmd +clap = "2.33.3" + +# Logging +log = "0.4.13" +simplelog = "0.9.0" + +# Error handling +anyhow = "1.0.38" +thiserror = "1.0.23" diff --git a/dcb-tools/drv-packer/src/cli.rs b/dcb-tools/drv-packer/src/cli.rs new file mode 100644 index 0000000..f496301 --- /dev/null +++ b/dcb-tools/drv-packer/src/cli.rs @@ -0,0 +1,65 @@ +//! Cli manager + +// Imports +use clap::{App as ClapApp, Arg as ClapArg}; +use std::path::{Path, PathBuf}; + +/// Data from the command line +#[derive(PartialEq, Clone, Debug)] +pub struct CliData { + /// Input directory + pub input_dir: PathBuf, + + /// The output file + pub output_file: PathBuf, +} + +impl CliData { + /// Constructs all of the cli data given and returns it + pub fn new() -> Self { + // Get all matches from cli + let matches = ClapApp::new("Drv Packer") + .version("0.0") + .author("Filipe [...] <[...]@gmail.com>") + .about("Packs a folder into a `.drv` filesystem") + .arg( + ClapArg::with_name("INPUT_DIR") + .help("Sets the input directory to use") + .required(true) + .index(1), + ) + .arg( + ClapArg::with_name("OUTPUT") + .help("Sets the output file to use") + .short("o") + .long("output") + .takes_value(true) + .required(false), + ) + .get_matches(); + + // Get the input filename + // Note: required + let input_dir = matches + .value_of("INPUT_DIR") + .map(Path::new) + .map(Path::to_path_buf) + .expect("Unable to get required argument `INPUT_FILE`"); + + // Try to get the output, else use the input filename + `.drv` + let output_file = match matches.value_of("OUTPUT") { + Some(output) => PathBuf::from(output), + None => { + let extension = match input_dir.extension() { + Some(extension) => format!("{}.DRV", extension.to_string_lossy()), + None => ".DRV".to_string(), + }; + + input_dir.with_extension(extension) + }, + }; + + // Return the data + Self { input_dir, output_file } + } +} diff --git a/dcb-tools/drv-packer/src/logger.rs b/dcb-tools/drv-packer/src/logger.rs new file mode 100644 index 0000000..32ec61e --- /dev/null +++ b/dcb-tools/drv-packer/src/logger.rs @@ -0,0 +1,25 @@ +//! Logger + +// Imports +use log::LevelFilter; +use simplelog::{CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode, WriteLogger}; + +/// The type of logger required to pass to `CombinedLogger::init` +type BoxedLogger = Box; + +/// Initializes the global logger +pub fn init() { + // All loggers to try and initialize + let loggers = [ + Some(TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Stderr)).map(|logger| BoxedLogger::from(logger)), + std::fs::File::create("latest.log") + .ok() + .map(|file| WriteLogger::new(LevelFilter::Debug, Config::default(), file)) + .map(|logger| BoxedLogger::from(logger)), + ]; + + // Filter all logger that actually work and initialize them + if CombinedLogger::init(std::array::IntoIter::new(loggers).filter_map(std::convert::identity).collect()).is_err() { + log::warn!("Logger was already initialized"); + } +} diff --git a/dcb-tools/drv-packer/src/main.rs b/dcb-tools/drv-packer/src/main.rs new file mode 100644 index 0000000..a3449d2 --- /dev/null +++ b/dcb-tools/drv-packer/src/main.rs @@ -0,0 +1,208 @@ +//! `.DRV` packer + +// Features +#![feature(array_value_iter, try_blocks, seek_convenience)] + +// Modules +mod cli; +mod logger; + +// Imports +use anyhow::Context; +use dcb_io::drv::{dir::entry::DirEntryWriterKind, DirEntryWriter, DirWriter, DirWriterList, DrvFsWriter, FileWriter}; +use std::{ + convert::{TryFrom, TryInto}, + fs, + io::{self, Seek}, + path::{Path, PathBuf}, + time::SystemTime, +}; + + +fn main() -> Result<(), anyhow::Error> { + // Initialize the logger + logger::init(); + + // Get all data from cli + let cli::CliData { input_dir, output_file } = cli::CliData::new(); + + // Try to pack the filesystem + self::pack_filesystem(&input_dir, &output_file).context("Unable to pack `drv` file")?; + + Ok(()) +} + +/// Extracts a `.drv` file to `output_dir`. +fn pack_filesystem(input_dir: &Path, output_file: &Path) -> Result<(), anyhow::Error> { + // Create the output file + let mut output_file = fs::File::create(output_file).context("Unable to create output file")?; + + // Create the filesystem writer + let (root_entries, root_entries_len) = DirList::new(input_dir).context("Unable to read root directory")?; + DrvFsWriter::write_fs(&mut output_file, root_entries, root_entries_len).context("Unable to write filesystem") +} + +/// Directory list +#[derive(Debug)] +struct DirList { + /// Directory read + dir: fs::ReadDir, +} + +impl DirList { + /// Creates a new iterator from a path + fn new(path: &Path) -> Result<(Self, u32), DirListNewError> { + // Get the length + let len = fs::read_dir(path) + .map_err(|err| DirListNewError::ReadDir(path.to_path_buf(), err))? + .count(); + let len = u32::try_from(len).map_err(|_err| DirListNewError::TooManyEntries)?; + + // And read the directory + let dir = fs::read_dir(path).map_err(|err| DirListNewError::ReadDir(path.to_path_buf(), err))?; + + Ok((Self { dir }, len)) + } +} + +/// Error for [`DirList::new`] +#[derive(Debug, thiserror::Error)] +enum DirListNewError { + /// Unable to read directory + #[error("Unable to read directory {}", _0.display())] + ReadDir(PathBuf, #[source] io::Error), + + /// Too many entries in directory + #[error("Too many entries in directory")] + TooManyEntries, +} + +/// Error for [`Iterator::Item`] +#[derive(Debug, thiserror::Error)] +enum NextError { + /// Unable to read entry + #[error("Unable to read entry")] + ReadEntry(#[source] io::Error), + + /// Unable to read entry metadata + #[error("Unable to read entry metadata")] + ReadMetadata(#[source] io::Error), + + /// Entry had no name + #[error("Entry had no name")] + NoEntryName, + + /// Invalid file name + #[error("Invalid file name")] + InvalidEntryName(#[source] dcb_util::ascii_str_arr::FromBytesError<0x10>), + + /// File had no file name + #[error("file had no file name")] + NoFileExtension, + + /// Invalid extension + #[error("Invalid extension")] + InvalidFileExtension(#[source] dcb_util::ascii_str_arr::FromBytesError<0x3>), + + /// Unable to get entry date + #[error("Unable to get entry date")] + EntryDate(#[source] io::Error), + + /// Unable to get entry date as time since epoch + #[error("Unable to get entry date as time since epoch")] + EntryDateSinceEpoch(#[source] std::time::SystemTimeError), + + /// Unable to get entry date as `i64` seconds since epoch + #[error("Unable to get entry date as `i64` seconds since epoch")] + EntryDateI64Secs, + + /// Unable to open file + #[error("Unable to open file")] + OpenFile(#[source] io::Error), + + /// Unable to get file size + #[error("Unable to get file size")] + FileSize(#[source] io::Error), + + /// File was too big + #[error("File was too big")] + FileTooBig, + + /// Unable to open directory + #[error("Unable to open directory")] + OpenDir(#[source] DirListNewError), +} + +impl DirWriterList for DirList { + type DirList = Self; + type Error = NextError; + type FileReader = std::fs::File; + type Iter = Self; + + fn into_iter(self) -> Self::Iter { + self + } +} + +impl Iterator for DirList { + type Item = Result::DirList>, ::Error>; + + fn next(&mut self) -> Option { + // Get the next entry + let entry = self.dir.next()?; + + // Then read it + let res = try { + // Read the entry and it's metadata + let entry = entry.map_err(NextError::ReadEntry)?; + let metadata = entry.metadata().map_err(NextError::ReadMetadata)?; + let path = entry.path(); + let name = path + .file_stem() + .ok_or(NextError::NoEntryName)? + .try_into() + .map_err(NextError::InvalidEntryName)?; + let secs_since_epoch = metadata + .modified() + .map_err(NextError::EntryDate)? + .duration_since(SystemTime::UNIX_EPOCH) + .map_err(NextError::EntryDateSinceEpoch)? + .as_secs(); + let date = chrono::NaiveDateTime::from_timestamp(i64::try_from(secs_since_epoch).map_err(|_err| NextError::EntryDateI64Secs)?, 0); + + // Check if it's a directory or file + let kind = match metadata.is_file() { + true => { + let mut file = std::fs::File::open(&path).map_err(NextError::OpenFile)?; + let size = file + .stream_len() + .map_err(NextError::FileSize)? + .try_into() + .map_err(|_err| NextError::FileTooBig)?; + let extension = path + .extension() + .ok_or(NextError::NoFileExtension)? + .try_into() + .map_err(NextError::InvalidFileExtension)?; + + log::info!("{} ({} bytes)", path.display(), size); + + let file = FileWriter::new(extension, file, size); + DirEntryWriterKind::File(file) + }, + false => { + let (entries, entries_len) = Self::new(&path).map_err(NextError::OpenDir)?; + + log::info!("{} ({} entries)", path.display(), entries_len); + + let dir = DirWriter::new(entries, entries_len); + DirEntryWriterKind::Dir(dir) + }, + }; + + DirEntryWriter::new(name, date, kind) + }; + + Some(res) + } +} diff --git a/dcb-util/src/ascii_str_arr.rs b/dcb-util/src/ascii_str_arr.rs index b492f46..0d04265 100644 --- a/dcb-util/src/ascii_str_arr.rs +++ b/dcb-util/src/ascii_str_arr.rs @@ -340,6 +340,15 @@ impl TryFrom<&str> for AsciiStrArr { } } +impl TryFrom<&std::ffi::OsStr> for AsciiStrArr { + type Error = FromBytesError; + + fn try_from(s: &std::ffi::OsStr) -> Result { + // TODO: Not allocate here, although `OsStr` doesn't provide a `as_bytes` impl, so we can't do much + Self::from_bytes(s.to_string_lossy().as_bytes()) + } +} + impl std::str::FromStr for AsciiStrArr { type Err = FromUtf8Error;