diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..05fedd3 --- /dev/null +++ b/readme.md @@ -0,0 +1,37 @@ +# Eyepiece: An IPS patcher + +## Usage +```sh +eyepiece patch.ips input.file output.file +``` + +## Building +```sh +cargo build +cargo run -- patch.ips in_file out_file +``` + +## Description + +The IPS(24/16) patchfile format is one of the simplest possible patchfile formats. +It contains no way to identify the correct target file, and cannot insert or remove bytes. + +An IPS file starts with the magic number "PATCH" +```console +0000: 50 41 54 43 48 |PATCH| +``` + +Patches are encoded linearly with no padding or alignment, and +all numeric values are encoded big-endian. Patches cannot have +a length of b"EOF", as "EOF" marks the end of the patchfile. +```console +xxxx: 45 4f 46 |EOF| +``` + +The patchfile matches the following pseudo-grammar +```console +IPS = "PATCH" Patch* "EOF" +Patch = { offset: u24 != "EOF", data: Data } +Data = { len: u16 != 0, data: [u8; len] } + | { _: u16 == 0, len: u16, byte: u8 } // Run-length-encoded data +``` diff --git a/sample-patches/empty.ips b/sample-patches/empty.ips new file mode 100644 index 0000000..008c455 --- /dev/null +++ b/sample-patches/empty.ips @@ -0,0 +1 @@ +PATCHEOF \ No newline at end of file diff --git a/sample-patches/example.ips b/sample-patches/example.ips new file mode 100644 index 0000000..80f077f Binary files /dev/null and b/sample-patches/example.ips differ diff --git a/src/ips.rs b/src/ips.rs new file mode 100644 index 0000000..3cd2cdf --- /dev/null +++ b/src/ips.rs @@ -0,0 +1,127 @@ +//! The IPS(24/16) patchfile format is one of the simplest possible patchfile formats. +//! It contains no way to identify the correct target file, and cannot insert or remove bytes. +//! +//! An IPS file starts with the magic number "PATCH" +//! ```console +//! 0000: 50 41 54 43 48 |PATCH| +//! ``` +//! +//! Patches are encoded linearly with no padding or alignment, and +//! all numeric values are encoded big-endian. Patches cannot have +//! a length of b"EOF", as "EOF" marks the end of the patchfile. +//! ```console +//! xxxx: 45 4f 46 |EOF| +//! ``` +//! +//! The patchfile matches the following pseudo-grammar +//! ```console +//! IPS = "PATCH" Patch* "EOF" +//! Patch = { offset: u24 != "EOF", kind: PatchKind } +//! PatchData = { len: u16 != 0, data: [u8; len] } // Plain ol' data +//! | { _: u16 == 0, len: u16, byte: u8 } // Run-length-encoded data +//! ``` + +use crate::{parse_utils::*, Apply}; +use std::io::{self, Read, Seek, SeekFrom, Write}; + +pub struct IPS { + pub magic: [u8; 5], + pub patches: Vec, +} + +// A single IPS patch +pub struct Patch { + pub offset: u32, + pub data: Data, +} + +/// The data +pub enum Data { + /// Run-length-encoded (repeated) data + RLEnc(u16, u8), + + /// Verbatim data + Plain(Vec), +} + +impl Apply for IPS { + fn apply(&self, writer: &mut W) -> io::Result<()> { + for patch in &self.patches { + patch.apply(writer)?; + } + Ok(()) + } +} + +impl Apply for Patch { + fn apply(&self, writer: &mut W) -> io::Result<()> { + let Self { offset, data } = self; + writer.seek(SeekFrom::Start(*offset as _))?; + data.apply(writer) + } +} + +impl Apply for Data { + fn apply(&self, writer: &mut W) -> io::Result<()> { + match self { + &Data::RLEnc(len, value) => { + io::copy(&mut io::repeat(value).take(len as _), writer)?; + Ok(()) + } + Data::Plain(buf) => writer.write_all(buf), + } + } +} + +impl IPS { + /// The expected magic number + pub const MAGIC: &[u8] = b"PATCH"; + + /// Reads an IPS file out of the provided reader. + /// + /// Consumes 5 bytes and returns Ok(None) if the reader doesn't yield an IPS file. + pub fn parse(reader: &mut impl Read) -> io::Result> { + let magic = read_bytes(reader)?; + if Self::MAGIC != magic { + return Ok(None); + } + + let mut patches = vec![]; + while let Some(patch) = Patch::parse(reader)? { + patches.push(patch) + } + + Ok(Some(Self { magic, patches })) + } +} + +impl Patch { + pub fn parse(reader: &mut impl Read) -> io::Result> { + match offset(reader)? { + None => Ok(None), + Some(offset) => { + let data = Data::parse(reader)?; + Ok(Some(Self { offset, data })) + } + } + } +} + +impl Data { + pub fn parse(reader: &mut impl Read) -> io::Result { + match read16(reader)? { + 0 => { + let len = read16(reader)?; + let value = read_bytes::<1>(reader)?[0]; + + Ok(Data::RLEnc(len, value)) + } + len => { + let mut data = vec![0u8; len as _]; + reader.read_exact(&mut data)?; + + Ok(Data::Plain(data)) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e69de29..e3f3427 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -0,0 +1,53 @@ +//! Simple IPS file parsing and application + +pub use ips::IPS; +use std::io::{Result as IoResult, Seek, Write}; + +pub mod ips; + +/// Patches a [seekable](Seek) [writer](Write) with the provided [patch](Apply) +pub trait Patchable: Write + Seek { + fn patch(&mut self, with: P) -> IoResult<&mut Self>; +} + +impl Patchable for W { + /// Applies the provided patch to this [seekable](Seek) [writer](Write) + fn patch(&mut self, with: P) -> IoResult<&mut Self> { + with.apply(self)?; + Ok(self) + } +} + +/// Applies a patch to a [seekable](Seek) [writer](Write) +pub trait Apply { + /// Applies this patch to the provided [seekable](Seek) [writer](Write) + fn apply(&self, writer: &mut W) -> IoResult<()>; +} + +mod parse_utils { + use std::io; + + pub fn read_bytes(reader: &mut impl io::Read) -> io::Result<[u8; N]> { + let mut buf = [0; N]; + reader.read_exact(&mut buf).map(|_| buf) + } + + /// Parses a 24-bit big-endian "offset" and ensures it isn't `0x454f46` (`b"EOF"`) + pub fn offset(reader: &mut impl io::Read) -> io::Result> { + let buf = read_bytes(reader)?; + + if buf == *b"EOF" { + return Ok(None); + } + + let mut offset = 0; + for byte in buf { + offset = offset << 8 | byte as u32; + } + Ok(Some(offset)) + } + + pub fn read16(reader: &mut impl io::Read) -> io::Result { + Ok(u16::from_be_bytes(read_bytes(reader)?)) + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..e9e4d96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,41 @@ -fn main() { - println!("Hello, world!"); +use std::{error::Error, fs, io}; + +use eyepiece::{Patchable, IPS}; + +const NAME: &str = env!("CARGO_PKG_NAME"); + +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args().skip(1).collect(); + let [ref patch, ref input, ref output] = args[..] else { + eprintln!("Usage: {NAME} "); + return Ok(()); + }; + + eprintln!("{NAME} v{}", env!("CARGO_PKG_VERSION")); + + let Some(ips) = IPS::parse(&mut fs::File::open(patch)?)? else { + eprintln!("Magic did not match! Is '{patch}' an IPS file?"); + return Ok(()); + }; + + println!("Successfully read patch file '{patch}'"); + + let mut outfile = fs::OpenOptions::new() + .create(true) + .truncate(false) + .write(true) + .open(output)?; + + // FIXME: compare via normalized path equality + if input != output { + let mut infile = fs::File::open(input)?; + + let size = io::copy(&mut infile, &mut outfile)?; + println!("Copied 0x{size:x} bytes from '{input}' to '{output}'"); + } + + outfile.patch(ips)?; + + println!("Patched!"); + Ok(()) }