Draw the rest of the owl

This commit is contained in:
John 2024-07-07 03:04:41 -05:00
parent 8610a2fe1d
commit f5940e9c79
6 changed files with 258 additions and 2 deletions

37
readme.md Normal file
View File

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

1
sample-patches/empty.ips Normal file
View File

@ -0,0 +1 @@
PATCHEOF

BIN
sample-patches/example.ips Normal file

Binary file not shown.

127
src/ips.rs Normal file
View File

@ -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<Patch>,
}
// 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<u8>),
}
impl Apply for IPS {
fn apply<W: Write + Seek>(&self, writer: &mut W) -> io::Result<()> {
for patch in &self.patches {
patch.apply(writer)?;
}
Ok(())
}
}
impl Apply for Patch {
fn apply<W: Write + Seek>(&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<W: Write + Seek>(&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<Option<Self>> {
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<Option<Self>> {
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<Self> {
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))
}
}
}
}

View File

@ -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<P: Apply>(&mut self, with: P) -> IoResult<&mut Self>;
}
impl<W: Write + Seek> Patchable for W {
/// Applies the provided patch to this [seekable](Seek) [writer](Write)
fn patch<P: Apply>(&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<W: Write + Seek>(&self, writer: &mut W) -> IoResult<()>;
}
mod parse_utils {
use std::io;
pub fn read_bytes<const N: usize>(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<Option<u32>> {
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<u16> {
Ok(u16::from_be_bytes(read_bytes(reader)?))
}
}

View File

@ -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<dyn Error>> {
let args: Vec<_> = std::env::args().skip(1).collect();
let [ref patch, ref input, ref output] = args[..] else {
eprintln!("Usage: {NAME} <patch> <in> <out>");
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(())
}