start adding SSR support

This commit is contained in:
lifning 2021-01-16 22:53:03 -08:00
parent cc3a4b2e09
commit 8472af4c21
5 changed files with 369 additions and 160 deletions

View File

@ -6,7 +6,7 @@ use std::fs::File;
use std::io::prelude::*;
use std::path::PathBuf;
use binread::prelude::*;
use one_rust::JodOne;
use one_rust::prelude::*;
/// Unpack .one archives from NiGHTS: Journey of Dreams
#[derive(FromArgs)]
@ -70,10 +70,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
Action::DescribeFile(subcmd) => {
let mut infile = File::open(&subcmd.file)?;
let archive: JodOne = infile.read_le()?;
let infile = File::open(&subcmd.file)?;
let archive = read_one_archive(infile)?;
println!("{:?}", archive);
for entry in archive.entries {
for entry in archive.entries() {
println!("{:?}", entry);
}
}
@ -89,9 +89,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
entries.push((path, data));
}
let archive = JodOne::pack(entries, None);
let outfile = File::create(&subcmd.file)?;
archive.write(outfile)?;
let archive = JodOne::pack(entries);
let mut outfile = File::create(&subcmd.file)?;
archive.write(&mut outfile)?;
}
}
Ok(())

View File

@ -5,4 +5,24 @@ extern crate binwrite;
extern crate prs_rust;
mod one;
pub use one::*;
pub mod prelude {
pub use one::OneArchive;
pub use one::dreams::*;
pub use one::rings::*;
pub use one::read_one_archive;
}
use binread::NullString;
fn string_binread_helper(x: NullString) -> String {
x.into_string()
}
fn string_binwrite_helper(pad_to_len: usize) -> impl Fn(&String) -> Vec<u8> {
move |x: &String| {
let mut v = x.as_bytes().to_vec();
v.resize(pad_to_len, 0);
v
}
}

159
one-rust/src/one/dreams.rs Normal file
View File

@ -0,0 +1,159 @@
use binread::FilePtr;
use binwrite::BinWrite;
use prs_rust::prs;
use one::{OneArchive, WriteSeek, OneArchiveEntry};
use std::fmt::Formatter;
use std::path::PathBuf;
use std::mem::size_of;
use crate::string_binread_helper;
use crate::string_binwrite_helper;
// stats on version, comment, and category:
// $ find . -name \*.one -exec bash -c "onear tf "{}" > "{}".txt" \;
// $ fd one.txt | xargs cat | sed 's/count: .*/..}/' | grep '^JodOne{' | sort | uniq -c
// 1 JodOne{version: 0xca, comment: " ", category: Default, ..}
// 11 JodOne{version: 0xca, comment: " ", category: landData, ..}
// 4 JodOne{version: 0xcb, comment: " ", category: landData, ..}
// 988 JodOne{version: 0xcc, comment: " ", category: Default, ..}
// 1579 JodOne{version: 0xcc, comment: " ", category: landData, ..}
const JOD_ONE_MAGIC: &'static [u8; 16] = b"ThisIsOneFile\0\0\0";
const SIZE_OF_JOD_ONE_HEADER: usize = JOD_ONE_MAGIC.len() + 2 * size_of::<u32>() + 32 + 132;
const SIZE_OF_JOD_ONE_ENTRY: usize = 6 * size_of::<u32>() + 192;
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(little, magic = b"ThisIsOneFile\0\0\0")]
#[binwrite(little)]
pub struct JodOne {
// usually 0xCC... archive creator version maybe?
// but i've seen 0xCA in Test04/disp/Test04.one and 0xCB in Test05/disp/stg2010_sun.one
pub version: u32,
// "Default" or "landData"
#[br(pad_size_to = 32, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(32)))]
pub category: String,
pub count: u32,
// always " "
#[br(pad_size_to = 132, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(132)))]
pub comment: String,
#[br(count = count)]
pub entries: Vec<JodOneEntry>
}
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(little, assert(id_again == id))]
#[binwrite(little)]
pub struct JodOneEntry {
pub id: u32,
pub size_cmp: u32,
pub is_compressed: u32,
pub size_dec: u32,
pub id_again: u32,
#[br(count = size_cmp)]
#[binwrite(preprocessor(|x: &FilePtr<u32, Vec<u8>>| x.ptr))]
pub data: FilePtr<u32, Vec<u8>>,
#[br(pad_size_to = 192, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(192)))]
pub name: String,
}
impl std::fmt::Debug for JodOne {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "JodOne{{version: 0x{:x}, comment: {:?}, category: {}, count: {}, ..}}", self.version, self.comment, self.category, self.count)
}
}
impl std::fmt::Debug for JodOneEntry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"JodOneEntry{{id: {}, is_compressed: {}, name: {:?}, size_cmp: {}, size_dec: {}, ..}}",
self.id, self.is_compressed != 0, self.name, self.size_cmp, self.size_dec,
)
}
}
impl OneArchiveEntry for JodOneEntry {}
impl OneArchive for JodOne {
fn write(&self, mut writer: &mut dyn WriteSeek) -> std::io::Result<()> {
writer.write(JOD_ONE_MAGIC)?;
BinWrite::write(&self, &mut writer)?;
for entry in &self.entries {
let loc = writer.seek(std::io::SeekFrom::Current(0)).unwrap() as usize;
let padding = vec![0; entry.data.ptr as usize - loc];
writer.write(&padding)?;
writer.write(entry.data.value.as_ref().unwrap())?;
}
Ok(())
}
fn pack(inputs: impl IntoIterator<Item=(PathBuf, Vec<u8>)>) -> Self where Self: Sized {
let mut entries = Vec::new();
for (id, (path, data)) in inputs.into_iter().enumerate() {
let cmp = prs::Compressor::new(&data, None).compress();
let name = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "\\");
if cmp.len() < data.len() {
entries.push(JodOneEntry {
id: id as u32,
size_cmp: cmp.len() as u32,
is_compressed: 1,
size_dec: data.len() as u32,
id_again: id as u32,
data: FilePtr { ptr: 0, value: Some(cmp) },
name,
});
} else {
entries.push(JodOneEntry {
id: id as u32,
size_cmp: data.len() as u32,
is_compressed: 0,
size_dec: 0,
id_again: id as u32,
data: FilePtr { ptr: 0, value: Some(data) },
name,
});
}
}
let mut loc = (SIZE_OF_JOD_ONE_HEADER + SIZE_OF_JOD_ONE_ENTRY * entries.len()) as u32;
for entry in &mut entries {
loc = ((loc + 15) / 16) * 16; // round up to 16 byte alignment
entry.data.ptr = loc;
loc += entry.data.value.as_ref().unwrap().len() as u32;
}
// user can overwrite string/version fields afterward
JodOne {
version: 0xCC,
category: "Default".to_string(),
count: entries.len() as u32,
comment: " ".to_string(),
entries,
}
}
fn unpack(self) -> Box<dyn Iterator<Item=(PathBuf, Vec<u8>)>> {
Box::new(self.entries.into_iter().map(|entry| {
let key = PathBuf::from(entry.name.replace('\\', std::path::MAIN_SEPARATOR.encode_utf8(&mut [0u8; 4])));
if entry.is_compressed == 0 || entry.size_dec == 0 {
(key, entry.data.into_inner())
} else {
let dec_buf = prs::Decompressor::new(entry.data.into_inner(), Some(entry.size_dec as usize)).decompress();
(key, dec_buf)
}
}))
}
fn entries(&self) -> Vec<&dyn OneArchiveEntry> {
self.entries.iter().map(|x| {
let y: &dyn OneArchiveEntry = x;
y
}).collect()
}
}

View File

@ -1,159 +1,35 @@
use binread::{FilePtr, NullString};
use prs_rust::prs;
use std::fmt::Formatter;
use std::path::PathBuf;
use std::mem::size_of;
use std::io::{Write, Seek};
use binwrite::BinWrite;
use std::io::{Seek, Write, Read};
use binread::BinReaderExt;
use std::error::Error;
use binread::io::SeekFrom;
use std::fmt::Debug;
fn string_binread_helper(x: NullString) -> String {
x.into_string()
pub(crate) mod dreams;
pub(crate) mod rings;
pub trait WriteSeek: Write + Seek {}
impl<T> WriteSeek for T where T: Write + Seek {}
pub trait OneArchive: Debug {
fn write(&self, writer: &mut dyn WriteSeek) -> std::io::Result<()>;
fn pack(inputs: impl IntoIterator<Item=(PathBuf, Vec<u8>)>) -> Self where Self: Sized;
fn unpack(self) -> Box<dyn Iterator<Item=(PathBuf, Vec<u8>)>>;
fn entries(&self) -> Vec<&dyn OneArchiveEntry>;
}
fn string_binwrite_helper(pad_to_len: usize) -> impl Fn(&String) -> Vec<u8> {
move |x: &String| {
let mut v = x.as_bytes().to_vec();
v.resize(pad_to_len, 0);
v
pub trait OneArchiveEntry: Debug {}
pub fn read_one_archive(mut reader: impl Read + Seek) -> Result<Box<dyn OneArchive>, Box<dyn Error>> {
let rewind = reader.seek(SeekFrom::Current(0))?;
match reader.read_le::<dreams::JodOne>() {
Ok(one) => return Ok(Box::new(one)),
Err(e) => info!("Not a NiGHTS Journey of Dreams archive: {}", e),
}
}
// stats on version, comment, and category:
// $ find . -name \*.one -exec bash -c "onear tf "{}" > "{}".txt" \;
// $ fd one.txt | xargs cat | sed 's/count: .*/..}/' | grep '^JodOne{' | sort | uniq -c
// 1 JodOne{version: 0xca, comment: " ", category: Default, ..}
// 11 JodOne{version: 0xca, comment: " ", category: landData, ..}
// 4 JodOne{version: 0xcb, comment: " ", category: landData, ..}
// 988 JodOne{version: 0xcc, comment: " ", category: Default, ..}
// 1579 JodOne{version: 0xcc, comment: " ", category: landData, ..}
const JOD_ONE_MAGIC: &'static [u8; 16] = b"ThisIsOneFile\0\0\0";
const SIZE_OF_JOD_ONE_HEADER: usize = JOD_ONE_MAGIC.len() + 2 * size_of::<u32>() + 32 + 132;
const SIZE_OF_JOD_ONE_ENTRY: usize = 6 * size_of::<u32>() + 192;
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(little, magic = b"ThisIsOneFile\0\0\0")]
#[binwrite(little)]
pub struct JodOne {
// usually 0xCC... archive creator version maybe?
// but i've seen 0xCA in Test04/disp/Test04.one and 0xCB in Test05/disp/stg2010_sun.one
pub version: u32,
// "Default" or "landData"
#[br(pad_size_to = 32, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(32)))]
pub category: String,
pub count: u32,
// always " "
#[br(pad_size_to = 132, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(132)))]
pub comment: String,
#[br(count = count)]
pub entries: Vec<JodOneEntry>
}
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(little, assert(id_again == id))]
#[binwrite(little)]
pub struct JodOneEntry {
pub id: u32,
pub size_cmp: u32,
pub is_compressed: u32,
pub size_dec: u32,
pub id_again: u32,
#[br(count = size_cmp)]
#[binwrite(preprocessor(|x: &FilePtr<u32, Vec<u8>>| x.ptr))]
pub data: FilePtr<u32, Vec<u8>>,
#[br(pad_size_to = 192, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(192)))]
pub name: String,
}
impl std::fmt::Debug for JodOne {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "JodOne{{version: 0x{:x}, comment: {:?}, category: {}, count: {}, ..}}", self.version, self.comment, self.category, self.count)
}
}
impl std::fmt::Debug for JodOneEntry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"JodOneEntry{{id: {}, is_compressed: {}, name: {:?}, size_cmp: {}, size_dec: {}, ..}}",
self.id, self.is_compressed != 0, self.name.to_string(), self.size_cmp, self.size_dec,
)
}
}
impl JodOne {
pub fn write(&self, mut writer: impl Write + Seek) -> std::io::Result<()> {
writer.write(JOD_ONE_MAGIC)?;
BinWrite::write(&self, &mut writer)?;
for entry in &self.entries {
let loc = writer.seek(std::io::SeekFrom::Current(0)).unwrap() as usize;
let padding = vec![0; entry.data.ptr as usize - loc];
writer.write(&padding)?;
writer.write(entry.data.value.as_ref().unwrap())?;
}
Ok(())
}
pub fn pack(inputs: impl IntoIterator<Item = (PathBuf, Vec<u8>)>, category: Option<&str>) -> Self {
let mut entries = Vec::new();
for (id, (path, data)) in inputs.into_iter().enumerate() {
let cmp = prs::Compressor::new(&data, None).compress();
let name = path.to_string_lossy().replace(std::path::MAIN_SEPARATOR, "\\");
if cmp.len() < data.len() {
entries.push(JodOneEntry {
id: id as u32,
size_cmp: cmp.len() as u32,
is_compressed: 1,
size_dec: data.len() as u32,
id_again: id as u32,
data: FilePtr { ptr: 0, value: Some(cmp) },
name,
});
} else {
entries.push(JodOneEntry {
id: id as u32,
size_cmp: data.len() as u32,
is_compressed: 0,
size_dec: 0,
id_again: id as u32,
data: FilePtr { ptr: 0, value: Some(data) },
name,
});
}
}
let mut loc = (SIZE_OF_JOD_ONE_HEADER + SIZE_OF_JOD_ONE_ENTRY * entries.len()) as u32;
for entry in &mut entries {
loc = ((loc + 15) / 16) * 16; // round up to 16 byte alignment
entry.data.ptr = loc;
loc += entry.data.value.as_ref().unwrap().len() as u32;
}
JodOne {
version: 0xCC,
category: category.unwrap_or("Default").to_string(),
count: entries.len() as u32,
comment: " ".to_string(),
entries,
}
}
pub fn unpack(self) -> impl Iterator<Item = (PathBuf, Vec<u8>)> {
self.entries.into_iter().map(|entry| {
debug!("{:?}", entry);
let key = PathBuf::from(entry.name.replace('\\', std::path::MAIN_SEPARATOR.encode_utf8(&mut [0u8; 4])));
if entry.is_compressed == 0 || entry.size_dec == 0 {
(key, entry.data.into_inner())
} else {
let dec_buf = prs::Decompressor::new(entry.data.into_inner(), Some(entry.size_dec as usize)).decompress();
(key, dec_buf)
}
})
reader.seek(SeekFrom::Start(rewind))?;
match reader.read_be::<rings::SsrOne>() {
Ok(one) => return Ok(Box::new(one)),
Err(e) => info!("Not a Sonic and the Secret Rings archive: {}", e),
}
Err("No valid .one archive found".into())
}

154
one-rust/src/one/rings.rs Normal file
View File

@ -0,0 +1,154 @@
use binread::FilePtr;
use binwrite::BinWrite;
use std::io::SeekFrom;
use crate::string_binread_helper;
use crate::string_binwrite_helper;
use one::{OneArchive, WriteSeek, OneArchiveEntry};
use prs_rust::prs;
use std::path::PathBuf;
use std::fmt::Formatter;
use std::mem::size_of;
const SIZE_OF_SSR_ONE_HEADER: usize = 4 * size_of::<u32>(); // 0x10
const SIZE_OF_SSR_ONE_ENTRY: usize = 4 * size_of::<u32>() + 32; // 0x30
// stats on padding and size_dec:
// $ find . -name \*.one -exec bash -c "onear tf "{}" > "{}".txt" \;
// $ fd one.txt | xargs cat | sed 's/count: [0-9]\+, //' | grep '^SsrOne{' | sort | uniq -c
// 284 SsrOne{_unknown: 0x0, ..}
// $ fd one.txt | xargs cat | grep '^SsrOneEntry{' | sed \
// > -e 's/ [1-9][0-9]*, / 1+, /g' \
// > -e 's/id: [01]+\?, name: "[^"]\+", //' | sort | uniq -c
// 36 SsrOneEntry{size_cmp: 1+, size_dec: 0, ..}
// 5105 SsrOneEntry{size_cmp: 1+, size_dec: 1+, ..}
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(big, assert(entries.ptr == 0x10 && data_offset == entries.ptr + count * 0x30))]
#[binwrite(big)]
pub struct SsrOne {
/// number of files stored in the .one
pub count: u32,
/// the location of the table of contents. usually 0x10, just after this header
#[br(count = count)]
#[binwrite(preprocessor(|x: &FilePtr<u32, Vec<_>>| x.ptr))]
pub entries: FilePtr<u32, Vec<SsrOneEntry>>,
/// location of the end of the toc/beginning of data. TODO: assertion
pub data_offset: u32,
/// always 0
pub _padding: u32,
}
#[derive(BinRead)]
#[derive(BinWrite)]
#[br(big)]
#[binwrite(big)]
pub struct SsrOneEntry {
#[br(pad_size_to = 32, map = string_binread_helper)]
#[binwrite(preprocessor(string_binwrite_helper(32)))]
pub name: String,
pub id: u32,
// note: seek_before/restore_position shenanigans because we need size_cmp for data's count
#[br(seek_before = SeekFrom::Current(4), restore_position)]
pub size_cmp: u32,
#[br(count = size_cmp)]
#[binwrite(preprocessor(|x: &FilePtr<u32, Vec<u8>>| x.ptr))]
pub data: FilePtr<u32, Vec<u8>>,
#[br(seek_before = SeekFrom::Current(4))]
pub size_dec: u32,
}
impl std::fmt::Debug for SsrOne {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "SsrOne{{count: {}, _unknown: 0x{:x}, ..}}", self.count, self._padding)
}
}
impl std::fmt::Debug for SsrOneEntry {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"SsrOneEntry{{id: {}, name: {:?}, size_cmp: {}, size_dec: {}, ..}}",
self.id, self.name, self.size_cmp, self.size_dec,
)
}
}
impl OneArchiveEntry for SsrOneEntry {}
impl OneArchive for SsrOne {
fn write(&self, mut writer: &mut dyn WriteSeek) -> std::io::Result<()> {
BinWrite::write(&self, &mut writer)?;
for entry in self.entries.value.as_ref().unwrap() {
let loc = writer.seek(std::io::SeekFrom::Current(0)).unwrap() as usize;
let padding = vec![0; entry.data.ptr as usize - loc];
writer.write(&padding)?;
writer.write(entry.data.value.as_ref().unwrap())?;
}
Ok(())
}
fn pack(inputs: impl IntoIterator<Item=(PathBuf, Vec<u8>)>) -> Self where Self: Sized {
let mut entries = Vec::new();
for (id, (path, data)) in inputs.into_iter().enumerate() {
let cmp = prs::Compressor::new(&data, None).compress();
let name = path.to_string_lossy().to_uppercase();
if cmp.len() < data.len() {
entries.push(SsrOneEntry {
id: id as u32,
size_cmp: cmp.len() as u32,
size_dec: data.len() as u32,
data: FilePtr { ptr: 0, value: Some(cmp) },
name,
});
} else {
entries.push(SsrOneEntry {
id: id as u32,
size_cmp: data.len() as u32,
size_dec: 0,
data: FilePtr { ptr: 0, value: Some(data) },
name,
});
}
}
let data_offset = (SIZE_OF_SSR_ONE_HEADER + SIZE_OF_SSR_ONE_ENTRY * entries.len()) as u32;
let mut loc = data_offset;
for entry in &mut entries {
loc = ((loc + 3) / 4) * 4; // round up to 4 byte alignment
entry.data.ptr = loc;
loc += entry.data.value.as_ref().unwrap().len() as u32;
}
// user can overwrite string/version fields afterward
SsrOne {
count: entries.len() as u32,
entries: FilePtr {
ptr: SIZE_OF_SSR_ONE_HEADER as u32,
value: Some(entries),
},
data_offset,
_padding: 0
}
}
fn unpack(self) -> Box<dyn Iterator<Item=(PathBuf, Vec<u8>)>> {
Box::new(self.entries.value.unwrap().into_iter().map(|entry| {
let key = PathBuf::from(entry.name);
if entry.size_dec == 0 {
(key, entry.data.into_inner())
} else {
let dec_buf = prs::Decompressor::new(entry.data.into_inner(), Some(entry.size_dec as usize)).decompress();
(key, dec_buf)
}
}))
}
fn entries(&self) -> Vec<&dyn OneArchiveEntry> {
self.entries.iter().map(|x| {
let y: &dyn OneArchiveEntry = x;
y
}).collect()
}
}