Improved DrvFsWriter interface by using the DirWriterList trait.

Fixed `DirWriter::write_entries` not writing all entries correctly.
Added `drv-packer` binary.
This commit is contained in:
Filipe Rodrigues 2021-01-25 00:23:12 +00:00
parent 26972d128b
commit 3d54f23022
12 changed files with 457 additions and 76 deletions

View File

@ -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",
]

View File

@ -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<W: io::Write + io::Seek, R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::Error>>>(
writer: &mut W, root_entries: I,
) -> Result<(), ToWriterError> {
pub fn write_fs<W: io::Write + io::Seek, L: DirWriterList>(
writer: &mut W, root_entries: L, root_entries_len: u32,
) -> Result<(), ToWriterError<L::Error>> {
// 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(())

View File

@ -68,72 +68,106 @@ impl DirReader {
}
}
/// Directory writer
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct DirWriter<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, 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<Item = Result<DirEntryWriter<Self>, Self::Error>>;
/// Converts this list into an iterator
fn into_iter(self) -> Self::Iter;
}
impl<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::Error>>> DirWriter<R, I> {
/// Directory writer
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct DirWriter<L: DirWriterList> {
/// Writer list
entries: L,
/// Number of entries
entries_len: u32,
}
impl<L: DirWriterList> DirWriter<L> {
/// 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<W: io::Write + io::Seek>(self, writer: &mut W) -> Result<(), WriteEntriesError> {
///
/// Returns the number of sectors written by this directory
pub fn write_entries<W: io::Write + io::Seek>(self, writer: &mut W) -> Result<u32, WriteEntriesError<L::Error>> {
// 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::<Result<Vec<_>, _>>()
.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)
}
}

View File

@ -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<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::Error>>> {
#[derive(Debug)]
pub enum DirEntryWriterKind<L: DirWriterList> {
/// A file
File(FileWriter<R>),
File(FileWriter<L::FileReader>),
/// Directory
Dir(DirWriter<R, I>),
Dir(DirWriter<L>),
}
/// A directory entry reader
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct DirEntryWriter<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::Error>>> {
#[derive(Debug)]
pub struct DirEntryWriter<L: DirWriterList> {
/// Entry name
name: AsciiStrArr<0x10>,
@ -121,12 +121,12 @@ pub struct DirEntryWriter<R: io::Read, I: ExactSizeIterator<Item = Result<DirEnt
date: NaiveDateTime,
/// Entry kind
kind: DirEntryWriterKind<R, I>,
kind: DirEntryWriterKind<L>,
}
impl<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::Error>>> DirEntryWriter<R, I> {
impl<L: DirWriterList> DirEntryWriter<L> {
/// Creates a new entry writer from it's name, date and kind
pub fn new(name: AsciiStrArr<0x10>, date: NaiveDateTime, kind: DirEntryWriterKind<R, I>) -> Self {
pub fn new(name: AsciiStrArr<0x10>, date: NaiveDateTime, kind: DirEntryWriterKind<L>) -> Self {
Self { name, date, kind }
}
@ -134,17 +134,17 @@ impl<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, 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<R, I> {
pub fn kind(&self) -> &DirEntryWriterKind<L> {
&self.kind
}
/// Returns this entry's kind
pub fn into_kind(self) -> DirEntryWriterKind<R, I> {
pub fn into_kind(self) -> DirEntryWriterKind<L> {
self.kind
}
@ -171,6 +171,8 @@ impl<R: io::Read, I: ExactSizeIterator<Item = Result<DirEntryWriter<R, I>, io::E
},
DirEntryWriterKind::Dir(_) => {
*bytes.kind = 0x80;
LittleEndian::write_u32(bytes.size, 0);
},
};

View File

@ -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<E: std::error::Error + 'static> {
/// 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<Self>),
/// 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),
}

View File

@ -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<E: std::error::Error + 'static> {
/// Unable to write root directory
#[error("Unable to write root directory")]
RootDir(#[source] dir::WriteEntriesError),
RootDir(#[source] dir::WriteEntriesError<E>),
}

View File

@ -90,8 +90,8 @@ impl<R: io::Read> FileWriter<R> {
}
/// Writes this file to a writer
pub fn into_writer<W: io::Write>(mut self, writer: &mut W) -> Result<(), io::Error> {
let written = std::io::copy(&mut self.reader, writer)?;
pub fn write<W: io::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(())
}

View File

@ -0,0 +1,25 @@
[package]
name = "drv-packer"
version = "0.1.0"
authors = ["Filipe Rodrigues <filipejacintorodrigues1@gmail.com>"]
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"

View File

@ -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 }
}
}

View File

@ -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<dyn SharedLogger>;
/// 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");
}
}

View File

@ -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<DirEntryWriter<<Self as DirWriterList>::DirList>, <Self as DirWriterList>::Error>;
fn next(&mut self) -> Option<Self::Item> {
// 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)
}
}

View File

@ -340,6 +340,15 @@ impl<const N: usize> TryFrom<&str> for AsciiStrArr<N> {
}
}
impl<const N: usize> TryFrom<&std::ffi::OsStr> for AsciiStrArr<N> {
type Error = FromBytesError<N>;
fn try_from(s: &std::ffi::OsStr) -> Result<Self, Self::Error> {
// 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<const N: usize> std::str::FromStr for AsciiStrArr<N> {
type Err = FromUtf8Error<N>;