diff --git a/README.md b/README.md index b5ba707..cab25f0 100644 --- a/README.md +++ b/README.md @@ -27,5 +27,5 @@ Where: # License -NML is licensed under the GNU AGPL version 3 or later. See [LICENSE.md](LICENSE.md) for more information. +png_data is licensed under the GNU AGPL version 3 or later. See [LICENSE.md](LICENSE.md) for more information. License for third-party dependencies can be accessed via `cargo license` diff --git a/src/png_data/header.rs b/src/png_data/header.rs new file mode 100644 index 0000000..aafd546 --- /dev/null +++ b/src/png_data/header.rs @@ -0,0 +1,173 @@ +use crc::Crc; + + +pub trait Encode { + /// Encode the data into a vector + fn encode(&self, vec: &mut Vec); +} + +pub trait Decode { + type Type; + + /// Decode the data from an iterator + fn decode(it: &mut I) -> Result + where I: Iterator; +} + +/// The program's version. +/// Used for compatibility reasons. +#[repr(u16)] +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, Copy)] +pub enum Version { + VERSION_1, +} + +impl TryFrom for Version { + type Error = String; + + fn try_from(value: u16) -> Result { + match value { + 0 => Ok(Version::VERSION_1), + ver => Err(format!("Unknown version: {ver}")), + } + } +} + +#[derive(Debug)] +pub struct Header { + pub version: Version, + pub data_len: u32, + pub data_crc: u32, + pub comment: Option, +} + +impl Header { + /// Construct a new header from the embedded data + pub fn new(version: Version, data: &[u8], comment: Option) -> Result { + if data.len() > u32::MAX as usize { + return Err(format!( + "Embedded data length: {} is greater than maximum {}", + data.len(), + u32::MAX + )); + } else if let Some(len) = comment.as_ref().map(|c| c.len()) { + if len > u16::MAX as usize { + return Err(format!( + "Embedded comment is too long, maximum length: {}, got {len}", + u16::MAX + )); + } + } + + Ok(Self { + version, + data_len: data.len() as u32, + data_crc: Crc::::new(&crc::CRC_32_CKSUM).checksum(data), + comment, + }) + } +} + +impl Encode for Header { + fn encode(&self, vec: &mut Vec) { + // Version + vec.extend_from_slice((self.version as u16).to_le_bytes().as_slice()); + + // Data Len + vec.extend_from_slice(self.data_len.to_le_bytes().as_slice()); + + // Data CRC + vec.extend_from_slice(self.data_crc.to_le_bytes().as_slice()); + + // Comment length + let comment_length = self.comment.as_ref().map_or(0u16, |c| c.len() as u16); + vec.extend_from_slice(comment_length.to_le_bytes().as_slice()); + + // Comment + if let Some(comment) = &self.comment { + vec.extend_from_slice(comment.as_bytes()); + } + } +} + +impl Decode for Header { + type Type = Header; + + fn decode(it: &mut I) -> Result + where I: Iterator { + let mut count = 0; + let mut next = || -> Result { + let result = it + .next() + .ok_or(format!("Failed to get byte at index: {count}")); + count += 1; + result + }; + + let version = u16::from_le_bytes([next()?, next()?]); + let data_len = u32::from_le_bytes([next()?, next()?, next()?, next()?]); + let data_crc = u32::from_le_bytes([next()?, next()?, next()?, next()?]); + let comment_length = u16::from_le_bytes([next()?, next()?]); + + let comment = if comment_length != 0 { + let mut comment_data = Vec::with_capacity(comment_length as usize); + for _ in 0..comment_length { + comment_data.push(next()?); + } + + Some( + String::from_utf8(comment_data) + .map_err(|e| format!("Failed to retrieve comment: {e}"))?, + ) + } else { + None + }; + + Ok(Header { + version: Version::try_from(version)?, + data_len, + data_crc, + comment, + }) + } +} + +/* +#[derive(Debug)] +pub struct HeaderCrypt { + pub version: Version, + pub nonce: Vec, + pub data_len: u32, + pub data_crc: u32, + pub comment: Option, +} + +impl HeaderCrypt { + /// Construct a new header from the embedded data + pub fn new(version: Version, nonce: Vec, data: &[u8], comment: Option) -> Result { + if data.len() > u32::MAX as usize { + return Err(format!( + "Embedded data length: {} is greater than maximum {}", + data.len(), + u32::MAX + )); + } else if let Some(len) = comment.as_ref().map(|c| c.len()) { + if len > u16::MAX as usize { + return Err(format!( + "Embedded comment is too long, maximum length: {}, got {len}", + u16::MAX + )); + } + } + + Ok(Self { + version, + nonce, + data_len: data.len() as u32, + data_crc: Crc::::new(&crc::CRC_32_CKSUM).checksum(data), + comment, + }) + } +} +*/ diff --git a/src/png_data/main.rs b/src/png_data/main.rs new file mode 100644 index 0000000..5c796e5 --- /dev/null +++ b/src/png_data/main.rs @@ -0,0 +1,105 @@ +mod header; + +use std::env; +use std::process::ExitCode; + +use getopts::Matches; +use getopts::Options; + +fn print_usage(program: &str, opts: Options) { + let brief = format!( + "Usage: {0} -(e|z|d) [FILE [-o OUTPUT]] [opts] + Encode: {0} -e file.tar -l rgb8 -o out.png -c \"(.tar)\" + Info: {0} -z out.png # (.tar) + Decode: {0} -d out.png -o file.tar", + program + ); + print!("{}", opts.usage(&brief)); +} + +fn print_version() { + print!( + r#"png_data (c) ef3d0c3e -- Pass data as PNGs +Copyright (c) 2024 +png_data is licensed under the GNU Affero General Public License version 3 (AGPLv3), +under the terms of the Free Software Foundation . + +This program is free software; you may modify and redistribute it. +There is NO WARRANTY, to the extent permitted by law."# + ); +} + +fn best_layout(size: u32, bits_per_pixel: u8) -> (u32, u32) { + let sz : f64 = size as f64 / bits_per_pixel as f64; + let width = sz.sqrt().floor(); + (width as u32, (sz / width as f64).ceil() as u32) +} + +fn encode(input: String, output: String, layout: String, matches: Matches) -> Result<(), String> { + Ok(()) +} + +fn main() -> ExitCode { + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let mut opts = Options::new(); + opts.optopt("e", "encode", "Embed file", "PATH"); + opts.optopt("d", "decode", "Decode mode", "PATH"); + opts.optflag("z", "info", "Read header"); + opts.optopt("l", "layout", "Png image layout", "TXT"); + opts.optopt("p", "password", "Data password", "TXT"); + opts.optopt("o", "output", "Output file", "PATH"); + opts.optopt("c", "comment", "Header comment", "TXT"); + opts.optflag("h", "help", "Print this help menu"); + opts.optflag("v", "version", "Print program version and licenses"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + panic!("{}", f.to_string()) + } + }; + if matches.opt_present("v") { + print_version(); + return ExitCode::SUCCESS; + } + if matches.opt_present("h") { + print_usage(&program, opts); + return ExitCode::SUCCESS; + } + + // Check options + if matches.opt_present("e") as usize + + matches.opt_present("d") as usize + + matches.opt_present("z") as usize + > 1 + { + eprintln!("Specify either `-e(--encode)`, `-z(--info)` or `-d(--decode)`"); + return ExitCode::FAILURE; + } + + if let Some(input_file) = matches.opt_str("e") { + let layout = match matches.opt_str("l") { + None => { + eprintln!("Missing required png layout (-l|--layout) option"); + return ExitCode::FAILURE; + } + Some(layout) => layout, + }; + + let output_file = match matches.opt_str("o") { + None => { + eprintln!("Missing required output (-o|--output) option"); + return ExitCode::FAILURE; + } + Some(output_file) => output_file, + }; + + if let Err(e) = encode(input_file, output_file, layout, matches) { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + } + ExitCode::SUCCESS +} diff --git a/src/png_embed/main.rs b/src/png_embed/main.rs index b01c0eb..2e05b33 100644 --- a/src/png_embed/main.rs +++ b/src/png_embed/main.rs @@ -27,10 +27,10 @@ use rand_chacha::ChaCha8Rng; fn print_usage(program: &str, opts: Options) { let brief = format!( - "Usage: {0} -l ALGORITHM -(e|d|z) [EMBED] FILE -o OUTPUT [opts] + "Usage: {0} -l ALGORITHM -(e|z|d) [EMBED] FILE -o OUTPUT [opts] Encode: {0} -l lo3 -e embed.jpg input.png -o out.png -c \"Embedded JPEG file\" Info: {0} -l lo3 out.png # Embedded JPEG file - Decode: {0} -l lo3 out.png > decoded.jpg", + Decode: {0} -l lo3 -d out.png -o decoded.jpg", program ); print!("{}", opts.usage(&brief)); @@ -40,7 +40,7 @@ fn print_version() { print!( r#"png_embed (c) ef3d0c3e -- Embed data into PNGs Copyright (c) 2024 -NML is licensed under the GNU Affero General Public License version 3 (AGPLv3), +png_embed is licensed under the GNU Affero General Public License version 3 (AGPLv3), under the terms of the Free Software Foundation . This program is free software; you may modify and redistribute it. @@ -288,7 +288,7 @@ fn main() -> ExitCode { + matches.opt_present("z") as usize > 1 { - eprintln!("Specify either `-e(--embed)`, -z(--info) or `-d(--decode)`"); + eprintln!("Specify either `-e(--embed)`, `-z(--info)` or `-d(--decode)`"); return ExitCode::FAILURE; } else if !matches.opt_present("l") { eprintln!("Missing algorithm name");