diff --git a/.gitignore b/.gitignore index 96ef6c0..29e9259 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target Cargo.lock +tests/resources/save_test.dmi diff --git a/Cargo.toml b/Cargo.toml index 238648e..875114e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ crc32fast = "1" deflate = "1" image = { version = "0.25", default-features = false, features = ["png"] } inflate = "0.4" +png = "0.17.16" thiserror = "2" diff --git a/src/chunk.rs b/src/chunk.rs index 039c82f..65aafe3 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -23,7 +23,7 @@ impl RawGenericChunk { 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))); + return Err(error::DmiError::Generic(format!("Failed to load Chunk. Supplied reader contained size of {chunk_length} bytes, lower than the required 12."))); }; let data_length = [ @@ -46,8 +46,7 @@ impl RawGenericChunk { .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 + "Failed to load Chunk. Type contained unlawful characters: {chunk_type:#?}", ))); }; @@ -61,9 +60,10 @@ impl RawGenericChunk { ]; let recalculated_crc = crc::calculate_chunk_data_crc(chunk_type, &data); - if u32::from_be_bytes(crc) != recalculated_crc { + let crc_le = u32::from_be_bytes(crc); + if crc_le != 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))); + return Err(error::DmiError::Generic(format!("Failed to load Chunk of type {chunk_name}. Supplied CRC invalid: {crc:#?}. Its value ({crc_le}) does not match the recalculated one ({recalculated_crc})."))); } Ok(RawGenericChunk { @@ -74,40 +74,36 @@ impl RawGenericChunk { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.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 + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.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 + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.data)?; + let bytes_written = writer.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 + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.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 + "Failed to save Chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; diff --git a/src/error.rs b/src/error.rs index a45e3d0..58b370b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +use png::DecodingError; use std::io; use thiserror::Error; @@ -5,6 +6,8 @@ use thiserror::Error; pub enum DmiError { #[error("IO error: {0}")] Io(#[from] io::Error), + #[error("PNG decoding error: {0}")] + PngDecoding(#[from] DecodingError), #[error("Image-processing error: {0}")] Image(#[from] image::error::ImageError), #[error("FromUtf8 error: {0}")] @@ -19,6 +22,8 @@ pub enum DmiError { CrcMismatch { stated: u32, calculated: u32 }, #[error("Dmi error: {0}")] Generic(String), + #[error("Dmi block entry error: {0}")] + BlockEntry(String), #[error("Dmi IconState error: {0}")] IconState(String), #[error("Encoding error: {0}")] diff --git a/src/icon.rs b/src/icon.rs index f728b6b..ef0daa8 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -1,8 +1,8 @@ use crate::dirs::{Dirs, ALL_DIRS, CARDINAL_DIRS}; -use crate::{error::DmiError, ztxt, RawDmi}; +use crate::{error::DmiError, ztxt, RawDmi, RawDmiMetadata}; +use ::png::{ColorType, Decoder, Transformations}; use image::codecs::png; -use image::GenericImageView; -use image::{imageops, DynamicImage}; +use image::{imageops, RgbaImage}; use std::collections::HashMap; use std::io::prelude::*; use std::io::Cursor; @@ -44,98 +44,261 @@ pub fn dir_to_dmi_index(dir: &Dirs) -> Option { } } -impl Icon { - pub fn load(reader: R) -> Result { - let raw_dmi = RawDmi::load(reader)?; - let chunk_ztxt = match &raw_dmi.chunk_ztxt { - Some(chunk) => chunk.clone(), - None => { - return Err(DmiError::Generic( - "Error loading icon: no zTXt chunk found.".to_string(), - )) - } - }; - let decompressed_text = chunk_ztxt.data.decode()?; - let decompressed_text = String::from_utf8(decompressed_text)?; - let mut decompressed_text = decompressed_text.lines(); - - let current_line = decompressed_text.next(); - if current_line != Some("# BEGIN DMI") { - return Err(DmiError::Generic(format!( - "Error loading icon: no DMI header found. Beginning: {:#?}", - current_line - ))); - }; +struct DmiHeaders { + version: String, + width: Option, + height: Option, +} - let current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: no version header found.".to_string(), - )) +/// Splits the line of a DMI entry into a key/value pair on the equals sign. +/// The second string cannot be empty (a value must exist), or a DmiError is returned. +/// Only one set of quotes is allowed if allow_quotes is true, and it must wrap the entire value. +/// If require_quotes is set, will error if there are not quotes around the value. +/// +/// Other details about this function: +/// +/// Keys have very little validation and are meant to be checked against a known value in most cases. +/// Spaces are only allowed in the value if they are inside quotes or directly after the equals sign (where they are removed). +/// Tabs and equals signs are only allowed in the value if they are not inside quotes. +/// Removes quotes around values and removes backslashes for quotes inside the quotes. +/// Removes backslashes used to escape other backslashes. +fn parse_dmi_line( + line: &str, + allow_quotes: bool, + require_quotes: bool, +) -> Result<(&str, String), DmiError> { + let line_split = line.split_once(" = "); + if line_split.is_none() { + return Err(DmiError::BlockEntry(format!( + "No value was found for line: '{line}' (must contain ' = ')!" + ))); + } + let line_split = line_split.unwrap(); + // Now we need to parse after the equals + let num_chars = line_split.1.len(); + let mut post_equals = String::with_capacity(num_chars - if require_quotes { 2 } else { 0 }); + + // Flags + let mut quoted = false; + let mut escaped = false; + let mut used_quotes = false; + + let value_bytes = line_split.1.as_bytes(); + for (char_idx, char) in value_bytes.iter().enumerate() { + let char = *char as char; + let escape_this_char = escaped; + escaped = false; + match char { + '\\' => { + if !quoted { + return Err(DmiError::Generic(format!( + "Backslash found in line with value '{line}' after first equals without quotes." + ))); + } + if !escape_this_char { + escaped = true; + continue; + } } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "version" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper version header found: {:#?}", - split_version - ))); - }; - let version = split_version[1].to_string(); + '"' => { + if !allow_quotes { + return Err(DmiError::Generic(format!("Quote found in line with value '{line}' after first equals where they are not allowed."))); + } + if !escape_this_char { + if quoted && char_idx + 1 != num_chars { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' ends quotes prior to the last character on the line. This is not allowed."))); + } else if !quoted && !post_equals.is_empty() { + return Err(DmiError::BlockEntry(format!("Line with value '{line}' starts quotes after the first character in its value. This is not allowed."))); + } + quoted = !quoted; + used_quotes = true; + continue; + } + } + '\t' | '=' | ' ' => { + if !quoted { + return Err(DmiError::BlockEntry(format!("Invalid character {char} found in line with value '{line}' after first equals without quotes."))); + } + } + _ => {} + } + post_equals.push(char); + } + if allow_quotes && require_quotes && !used_quotes { + return Err(DmiError::Generic(format!("Line with value '{line}' is required to have quotes after the equals sign, but does not wrap its contents in quotes!"))); + } + Ok((line_split.0, post_equals)) +} - let current_line = match decompressed_text.next() { - Some(thing) => thing, +fn read_dmi_headers( + decompressed_text: &mut std::iter::Peekable>, +) -> Result { + let current_line = decompressed_text.next(); + if current_line != Some("# BEGIN DMI") { + return Err(DmiError::Generic(format!( + "Error loading icon: no DMI header found. Beginning: {current_line:#?}" + ))); + }; + + let current_line = match decompressed_text.next() { + Some(thing) => thing, + None => { + return Err(DmiError::Generic(String::from( + "Error loading icon: no version header found.", + ))) + } + }; + let (key, value) = parse_dmi_line(current_line, false, false)?; + if key != "version" { + return Err(DmiError::Generic(format!( + "Error loading icon: improper version header found: {key} = {value} ('{current_line}')" + ))); + }; + let version = value; + + let mut width = None; + let mut height = None; + for _ in 0..2 { + let current_line = match decompressed_text.peek() { + Some(thing) => *thing, None => { - return Err(DmiError::Generic( - "Error loading icon: no width found.".to_string(), - )) + return Err(DmiError::Generic(String::from( + "Error loading icon: DMI definition abruptly ends.", + ))) } }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\twidth" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper width found: {:#?}", - split_version - ))); - }; - let width = split_version[1].parse::()?; + let (key, value) = parse_dmi_line(current_line, false, false)?; + match key { + "\twidth" => { + width = Some(value.parse::()?); + decompressed_text.next(); // consume the peeked value + } + "\theight" => { + height = Some(value.parse::()?); + decompressed_text.next(); // consume the peeked value + } + _ => { + break; + } + } + } - let current_line = match decompressed_text.next() { - Some(thing) => thing, - None => { - return Err(DmiError::Generic( - "Error loading icon: no height found.".to_string(), - )) + if width == Some(0) || height == Some(0) { + return Err(DmiError::Generic(format!( + "Error loading icon: invalid width ({width:#?}) / height ({height:#?}) values." + ))); + }; + + Ok(DmiHeaders { + version, + width, + height, + }) +} + +impl Icon { + pub fn load(reader: R) -> Result { + Self::load_internal(reader, true) + } + + /// Returns an Icon {} without any images inside of the IconStates and with less error validation. + /// This is suitable for reading DMI metadata without caring about the actual images within. + /// Can load a full DMI about 10x faster than Icon::load. + pub fn load_meta(reader: R) -> Result { + Self::load_internal(reader, false) + } + + fn load_internal(reader: R, load_images: bool) -> Result { + let (dmi_meta, rgba_bytes) = if load_images { + let raw_dmi = RawDmi::load(reader)?; + + // Reconstruct the full PNG from memory. Preallocating the size saves a lot of compute here. + let mut png_data = Vec::with_capacity(raw_dmi.output_buffer_size(false)); + raw_dmi.save(&mut png_data, false)?; + + let mut png_decoder = Decoder::new(std::io::Cursor::new(png_data)); + // this will convert RGB->RGBA and increase bit depth to 8, interpret tRNS chunks, interpret PLTE chunks + // notably does not convert greyscale color types to RGB. + png_decoder.set_transformations(Transformations::EXPAND | Transformations::ALPHA); + let mut png_reader = png_decoder.read_info()?; + let mut rgba_buf = vec![0u8; png_reader.output_buffer_size()]; + let info = png_reader.next_frame(&mut rgba_buf)?; + + // EXPAND and ALPHA do not expand grayscale images into RGBA. We can just do this manually. + match info.color_type { + ColorType::GrayscaleAlpha => { + if rgba_buf.len() as u32 != info.width * info.height * 2 { + return Err(DmiError::Generic(String::from( + "GrayscaleAlpha buffer length mismatch", + ))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for chunk in rgba_buf.chunks(2) { + let gray = chunk[0]; + let alpha = chunk[1]; + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(alpha); + } + rgba_buf = new_buf; + } + ColorType::Grayscale => { + if rgba_buf.len() as u32 != info.width * info.height { + return Err(DmiError::Generic(String::from( + "Grayscale buffer length mismatch", + ))); + } + let mut new_buf = Vec::with_capacity((info.width * info.height * 4) as usize); + for gray in rgba_buf { + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(gray); + new_buf.push(255); + } + rgba_buf = new_buf; + } + ColorType::Rgba => {} + _ => { + return Err(DmiError::Generic(format!( + "Unsupported ColorType (must be RGBA or convertible to RGBA): {:#?}", + info.color_type + ))); + } } - }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "\theight" { - return Err(DmiError::Generic(format!( - "Error loading icon: improper height found: {:#?}", - split_version - ))); - }; - let height = split_version[1].parse::()?; - if width == 0 || height == 0 { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid width ({}) / height ({}) values.", - width, height - ))); + let dmi_meta = RawDmiMetadata { + chunk_ihdr: raw_dmi.chunk_ihdr, + chunk_ztxt: raw_dmi.chunk_ztxt.ok_or_else(|| { + DmiError::Generic(String::from("Error loading icon: no zTXt chunk found.")) + })?, + }; + + (dmi_meta, Some(rgba_buf)) + } else { + (RawDmi::load_meta(reader)?, None) }; - // Image time. - let mut reader = vec![]; - raw_dmi.save(&mut reader)?; - let base_image = image::load_from_memory_with_format(&reader, image::ImageFormat::Png)?; + let chunk_ztxt = &dmi_meta.chunk_ztxt; + let decompressed_text = chunk_ztxt.data.decode()?; + let decompressed_text = String::from_utf8(decompressed_text)?; + let mut decompressed_text = decompressed_text.lines().peekable(); + + let dmi_headers = read_dmi_headers(&mut decompressed_text)?; + let version = dmi_headers.version; + + // yes you can make a DMI without a width or height. it defaults to 32x32 + let width = dmi_headers.width.unwrap_or(32); + let height = dmi_headers.height.unwrap_or(32); - let dimensions = base_image.dimensions(); - let img_width = dimensions.0; - let img_height = dimensions.1; + let ihdr_data = dmi_meta.chunk_ihdr.data; + + let img_width: u32 = + u32::from_be_bytes([ihdr_data[0], ihdr_data[1], ihdr_data[2], ihdr_data[3]]); + let img_height = u32::from_be_bytes([ihdr_data[4], ihdr_data[5], ihdr_data[6], ihdr_data[7]]); if img_width == 0 || img_height == 0 || img_width % width != 0 || img_height % height != 0 { - return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({}) / height ({}) values. Missmatch with metadata width ({}) / height ({}).", img_width, img_height, width, height))); + return Err(DmiError::Generic(format!("Error loading icon: invalid image width ({img_width}) / height ({img_height}) values. Mismatch with metadata width ({width}) / height ({height})."))); }; let width_in_states = img_width / width; @@ -156,32 +319,18 @@ impl Icon { let mut states = vec![]; loop { - if current_line.contains("# END DMI") { + if current_line == "# END DMI" { break; }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 || split_version[0] != "state" { + let (key, value) = parse_dmi_line(current_line, true, true)?; + if key != "state" { return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version + "Error loading icon: Was expecting the next line's entry to have a key of 'state', but encountered '{key}'! The full line contents are as follows: '{current_line}'" ))); }; - let name = split_version[1].as_bytes(); - if !name.starts_with(b"\"") || !name.ends_with(b"\"") { - return Err(DmiError::Generic(format!("Error loading icon: invalid name icon_state found in metadata, should be preceded and succeeded by double-quotes (\"): {:#?}", name))); - }; - let name = match name.len() { - 0 | 1 => { - return Err(DmiError::Generic(format!( - "Error loading icon: invalid name icon_state found in metadata, improper size: {:#?}", - name - ))) - } - 2 => String::new(), //Only the quotes, empty name otherwise. - length => String::from_utf8(name[1..(length - 1)].to_vec())?, //Hacky way to trim. Blame the cool methods being nightly experimental. - }; + let name: String = value; let mut dirs = None; let mut frames = None; @@ -202,38 +351,31 @@ impl Icon { } }; - if current_line.contains("# END DMI") || current_line.contains("state = \"") { + if current_line == "# END DMI" || !current_line.starts_with('\t') { break; }; - let split_version: Vec<&str> = current_line.split_terminator(" = ").collect(); - if split_version.len() != 2 { - return Err(DmiError::Generic(format!( - "Error loading icon: improper state found: {:#?}", - split_version - ))); - }; + let (key, value) = parse_dmi_line(current_line, false, false)?; - match split_version[0] { - "\tdirs" => dirs = Some(split_version[1].parse::()?), - "\tframes" => frames = Some(split_version[1].parse::()?), + match key { + "\tdirs" => dirs = Some(value.parse::()?), + "\tframes" => frames = Some(value.parse::()?), "\tdelay" => { - let mut delay_vector = vec![]; - let text_delays = split_version[1].split_terminator(','); + let mut delay_vector = Vec::with_capacity(frames.unwrap_or(0) as usize); + let text_delays = value.split_terminator(','); for text_entry in text_delays { delay_vector.push(text_entry.parse::()?); } delay = Some(delay_vector); } - "\tloop" => loop_flag = Looping::new(split_version[1].parse::()?), - "\trewind" => rewind = split_version[1].parse::()? != 0, - "\tmovement" => movement = split_version[1].parse::()? != 0, + "\tloop" => loop_flag = Looping::new(value.parse::()?), + "\trewind" => rewind = value.parse::()? != 0, + "\tmovement" => movement = value.parse::()? != 0, "\thotspot" => { - let text_coordinates: Vec<&str> = split_version[1].split_terminator(',').collect(); + let text_coordinates: Vec<&str> = value.split_terminator(',').collect(); // Hotspot includes a mysterious 3rd parameter that always seems to be 1. if text_coordinates.len() != 3 { return Err(DmiError::Generic(format!( - "Error loading icon: improper hotspot found: {:#?}", - split_version + "Error loading icon: improper hotspot found: {current_line:#?}" ))); }; hotspot = Some(Hotspot { @@ -242,46 +384,67 @@ impl Icon { }); } _ => { - unknown_settings = match unknown_settings { - None => { - let mut new_map = HashMap::new(); - new_map.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(new_map) - } - Some(mut thing) => { - thing.insert(split_version[0].to_string(), split_version[1].to_string()); - Some(thing) - } - }; + if let Some((key, value)) = current_line.split_once(" = ") { + unknown_settings = match unknown_settings { + None => { + let mut new_map = HashMap::new(); + new_map.insert(key.to_string(), value.to_string()); + Some(new_map) + } + Some(mut thing) => { + thing.insert(key.to_string(), value.to_string()); + Some(thing) + } + }; + } } }; } if dirs.is_none() || frames.is_none() { return Err(DmiError::Generic(format!( - "Error loading icon: state lacks essential settings. dirs: {:#?}. frames: {:#?}.", - dirs, frames + "Error loading icon: state lacks essential settings. dirs: {dirs:#?}. frames: {frames:#?}." ))); }; let dirs = dirs.unwrap(); let frames = frames.unwrap(); - if index + (dirs as u32 * frames) > max_possible_states { - return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({}).", max_possible_states))); + let next_index = index + (dirs as u32 * frames); + if next_index > max_possible_states { + return Err(DmiError::Generic(format!("Error loading icon: metadata settings exceeded the maximum number of states possible ({max_possible_states})."))); }; - let mut images = vec![]; + let mut images = Vec::with_capacity((frames * dirs as u32) as usize); + + if let Some(rgba_bytes) = &rgba_bytes { + const RGBA_PIXEL_STRIDE: usize = 4; + let row_stride = img_width as usize * RGBA_PIXEL_STRIDE; + let expected_buffer_len = row_stride * (img_height as usize); + if rgba_bytes.len() != expected_buffer_len { + panic!("{} != {}", rgba_bytes.len(), expected_buffer_len); + } + + for image_idx in index..next_index { + let x = (image_idx % width_in_states) * width; + let y = (image_idx / width_in_states) * height; + + let mut cropped = + Vec::with_capacity((width * height * RGBA_PIXEL_STRIDE as u32) as usize); + for row in y..(y + height) { + let start = (row as usize * row_stride) + (x as usize * RGBA_PIXEL_STRIDE); + let end = start + (width as usize * RGBA_PIXEL_STRIDE); + cropped.extend_from_slice(&rgba_bytes[start..end]); + } + + let tile = image::ImageBuffer::, _>::from_raw(width, height, cropped) + .ok_or_else(|| DmiError::Generic("Failed to create image tile".to_string()))?; - for _frame in 0..frames { - for _dir in 0..dirs { - let x = (index % width_in_states) * width; - //This operation rounds towards zero, truncating any fractional part of the exact result, essentially a floor() function. - let y = (index / width_in_states) * height; - images.push(base_image.crop_imm(x, y, width, height)); - index += 1; + images.push(tile); } } + index = next_index; + states.push(IconState { name, dirs, @@ -304,7 +467,7 @@ impl Icon { }) } - pub fn save(&self, mut writter: &mut W) -> Result { + pub fn save(&self, mut writer: &mut W) -> Result { let mut sprites = vec![]; let mut signature = format!( "# BEGIN DMI\nversion = {}\n\twidth = {}\n\theight = {}\n", @@ -318,14 +481,16 @@ impl Icon { signature.push_str(&format!( "state = \"{}\"\n\tdirs = {}\n\tframes = {}\n", - icon_state.name, icon_state.dirs, icon_state.frames + icon_state.name.replace("\\", "\\\\").replace("\"", "\\\""), + icon_state.dirs, + icon_state.frames )); if icon_state.frames > 1 { match &icon_state.delay { Some(delay) => { if delay.len() as u32 != icon_state.frames { - return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) differs from the delay entry ({:3?}). Name: \"{}\".", icon_state.frames, delay, icon_state.name))) + return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) differs from the delay entry ({delay:3?}). Name: \"{}\".", icon_state.frames, icon_state.name))) }; let delay: Vec= delay.iter().map(|&c| c.to_string()).collect(); signature.push_str(&format!("\tdelay = {}\n", delay.join(","))); @@ -333,7 +498,7 @@ impl Icon { None => return Err(DmiError::Generic(format!("Error saving Icon: number of frames ({}) larger than one without a delay entry in icon state of name \"{}\".", icon_state.frames, icon_state.name))) }; if let Looping::NTimes(flag) = icon_state.loop_flag { - signature.push_str(&format!("\tloop = {}\n", flag)) + signature.push_str(&format!("\tloop = {flag}\n")) } if icon_state.rewind { signature.push_str("\trewind = 1\n"); @@ -353,7 +518,7 @@ impl Icon { if let Some(hashmap) = &icon_state.unknown_settings { for (setting, value) in hashmap.iter() { - signature.push_str(&format!("\t{} = {}\n", setting, value)); + signature.push_str(&format!("\t{setting} = {value}\n")); } }; @@ -395,7 +560,7 @@ impl Icon { new_dmi.chunk_ztxt = Some(new_ztxt); - new_dmi.save(&mut writter) + new_dmi.save(&mut writer, true) } } @@ -421,7 +586,10 @@ pub enum Looping { impl Looping { /// Creates a new `NTimes` variant with `x` number of times to loop pub fn new(x: u32) -> Self { - Self::NTimes(NonZeroU32::new(x).unwrap()) + match x { + 0 => Self::default(), + _ => Self::NTimes(NonZeroU32::new(x).unwrap()), + } } /// Unwraps the Looping yielding the `u32` if the `Looping` is a `Looping::NTimes` @@ -488,7 +656,7 @@ pub struct IconState { pub name: String, pub dirs: u8, pub frames: u32, - pub images: Vec, + pub images: Vec, pub delay: Option>, pub loop_flag: Looping, pub rewind: bool, @@ -500,7 +668,7 @@ pub struct IconState { impl IconState { /// Gets a specific DynamicImage from `images`, given a dir and frame. /// If the dir or frame is invalid, returns a DmiError. - pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&DynamicImage, DmiError> { + pub fn get_image(&self, dir: &Dirs, frame: u32) -> Result<&RgbaImage, DmiError> { if self.frames < frame { return Err(DmiError::IconState(format!( "Specified frame \"{frame}\" is larger than the number of frames ({}) for icon_state \"{}\"", diff --git a/src/iend.rs b/src/iend.rs index 62344c9..0b45fad 100644 --- a/src/iend.rs +++ b/src/iend.rs @@ -45,8 +45,8 @@ impl RawIendChunk { ]; if data_length != default_iend_chunk.data_length { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. Lengh field value: {:#?}. Expected: {:#?}.", - data_length, default_iend_chunk.data_length + "Failed to load RawIendChunk from reader. Lengh field value: {data_length:#?}. Expected: {:#?}.", + default_iend_chunk.data_length ))); } @@ -58,8 +58,8 @@ impl RawIendChunk { ]; if chunk_type != default_iend_chunk.chunk_type { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. Chunk type: {:#?}. Expected {:#?}.", - chunk_type, default_iend_chunk.chunk_type + "Failed to load RawIendChunk from reader. Chunk type: {chunk_type:#?}. Expected {:#?}.", + default_iend_chunk.chunk_type ))); } @@ -71,39 +71,36 @@ impl RawIendChunk { ]; if crc != default_iend_chunk.crc { return Err(error::DmiError::Generic(format!( - "Failed to load RawIendChunk from reader. CRC: {:#?}. Expected {:#?}.", - crc, default_iend_chunk.crc + "Failed to load RawIendChunk from reader. CRC: {crc:#?}. Expected {:#?}.", + default_iend_chunk.crc ))); } Ok(default_iend_chunk) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.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 IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.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 IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.write(&self.crc)?; total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( - "Failed to save IEND chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save IEND chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -129,8 +126,7 @@ impl TryFrom for RawIendChunk { fn try_from(raw_generic_chunk: chunk::RawGenericChunk) -> Result { if !raw_generic_chunk.data.is_empty() { return Err(error::DmiError::Generic(format!( - "Failed to convert RawGenericChunk into RawIendChunk. Non-empty data field. Chunk: {:#?}.", - raw_generic_chunk + "Failed to convert RawGenericChunk into RawIendChunk. Non-empty data field. Chunk: {raw_generic_chunk:#?}." ))); }; diff --git a/src/lib.rs b/src/lib.rs index e97250d..01c0864 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,10 +6,12 @@ pub mod icon; pub mod iend; pub mod ztxt; -use std::io::{Read, Write}; +use std::io::{Cursor, Read, Seek, Write}; /// The PNG magic header pub const PNG_HEADER: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10]; +pub const IHDR_HEADER: [u8; 8] = [0, 0, 0, 13, 73, 72, 68, 82]; +const ASSUMED_ZTXT_MAX: usize = 500; #[derive(Clone, Eq, PartialEq, Debug, Default)] pub struct RawDmi { @@ -22,6 +24,31 @@ pub struct RawDmi { pub chunk_iend: iend::RawIendChunk, } +#[derive(Clone, Eq, PartialEq, Debug, Default)] +pub struct RawDmiMetadata { + pub chunk_ihdr: chunk::RawGenericChunk, + pub chunk_ztxt: ztxt::RawZtxtChunk, +} + +fn ensure_buffered_bytes( + buffered_bytes: &mut Cursor>, + source_reader: &mut R, + source_amount_read: &mut usize, + additional_length_required: usize, +) -> Result<(), error::DmiError> { + let original_position = buffered_bytes.position(); + if original_position + additional_length_required as u64 > *source_amount_read as u64 { + let mut new_bytes = vec![0u8; additional_length_required]; + source_reader.read_exact(&mut new_bytes)?; + // Append all the new bytes to our cursor and go back to our old spot + buffered_bytes.seek_relative(*source_amount_read as i64 - original_position as i64)?; + buffered_bytes.write_all(&new_bytes)?; + *source_amount_read += new_bytes.len(); + buffered_bytes.seek_relative(original_position as i64 - *source_amount_read as i64)?; + } + Ok(()) +} + impl RawDmi { pub fn new() -> RawDmi { RawDmi { @@ -47,15 +74,14 @@ impl RawDmi { let header = &dmi_bytes[0..8]; if dmi_bytes[0..8] != PNG_HEADER { return Err(error::DmiError::Generic(format!( - "PNG header mismatch (expected {:#?}, found {:#?})", - PNG_HEADER, header + "PNG header mismatch (expected {PNG_HEADER:#?}, found {header:#?})" ))); }; let header = PNG_HEADER; let mut chunk_ihdr = None; let mut chunk_ztxt = None; let mut chunk_plte = None; - let mut chunks_idat = vec![]; + let mut chunks_idat: Vec = vec![]; let chunk_iend; let mut other_chunks = vec![]; @@ -64,9 +90,9 @@ impl RawDmi { 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(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IEND chunk.", + ))); } let chunk_data_length = u32::from_be_bytes([ @@ -94,14 +120,14 @@ impl RawDmi { } } if chunk_ihdr.is_none() { - return Err(error::DmiError::Generic( - "Failed to load DMI. Buffer end reached without finding an IHDR chunk.".to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IHDR chunk.", + ))); }; if chunks_idat.is_empty() { - return Err(error::DmiError::Generic( - "Failed to load DMI. Buffer end reached without finding an IDAT chunk.".to_string(), - )); + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. Buffer end reached without finding an IDAT chunk.", + ))); } let other_chunks = match other_chunks.len() { 0 => None, @@ -121,77 +147,207 @@ impl RawDmi { }) } - pub fn save(&self, mut writter: &mut W) -> Result { - let bytes_written = writter.write(&self.header)?; + /// Equivalent of load, but only parses IHDR and zTXt. May not catch an improperly formatted PNG file, because it only reads those headers. + pub fn load_meta(mut reader: R) -> Result { + let mut dmi_bytes = vec![0u8; ASSUMED_ZTXT_MAX]; + + // Since we only want the zTXt it's unlikely to be any longer than ASSUMED_ZTXT_MAX bytes when combined with headers until we encounter it + // If the zTxt is especially long and its length exceeds our index we can read extra bytes later. + let mut dmi_bytes_read = reader.read(&mut dmi_bytes)?; + + if dmi_bytes_read < 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 mut buffered_dmi_bytes = Cursor::new(dmi_bytes); + + // 8 bytes for the PNG file signature. + let mut png_header = [0u8; 8]; + buffered_dmi_bytes.read_exact(&mut png_header)?; + if png_header != PNG_HEADER { + return Err(error::DmiError::Generic(format!( + "PNG header mismatch (expected {PNG_HEADER:#?}, found {png_header:#?})" + ))); + }; + // 4 (size) + 4 (type) + 13 (data) + 4 (crc) for the IHDR chunk. + let mut ihdr = [0u8; 25]; + buffered_dmi_bytes.read_exact(&mut ihdr)?; + if ihdr[0..8] != IHDR_HEADER { + return Err(error::DmiError::Generic( + String::from("Failed to load DMI. IHDR chunk is not in the correct location (1st chunk), has an invalid size, or an invalid identifier."), + )); + } + let chunk_ihdr = chunk::RawGenericChunk::load(&mut &ihdr[0..25])?; + + let mut chunk_ztxt = None; + + loop { + // Read len[u8; 4] + header[u8; 4] + let mut chunk_header_full: [u8; 8] = [0u8; 8]; + buffered_dmi_bytes.read_exact(&mut chunk_header_full)?; + + let chunk_len = u32::from_be_bytes([ + chunk_header_full[0], + chunk_header_full[1], + chunk_header_full[2], + chunk_header_full[3], + ]) as usize; + + // Read header + let chunk_header_type = &chunk_header_full[4..8]; + + // If we encounter IDAT or IEND we can just break because the zTXt header aint happening + if chunk_header_type == b"IDAT" || chunk_header_type == b"IEND" { + break; + } + + // Skip non-zTXt chunks + if chunk_header_type != b"zTXt" { + // We will overread the file's buffer on our seek. + // Read the remainder of the chunk + 4 bytes for CRC + 8 bytes for the next header. + // There will always be a next header because IEND headers break before this check. + ensure_buffered_bytes( + &mut buffered_dmi_bytes, + &mut reader, + &mut dmi_bytes_read, + chunk_len + 12, + )?; + buffered_dmi_bytes.seek_relative((chunk_len + 4) as i64)?; + continue; + } + + // Make sure we have enough bytes to finish the zTXt chunk and nothing else. + ensure_buffered_bytes( + &mut buffered_dmi_bytes, + &mut reader, + &mut dmi_bytes_read, + chunk_len + 4, + )?; + + // Create vec for full chunk data + let mut chunk_full: Vec = Vec::with_capacity(chunk_len + 12); + + // Fill it up with the data we already have + chunk_full.extend_from_slice(&chunk_header_full); + + // Read actual chunk data + CRC and append + let mut chunk_data = vec![0; chunk_len + 4]; + buffered_dmi_bytes.read_exact(&mut chunk_data)?; + chunk_full.extend_from_slice(&chunk_data); + + let raw_chunk = chunk::RawGenericChunk::load(&mut &*chunk_full)?; + + chunk_ztxt = Some(ztxt::RawZtxtChunk::try_from(raw_chunk)?); + // We got all we need, let's gooo + break; + } + + if chunk_ztxt.is_none() { + return Err(error::DmiError::Generic(String::from( + "Failed to load DMI. zTXt chunk was not found or is after the first IDAT chunk.", + ))); + } + let chunk_ztxt = chunk_ztxt.unwrap(); + + Ok(RawDmiMetadata { + chunk_ihdr, + chunk_ztxt, + }) + } + + /// Calculates the size of a buffer needed to save this DMI with RawDmi::save. + pub fn output_buffer_size(&self, include_ztxt: bool) -> usize { + let mut total_bytes = 45; + if include_ztxt { + if let Some(chunk_ztxt) = &self.chunk_ztxt { + total_bytes += chunk_ztxt.data.keyword.len() + chunk_ztxt.data.compressed_text.len() + 14 + } + } + if let Some(chunk_plte) = &self.chunk_plte { + total_bytes += chunk_plte.data.len() + 12 + } + if let Some(other_chunks) = &self.other_chunks { + for chunk in other_chunks { + total_bytes += chunk.data.len() + 12 + } + } + for idat in &self.chunks_idat { + total_bytes += idat.data.len() + 12; + } + + total_bytes + } + + pub fn save( + &self, + mut writer: &mut W, + include_ztxt: bool, + ) -> Result { + let bytes_written = writer.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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = self.chunk_ihdr.save(&mut writter)?; + let bytes_written = self.chunk_ihdr.save(&mut writer)?; 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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - if let Some(chunk_ztxt) = &self.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 - ))); + if include_ztxt { + if let Some(chunk_ztxt) = &self.chunk_ztxt { + let bytes_written = chunk_ztxt.save(&mut writer)?; + 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 {total_bytes_written} bytes written." + ))); + }; }; - }; + } if let Some(chunk_plte) = &self.chunk_plte { - let bytes_written = chunk_plte.save(&mut writter)?; + let bytes_written = chunk_plte.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk_plte.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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; }; if let Some(other_chunks) = &self.other_chunks { for chunk in other_chunks { - let bytes_written = chunk.save(&mut writter)?; + let bytes_written = chunk.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; } } for chunk in &self.chunks_idat { - let bytes_written = chunk.save(&mut writter)?; + let bytes_written = chunk.save(&mut writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(chunk.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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; } - let bytes_written = self.chunk_iend.save(&mut writter)?; + let bytes_written = self.chunk_iend.save(&mut writer)?; 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 + "Failed to save DMI. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; diff --git a/src/ztxt.rs b/src/ztxt.rs index ecbd48a..63303a4 100644 --- a/src/ztxt.rs +++ b/src/ztxt.rs @@ -61,8 +61,7 @@ impl RawZtxtChunk { ]; if chunk_type != ZTXT_TYPE { return Err(error::DmiError::Generic(format!( - "Failed to load RawZtxtChunk from reader. Chunk type is not zTXt: {:#?}. Should be {:#?}.", - chunk_type, ZTXT_TYPE + "Failed to load RawZtxtChunk from reader. Chunk type is not zTXt: {chunk_type:#?}. Should be {ZTXT_TYPE:#?}.", ))); } let data_bytes = &raw_chunk_bytes[8..(total_bytes_length - 4)].to_vec(); @@ -74,8 +73,9 @@ impl RawZtxtChunk { raw_chunk_bytes[total_bytes_length - 1], ]; let calculated_crc = crc::calculate_chunk_data_crc(chunk_type, data_bytes); - if u32::from_be_bytes(crc) != calculated_crc { - return Err(error::DmiError::Generic(format!("Failed to load RawZtxtChunk from reader. Given CRC ({}) does not match the calculated one ({}).", u32::from_be_bytes(crc), calculated_crc))); + let crc_le = u32::from_be_bytes(crc); + if crc_le != calculated_crc { + return Err(error::DmiError::Generic(format!("Failed to load RawZtxtChunk from reader. Given CRC ({crc_le}) does not match the calculated one ({calculated_crc})."))); } Ok(RawZtxtChunk { data_length, @@ -85,40 +85,36 @@ impl RawZtxtChunk { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.data_length)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.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 zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.chunk_type)?; + let bytes_written = writer.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 zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = self.data.save(&mut *writter)?; + let bytes_written = self.data.save(&mut *writer)?; total_bytes_written += bytes_written; if bytes_written < u32::from_be_bytes(self.data_length) as usize { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.crc)?; + let bytes_written = writer.write(&self.crc)?; total_bytes_written += bytes_written; if bytes_written < self.crc.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt chunk. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt chunk. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -162,8 +158,7 @@ impl TryFrom for RawZtxtChunk { let chunk_type = raw_generic_chunk.chunk_type; if chunk_type != ZTXT_TYPE { return Err(error::DmiError::Generic(format!( - "Failed to convert RawGenericChunk into RawZtxtChunk. Wrong type: {:#?}. Expected: {:#?}.", - chunk_type, ZTXT_TYPE + "Failed to convert RawGenericChunk into RawZtxtChunk. Wrong type: {chunk_type:#?}. Expected: {ZTXT_TYPE:#?}." ))); }; let chunk_data = &raw_generic_chunk.data; @@ -228,8 +223,7 @@ impl RawZtxtData { let null_separator = 0; let compression_method = data_bytes_iter.next().ok_or_else(|| { error::DmiError::Generic(format!( - "Failed to load RawZtxtData from reader, during compression method reading.\nVector: {:#?}", - data_bytes + "Failed to load RawZtxtData from reader, during compression method reading.\nVector: {data_bytes:#?}" )) })?; //let compressed_text = RawCompressedText::try_from(back_to_vector)?; @@ -244,40 +238,36 @@ impl RawZtxtData { }) } - pub fn save(&self, writter: &mut W) -> Result { - let bytes_written = writter.write(&self.keyword)?; + pub fn save(&self, writer: &mut W) -> Result { + let bytes_written = writer.write(&self.keyword)?; let mut total_bytes_written = bytes_written; if bytes_written < self.keyword.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&[self.null_separator])?; + let bytes_written = writer.write(&[self.null_separator])?; total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&[self.compression_method])?; + let bytes_written = writer.write(&[self.compression_method])?; total_bytes_written += bytes_written; if bytes_written < 1 { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; - let bytes_written = writter.write(&self.compressed_text)?; + let bytes_written = writer.write(&self.compressed_text)?; total_bytes_written += bytes_written; if bytes_written < self.compressed_text.len() { return Err(error::DmiError::Generic(format!( - "Failed to save zTXt data. Buffer unable to hold the data, only {} bytes written.", - total_bytes_written + "Failed to save zTXt data. Buffer unable to hold the data, only {total_bytes_written} bytes written." ))); }; @@ -288,8 +278,7 @@ impl RawZtxtData { match inflate::inflate_bytes_zlib(&self.compressed_text) { Ok(decompressed_text) => Ok(decompressed_text), Err(text) => Err(error::DmiError::Generic(format!( - "Failed to read compressed text. Error: {}", - text + "Failed to read compressed text. Error: {text}" ))), } } diff --git a/tests/dmi_ops.rs b/tests/dmi_ops.rs index cc2e2a6..df4fbe1 100644 --- a/tests/dmi_ops.rs +++ b/tests/dmi_ops.rs @@ -1,18 +1,73 @@ -use dmi::icon::Icon; +use dmi::icon::{DmiVersion, Icon, IconState, Looping}; use std::fs::File; use std::path::PathBuf; #[test] fn load_and_save_dmi() { + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + load_path.push("tests/resources/empty.dmi"); + let load_file = + File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No empty dmi: {load_path:?}")); + let _ = Icon::load(&load_file).expect("Unable to load empty dmi"); + + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + load_path.push("tests/resources/greyscale_alpha.dmi"); + let load_file = File::open(load_path.as_path()) + .unwrap_or_else(|_| panic!("No greyscale_alpha dmi: {load_path:?}")); + let _ = Icon::load(&load_file).expect("Unable to greyscale_alpha dmi"); + let mut load_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); load_path.push("tests/resources/load_test.dmi"); let load_file = File::open(load_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); let lights_icon = Icon::load(&load_file).expect("Unable to load lights dmi"); + + assert_eq!(lights_icon.version, DmiVersion::default()); + assert_eq!(lights_icon.width, 160); + assert_eq!(lights_icon.height, 160); + assert_eq!(lights_icon.states.len(), 4); + + assert_default_state(&lights_icon.states[0], "0_1"); + assert_default_state(&lights_icon.states[1], "1_1"); + assert_default_state(&lights_icon.states[2], ""); + assert_default_state( + &lights_icon.states[3], + "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\"", + ); + let mut write_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); write_path.push("tests/resources/save_test.dmi"); let mut write_file = File::create(write_path.as_path()).expect("Failed to create dmi file"); let _written_dmi = lights_icon .save(&mut write_file) .expect("Failed to save lights dmi"); + + let load_write_file = + File::open(write_path.as_path()).unwrap_or_else(|_| panic!("No lights dmi: {load_path:?}")); + let reloaded_lights_icon = Icon::load_meta(&load_write_file).expect("Unable to load lights dmi"); + + assert_eq!(reloaded_lights_icon.version, DmiVersion::default()); + assert_eq!(reloaded_lights_icon.width, 160); + assert_eq!(reloaded_lights_icon.height, 160); + assert_eq!(reloaded_lights_icon.states.len(), 4); + + assert_default_state(&reloaded_lights_icon.states[0], "0_1"); + assert_default_state(&reloaded_lights_icon.states[1], "1_1"); + assert_default_state(&reloaded_lights_icon.states[2], ""); + assert_default_state( + &reloaded_lights_icon.states[3], + "\\\\ \\ \\\"\\t\\st\\\\\\T+e=5235=!\"", + ); +} + +fn assert_default_state(state: &IconState, name: &'static str) { + assert_eq!(state.name, name); + assert_eq!(state.dirs, 1); + assert_eq!(state.frames, 1); + assert_eq!(state.delay, None); + assert_eq!(state.loop_flag, Looping::Indefinitely); + assert_eq!(state.rewind, false); + assert_eq!(state.movement, false); + assert_eq!(state.hotspot, None); + assert_eq!(state.unknown_settings, None); } diff --git a/tests/resources/empty.dmi b/tests/resources/empty.dmi new file mode 100644 index 0000000..6c4f2b3 Binary files /dev/null and b/tests/resources/empty.dmi differ diff --git a/tests/resources/greyscale_alpha.dmi b/tests/resources/greyscale_alpha.dmi new file mode 100644 index 0000000..cafaa8f Binary files /dev/null and b/tests/resources/greyscale_alpha.dmi differ diff --git a/tests/resources/load_test.dmi b/tests/resources/load_test.dmi index 5fe7793..b61121e 100644 Binary files a/tests/resources/load_test.dmi and b/tests/resources/load_test.dmi differ diff --git a/tests/resources/save_test.dmi b/tests/resources/save_test.dmi deleted file mode 100644 index dd84169..0000000 Binary files a/tests/resources/save_test.dmi and /dev/null differ