Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Rohesie committed Nov 13, 2020
0 parents commit 018f559
Show file tree
Hide file tree
Showing 9 changed files with 1,053 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
Cargo.lock
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "dmi"
version = "0.1.0"
edition = "2018"
license = "MIT"
description = "DMI library written in Rust. Provides helpers to manipulate and produce DMI format files."
authors = ["Rohesie <[email protected]>"]
keywords = ["byond", "dreammaker", "dmi", "spacestation13"]
homepage = "https://github.com/Rohesie/dmi"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
thiserror = "1.0.22"
image = "0.23.6"
deflate = "0.8.6"
inflate = "0.4.5"
155 changes: 155 additions & 0 deletions src/dmi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
pub mod chunk;
pub mod crc;
pub mod error;
pub mod icon;
pub mod ztxt;

use std::convert::TryFrom;
use std::io::prelude::*;

/// The PNG magic header
pub const MAGIC: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];

#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct RawDmi {
pub header: [u8; 8],
pub chunk_ihdr: chunk::RawGenericChunk,
pub chunk_ztxt: Option<ztxt::RawZtxtChunk>,
pub chunk_idat: chunk::RawGenericChunk,
pub chunk_iend: chunk::RawGenericChunk,
pub other_chunks: Vec<chunk::RawGenericChunk>,
}

impl RawDmi {
pub fn load<R: Read>(mut reader: R) -> Result<RawDmi, error::DmiError> {
let mut dmi_bytes = Vec::new();
reader.read_to_end(&mut dmi_bytes)?;
// 8 bytes for the PNG file signature.
// 12 + 13 bytes for the IHDR chunk.
// 12 for the IDAT chunk.
// 12 + 3 for the zTXt chunk.
// 12 for the IEND chunk.

// Total minimum size for a DMI file: 72 bytes.

if dmi_bytes.len() < 72 {
return Err(error::DmiError::Generic(format!("Failed to load DMI. Supplied reader contained size of {} bytes, lower than the required 72.", dmi_bytes.len())));
};

let header = &dmi_bytes[0..8];
if dmi_bytes[0..8] != MAGIC {
return Err(error::DmiError::Generic(format!(
"PNG header mismatch (expected {:#?}, found {:#?})",
MAGIC, header
)));
};
let header = MAGIC;
let mut chunk_ihdr = None;
let mut chunk_ztxt = None;
let mut chunk_idat = None;
let chunk_iend;
let mut other_chunks = vec![];

// Index starts after the PNG header.
let mut index = 8;

loop {
if index + 12 > dmi_bytes.len() {
return Err(error::DmiError::Generic(
"Failed to load DMI. Buffer end reached without finding an IEND chunk."
.to_string(),
));
}

let chunk_data_length = u32::from_be_bytes([
dmi_bytes[index],
dmi_bytes[index + 1],
dmi_bytes[index + 2],
dmi_bytes[index + 3],
]) as usize;

// 12 minimum necessary bytes from the chunk plus the data length.
let chunk_bytes = dmi_bytes[index..(index + 12 + chunk_data_length)].to_vec();
let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_bytes)?;
index += 12 + chunk_data_length;

match &raw_chunk.chunk_type {
b"IHDR" => chunk_ihdr = Some(raw_chunk),
b"zTXt" => chunk_ztxt = Some(ztxt::RawZtxtChunk::try_from(raw_chunk)?),
b"IDAT" => chunk_idat = Some(raw_chunk),
b"IEND" => {
chunk_iend = Some(raw_chunk);
break;
}
_ => other_chunks.push(raw_chunk),
}
}
if chunk_ihdr == None || chunk_idat == None {
return Err(error::DmiError::Generic(format!("Failed to load DMI. Buffer end reached without finding a necessary chunk.\nIHDR: {:#?}\nIDAT: {:#?}", chunk_ihdr, chunk_idat)));
};
let chunk_ihdr = chunk_ihdr.unwrap();
let chunk_idat = chunk_idat.unwrap();
let chunk_iend = chunk_iend.unwrap();

Ok(RawDmi {
header,
chunk_ihdr,
chunk_ztxt,
chunk_idat,
chunk_iend,
other_chunks,
})
}

pub fn save<W: Write>(&self, mut writter: &mut W) -> Result<usize, error::DmiError> {
let bytes_written = writter.write(&self.header)?;
let mut total_bytes_written = bytes_written;
if bytes_written < 8 {
return Err(error::DmiError::Generic(format!(
"Failed to save DMI. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

let bytes_written = self.chunk_ihdr.save(&mut writter)?;
total_bytes_written += bytes_written;
if bytes_written < u32::from_be_bytes(self.chunk_ihdr.data_length) as usize + 12 {
return Err(error::DmiError::Generic(format!(
"Failed to save DMI. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

match &self.chunk_ztxt {
Some(chunk_ztxt) => {
let bytes_written = chunk_ztxt.save(&mut writter)?;
total_bytes_written += bytes_written;
if bytes_written < u32::from_be_bytes(chunk_ztxt.data_length) as usize + 12 {
return Err(error::DmiError::Generic(format!("Failed to save DMI. Buffer unable to hold the data, only {} bytes written.", total_bytes_written)));
};
}
None => (),
};

let bytes_written = self.chunk_idat.save(&mut writter)?;
total_bytes_written += bytes_written;
if bytes_written < u32::from_be_bytes(self.chunk_idat.data_length) as usize + 12 {
return Err(error::DmiError::Generic(format!(
"Failed to save DMI. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

let bytes_written = self.chunk_iend.save(&mut writter)?;
total_bytes_written += bytes_written;
if bytes_written < u32::from_be_bytes(self.chunk_iend.data_length) as usize + 12 {
return Err(error::DmiError::Generic(format!(
"Failed to save DMI. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

Ok(total_bytes_written)
}

}
117 changes: 117 additions & 0 deletions src/dmi/chunk.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
use super::crc;
use super::error;
use std::io::prelude::*;

#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct RawGenericChunk {
pub data_length: [u8; 4],
pub chunk_type: [u8; 4],
pub data: Vec<u8>,
pub crc: [u8; 4],
}

impl RawGenericChunk {
pub fn load<R: Read>(reader: &mut R) -> Result<RawGenericChunk, error::DmiError> {
let mut chunk_bytes = Vec::new();
reader.read_to_end(&mut chunk_bytes)?;

// 4 bytes for the length.
// 4 bytes for the type.
// Data can be 0 bytes.
// 4 bytes for the CRC.

// Total minimum size for an undetermined PNG chunk: 12 bytes.
let chunk_length = chunk_bytes.len();

if chunk_length < 12 {
return Err(error::DmiError::Generic(format!("Failed to load Chunk. Supplied reader contained size of {} bytes, lower than the required 12.", chunk_length)));
};

let data_length = [
chunk_bytes[0],
chunk_bytes[1],
chunk_bytes[2],
chunk_bytes[3],
];

let chunk_type = [
chunk_bytes[4],
chunk_bytes[5],
chunk_bytes[6],
chunk_bytes[7],
];

// The chunk type is made of four ascii characters. The valid ranges are A-Z and a-z.
if !chunk_type
.iter()
.all(|c| (b'A' <= *c && *c <= b'Z') || (b'a' <= *c && *c <= b'z'))
{
return Err(error::DmiError::Generic(format!(
"Failed to load Chunk. Type contained unlawful characters: {:#?}",
chunk_type
)));
};

let data: Vec<u8> = chunk_bytes[8..(chunk_length - 4)].iter().cloned().collect();

let crc = [
chunk_bytes[chunk_length - 4],
chunk_bytes[chunk_length - 3],
chunk_bytes[chunk_length - 2],
chunk_bytes[chunk_length - 1],
];

let recalculated_crc = crc::calculate_crc(chunk_type.iter().chain(data.iter()));
if u32::from_be_bytes(crc) != recalculated_crc {
let chunk_name = String::from_utf8(chunk_type.to_vec())?;
return Err(error::DmiError::Generic(format!("Failed to load Chunk of type {}. Supplied CRC invalid: {:#?}. Its value ({}) does not match the recalculated one ({}).", chunk_name, crc, u32::from_be_bytes(crc), recalculated_crc)));
}

Ok(RawGenericChunk {
data_length,
chunk_type,
data,
crc,
})
}

pub fn save<W: Write>(&self, writter: &mut W) -> Result<usize, error::DmiError> {
let bytes_written = writter.write(&self.data_length)?;
let mut total_bytes_written = bytes_written;
if bytes_written < self.data_length.len() {
return Err(error::DmiError::Generic(format!(
"Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

let bytes_written = writter.write(&self.chunk_type)?;
total_bytes_written += bytes_written;
if bytes_written < self.chunk_type.len() {
return Err(error::DmiError::Generic(format!(
"Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

let bytes_written = writter.write(&self.data)?;
total_bytes_written += bytes_written;
if bytes_written < self.data.len() {
return Err(error::DmiError::Generic(format!(
"Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

let bytes_written = writter.write(&self.crc)?;
total_bytes_written += bytes_written;
if bytes_written < self.crc.len() {
return Err(error::DmiError::Generic(format!(
"Failed to save Chunk. Buffer unable to hold the data, only {} bytes written.",
total_bytes_written
)));
};

Ok(total_bytes_written)
}
}
17 changes: 17 additions & 0 deletions src/dmi/crc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pub fn calculate_crc<'a, I: IntoIterator<Item = &'a u8>>(buffer: I) -> u32 {
const CRC_POLYNOMIAL: u32 = 0xedb8_8320;

fn update_crc(crc: u32, message: u8) -> u32 {
let message: u32 = u32::from(message);
let mut crc = crc ^ message;
for _ in 0..8 {
crc = (if crc & 1 != 0 { CRC_POLYNOMIAL } else { 0 }) ^ (crc >> 1);
}
crc
}

buffer
.into_iter()
.fold(u32::max_value(), |crc, message| update_crc(crc, *message))
^ u32::max_value()
}
26 changes: 26 additions & 0 deletions src/dmi/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DmiError {
#[error("IO error")]
Io(#[from] io::Error),
#[error("Invalid chunk type (byte outside the range `A-Za-z`): {chunk_type:?}")]
InvalidChunkType { chunk_type: [u8; 4] },
#[error("CRC mismatch (stated {stated:?}, calculated {calculated:?})")]
CrcMismatch { stated: u32, calculated: u32 },
#[error("Image-processing error")]
Image(#[from] image::error::ImageError),
#[error("Dmi error")]
Generic(String),
#[error("Encoding error")]
Encoding(String),
#[error("Conversion error")]
Conversion(String),
#[error("FromUtf8 error")]
FromUtf8(#[from] std::string::FromUtf8Error),
#[error("ParseInt error")]
ParseInt(#[from] std::num::ParseIntError),
#[error("ParseFloat error")]
ParseFloat(#[from] std::num::ParseFloatError),
}
Loading

0 comments on commit 018f559

Please sign in to comment.