Draw the rest of the owl
This commit is contained in:
parent
8610a2fe1d
commit
f5940e9c79
37
readme.md
Normal file
37
readme.md
Normal 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
1
sample-patches/empty.ips
Normal file
@ -0,0 +1 @@
|
||||
PATCHEOF
|
BIN
sample-patches/example.ips
Normal file
BIN
sample-patches/example.ips
Normal file
Binary file not shown.
127
src/ips.rs
Normal file
127
src/ips.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
53
src/lib.rs
53
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<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)?))
|
||||
}
|
||||
}
|
42
src/main.rs
42
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<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(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user