-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 018f559
Showing
9 changed files
with
1,053 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
Cargo.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
Oops, something went wrong.