From e4780cf729409e76817cea2101a79d7aaf82a8fc Mon Sep 17 00:00:00 2001 From: "adam@2khz.xyz" Date: Sun, 17 Aug 2025 00:44:45 +0100 Subject: [PATCH] 0.2.0 release --- Cargo.lock | 239 +++++--------------------------- Cargo.toml | 6 +- LICENSE | 33 ++--- README.md | 31 ++++- src/cfg.rs | 146 ++++++++++++++++++++ src/cli.rs | 223 ++++++++++++++++++++++++++++++ src/constants.rs | 50 +++++++ src/generate.rs | 345 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 284 +++++++++----------------------------- 9 files changed, 902 insertions(+), 455 deletions(-) create mode 100644 src/cfg.rs create mode 100644 src/cli.rs create mode 100644 src/constants.rs create mode 100644 src/generate.rs diff --git a/Cargo.lock b/Cargo.lock index 0ab4ab2..cf09fcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,243 +2,64 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys", -] - [[package]] name = "bin2hpp" -version = "0.1.1" +version = "0.2.0" dependencies = [ - "clap", + "thiserror", ] -[[package]] -name = "clap" -version = "4.5.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "syn" -version = "2.0.98" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml index d1aa54b..7aa17ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "bin2hpp" -version = "0.1.1" +version = "0.2.0" authors = ["Adam Macdonald"] edition = "2024" -license = "Unlicense" +license = "GPL-3.0-only" readme = "README.md" [dependencies] -clap = { version = "4.5.35", features = ["derive"] } +thiserror = "2.0.14" [profile.optimised] inherits = "release" diff --git a/LICENSE b/LICENSE index efb9808..2667c29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,15 @@ -This is free and unencumbered software released into the public domain. +bin2hpp +Copyright (C) 2025 -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md index 941fdb4..cb173bf 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,30 @@ CLI tool for converting files into header files which one can use to directly em 1. `cargo build` -## Future improvements +## Usage -- Not panicking if the source data is not UTF-8 encoded text when operating in text mode -- C support -- Choices between std::array, C-style arrays, std::string_view & C strings -- Customisable data types & data widths (unsigned vs. signed, uint8_t vs uint16_t, etc.) +### Basic usage + +`$ bin2hpp -i ~/my_image.bmp` will generate an `my_image_bmp.h` file in your current working directory containing something similar to the following: + +```c +// Generated by bin2hpp 0.2.0 +#ifndef MY_IMAGE_BMP_H +#define MY_IMAGE_BMP_H +#include +const size_t MYIMAGE_BMP_LEN = 36000054; +const unsigned char MYIMAGE_BMP[MYIMAGE_BMP_LEN] = {0x42,0x4d,0x36,0x51,0x25, ...} +#else +extern const size_t MYIMAGE_BMP_LEN; +extern const unsigned char MYIMAGE_BMP[MYIMAGE_BMP_LEN]; +#endif +``` + +### Note about CLI arguments + +Command line arguments are not positional. The input file path argument is the +only required command line argument. The command line argument parser will +choose the first instance of any provided argument. For example, if you provide +the `-i` argument twice; only the first `-i ./file/path` will be used. This behaviour +should not be relied upon as the implementation of the command line argument +parser may change at any time. diff --git a/src/cfg.rs b/src/cfg.rs new file mode 100644 index 0000000..9925708 --- /dev/null +++ b/src/cfg.rs @@ -0,0 +1,146 @@ +use std::path::PathBuf; + +use thiserror::Error; + +use crate::cli::{CliArgs, Constness, ContainerType, Language, LineEnding}; + +#[derive(Error, Debug)] +pub enum ProgramConfigError { + #[error("invalid path provided: \"{0}\"")] + InvalidPath(String), + #[error("invalid symbol name provided: \"{0}\"")] + InvalidSymbolName(String), +} + +#[derive(Debug)] +pub struct Config { + pub(crate) input_file_path: PathBuf, + pub(crate) output_file_path: PathBuf, + pub(crate) overwrite: bool, + pub(crate) line_ending: String, + pub(crate) language: Language, + pub(crate) constness: Constness, + pub(crate) symbol_name: String, + pub(crate) namespace: Option, + pub(crate) guard_name: String, + pub(crate) container_type: ContainerType, +} + +const CRLF_LINE_ENDING: &'static str = "\r\n"; +const LF_LINE_ENDING: &'static str = "\n"; + +pub fn parse_config_from_args(args: &CliArgs) -> Result { + let output_path = if let Some(path) = &args.output_file_path { + path.clone() + } else { + if let Some(stem) = args.input_file_path.file_stem() { + let mut out_path = PathBuf::new(); + + let mut filename = String::new(); + filename.push_str(&stem.to_string_lossy()); + if let Some(ext) = args.input_file_path.extension() { + filename.push_str(&format!("_{}", ext.to_string_lossy())); + } + if args.language == Language::Cpp { + filename.push_str(".hpp"); + } else { + filename.push_str(".h"); + } + out_path.push(filename); + + out_path + } else { + return Err(ProgramConfigError::InvalidPath( + args.input_file_path.to_string_lossy().to_string(), + )); + } + }; + + Ok(Config { + input_file_path: args.input_file_path.clone(), + output_file_path: output_path.clone(), + overwrite: args.overwrite, + line_ending: if args.line_ending == LineEnding::Unix { + LF_LINE_ENDING.into() + } else { + CRLF_LINE_ENDING.into() + }, + language: args.language, + constness: args.constness, + symbol_name: match &args.symbol_name { + Some(s) => { + if s.chars().all(|c| c.is_ascii_alphanumeric()) { + s.to_string() + } else { + return Err(ProgramConfigError::InvalidSymbolName(s.to_string())); + } + } + _ => { + let mut stem: String = match args.input_file_path.file_stem() { + Some(s) => s.to_string_lossy().to_string(), + None => { + return Err(ProgramConfigError::InvalidPath( + args.input_file_path.to_string_lossy().to_string(), + )); + } + }; + stem.retain(|c| c.is_ascii_alphanumeric()); + + let mut ext: String = match args.input_file_path.extension() { + Some(s) => s.to_string_lossy().to_string(), + None => { + return Err(ProgramConfigError::InvalidPath( + args.input_file_path.to_string_lossy().to_string(), + )); + } + }; + ext.retain(|c| c.is_ascii_alphanumeric()); + + format!("{}_{}", stem.to_ascii_uppercase(), ext.to_ascii_uppercase()) + } + }, + namespace: match &args.namespace { + Some(ns) => { + if ns.chars().all(|c| c.is_ascii_alphanumeric()) { + Some(ns.to_string()) + } else { + return Err(ProgramConfigError::InvalidSymbolName(ns.to_string())); + } + } + _ => None, + }, + guard_name: match &args.guard_name { + Some(guard) => { + if guard.chars().all(|c| c.is_ascii_alphanumeric()) { + guard.clone() + } else { + return Err(ProgramConfigError::InvalidSymbolName(guard.to_string())); + } + } + _ => { + let mut stem: String = match output_path.file_stem() { + Some(s) => s.to_string_lossy().to_ascii_uppercase(), + None => { + return Err(ProgramConfigError::InvalidPath( + args.input_file_path.to_string_lossy().to_string(), + )); + } + }; + stem.retain(|c| c.is_ascii_alphanumeric() || c == '_'); + + let mut ext: String = match output_path.extension() { + Some(s) => s.to_string_lossy().to_ascii_uppercase(), + None => { + return Err(ProgramConfigError::InvalidPath( + args.input_file_path.to_string_lossy().to_string(), + )); + } + }; + ext.retain(|c| c.is_ascii_alphanumeric()); + + format!("{}_{}", stem, ext) + } + }, + container_type: args.container_type, + }) +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..689c841 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,223 @@ +use std::path::{Path, PathBuf}; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum CliParseError { + #[error("missing command line argument: \"{0}\"")] + MissingArgument(&'static str), + #[error("missing command line argument operand, \"{0}\" requires parameters")] + MissingOperand(&'static str), + #[error("conflicting option: \"{0}\" (check C vs. C++ mode)")] + ConflictingOption(&'static str), +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ByteType { + UnsignedChar, + Char, + U8, + I8, +} + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ContainerType { + CArray(ByteType), + StdArray(ByteType), + CString, + StdString, + StdStringView, +} + +#[derive(Debug, PartialEq, Default, Clone, Copy)] +pub enum Language { + #[default] + C, + Cpp, +} + +#[derive(Debug, PartialEq, Default, Clone, Copy)] +pub enum LineEnding { + #[default] + Unix, + Windows, +} + +#[derive(Debug, PartialEq, Default, Clone, Copy)] +pub enum Constness { + #[default] + Const, + Constexpr, + NonConst, +} + +#[derive(Debug)] +pub struct CliArgs { + pub(crate) input_file_path: PathBuf, + pub(crate) output_file_path: Option, + pub(crate) overwrite: bool, + pub(crate) line_ending: LineEnding, + pub(crate) language: Language, + pub(crate) constness: Constness, + pub(crate) symbol_name: Option, + pub(crate) namespace: Option, + pub(crate) guard_name: Option, + pub(crate) container_type: ContainerType, +} + +pub fn parse_cli_args(args: &[String]) -> Result { + let input_path = match args.iter().position(|s| *s == "-i") { + Some(idx) => match args.get(idx + 1) { + Some(p) => Path::new(p), + None => return Err(CliParseError::MissingOperand("-i")), + }, + None => return Err(CliParseError::MissingArgument("-i")), + }; + + let output_path = match args.iter().position(|s| *s == "-o") { + Some(idx) => match args.get(idx + 1) { + Some(p) => Some(Path::new(p)), + None => return Err(CliParseError::MissingOperand("-o")), + }, + None => None, + }; + + let overwrite = match args.iter().find(|s| *s == "--overwrite") { + Some(_) => true, + None => false, + }; + + let symbol_name = match args.iter().position(|s| *s == "--symname") { + Some(idx) => match args.get(idx + 1) { + Some(s) => Some(s), + None => return Err(CliParseError::MissingOperand("--symname")), + }, + None => None, + }; + + let namespace = match args.iter().position(|s| *s == "--namespace") { + Some(idx) => match args.get(idx + 1) { + Some(s) => Some(s), + None => return Err(CliParseError::MissingOperand("--namespace")), + }, + None => None, + }; + + let guard_name = match args.iter().position(|s| *s == "--guard") { + Some(idx) => match args.get(idx + 1) { + Some(s) => Some(s), + None => return Err(CliParseError::MissingOperand("--guard")), + }, + None => None, + }; + + let line_ending = match args.iter().find(|s| *s == "--crlf") { + Some(_) => LineEnding::Windows, + _ => LineEnding::Unix, + }; + + let constness: Constness = match args + .iter() + .find(|s| *s == "--mutable" || *s == "--const" || *s == "--constexpr") + { + Some(a) => { + if a == "--const" { + Constness::Const + } else if a == "--constexpr" { + Constness::Constexpr + } else { + Constness::NonConst + } + } + None => Constness::Const, + }; + + let language = match args.iter().find(|s| *s == "--cpp") { + Some(_) => Language::Cpp, + _ => Language::C, + }; + + let byte_type = match args + .iter() + .find(|s| *s == "--char" || *s == "--uchar" || *s == "--u8" || *s == "--i8") + { + Some(a) => { + if a == "--char" { + ByteType::Char + } else if a == "--uchar" { + ByteType::UnsignedChar + } else if a == "--u8" { + ByteType::U8 + } else if a == "--i8" { + ByteType::I8 + } else { + ByteType::UnsignedChar + } + } + None => ByteType::UnsignedChar, + }; + + let container_type = match args.iter().find(|s| { + *s == "--carray" + || *s == "--cstring" + || *s == "--stdarray" + || *s == "--stdstring" + || *s == "--stdstringview" + }) { + Some(a) => { + if a == "--carray" { + ContainerType::CArray(byte_type) + } else if a == "--cstring" { + ContainerType::CString + } else if a == "--stdarray" { + ContainerType::StdArray(byte_type) + } else if a == "--stdstring" { + ContainerType::StdString + } else if a == "--stdstringview" { + ContainerType::StdStringView + } else { + ContainerType::CArray(byte_type) + } + } + None => ContainerType::CArray(byte_type), + }; + + if language == Language::C { + match container_type { + ContainerType::StdArray(_) => { + return Err(CliParseError::ConflictingOption("--stdarray")); + } + ContainerType::StdString => { + return Err(CliParseError::ConflictingOption("--stdstring")); + } + ContainerType::StdStringView => { + return Err(CliParseError::ConflictingOption("--stdstringview")); + } + _ => (), + }; + + match constness { + Constness::Constexpr => { + return Err(CliParseError::ConflictingOption("--constexpr")); + } + _ => (), + }; + } + + return Ok(CliArgs { + input_file_path: input_path.to_path_buf(), + output_file_path: if let Some(path) = output_path { + Some(path.to_path_buf()) + } else { + None + }, + overwrite: overwrite, + line_ending: line_ending, + language: language, + constness: constness, + symbol_name: symbol_name.cloned(), + namespace: namespace.cloned(), + guard_name: guard_name.cloned(), + container_type: container_type, + }); +} diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..d875060 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,50 @@ +pub const BIN2HPP_HELP_TEXT: &'static str = + "Usage:\tbin2hpp -i INPUT_FILE [-o OUTPUT_FILE] [OPTIONS] + +OPTIONS: + +-i INPUT_FILE Path to input file to encode + (example: -i ../assets/shoot.ogg) + +-o OUTPUT_FILE Optional output file path. Default behaviour is to create + the header file in the input file's directory. + (example: -o ./my_header.hpp) + +--overwrite Overwrite the output file if it already exists + (default: don't overwrite) + +LANGUAGE: + +--cpp Set language features to C++ (runs in C mode by default) + +OUTPUT OPTIONS: + +--lf Use UNIX-style LF line endings (default) +--crlf Use Windows-style CRLF line endings +--mutable Whether the symbol should not be marked as const +--const Whether the symbol should be marked as const (default) +--constexpr Whether the symbol should be marked as constexpr (C++ mode) +--symname NAME Symbol name +--namespace NAME Namespace in which the symbol will exist + (uses namespace example {} in C++ mode and prepends + EXAMPLE_ to symbol names in C mode) +--guard NAME Preprocessor macro to use for the header guard (#ifndef ...) + (default: derive from output filename) + +CONTAINER TYPES: + +--carray C-style array (default) +--cstring C string, NUL-terminated +--stdarray C++ std::array +--stdstring C++ std::string +--stdstringview C++ std::string_view + +BYTES TYPES FOR ARRAYS: + +--char C's char type +--uchar C's unsigned char type (default) +--u8 C/C++'s (std::)uint8_t type +--i8 C/C++'s (std::)int8_t type + +-h | --help Show this help text +-v | --version Print the program's build & version information"; diff --git a/src/generate.rs b/src/generate.rs new file mode 100644 index 0000000..fa09146 --- /dev/null +++ b/src/generate.rs @@ -0,0 +1,345 @@ +use std::fmt::Write; + +use crate::{ + HeaderGenerationError, + cfg::Config, + cli::{ByteType, Constness, ContainerType, Language}, +}; + +pub fn with_header( + config: &Config, + in_data: &[u8], + buf: &mut String, +) -> Result<(), HeaderGenerationError> { + // Program banner at the top of the header file + write!( + buf, + "// Generated by {} {}{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION"), + config.line_ending + ) + .unwrap(); + + // Header guard + if config.language == Language::Cpp { + write!( + buf, + "#if !defined({}){}", + config.guard_name, config.line_ending + ) + .unwrap(); + } else { + write!(buf, "#ifndef {}{}", config.guard_name, config.line_ending).unwrap(); + } + write!(buf, "#define {}{}", config.guard_name, config.line_ending).unwrap(); + + with_includes(config, buf); + with_contents(config, in_data, buf)?; + + write!(buf, "#endif{}", config.line_ending).unwrap(); + + Ok(()) +} + +pub fn with_contents( + config: &Config, + in_data: &[u8], + buf: &mut String, +) -> Result<(), HeaderGenerationError> { + if config.language == Language::Cpp { + // C++ mode + + if let Some(ns) = &config.namespace { + write!(buf, "namespace {}{{{}", ns, config.line_ending).unwrap(); + with_definition(config, in_data, buf)?; + write!(buf, "}}{}", config.line_ending).unwrap(); + } else { + with_definition(config, in_data, buf)?; + } + } else { + // C mode + + with_definition(config, in_data, buf)?; + write!(buf, "#else{}", config.line_ending).unwrap(); + with_declaration(config, in_data, buf); + } + + Ok(()) +} + +pub fn with_includes(config: &Config, buf: &mut String) { + // Includes + + let stdint_h = if config.language == Language::Cpp { + "#include " + } else { + "#include " + }; + + let stddef_h = if config.language == Language::Cpp { + "#include " + } else { + "#include " + }; + + match config.container_type { + ContainerType::CArray(byte_type) => { + write!(buf, "{}{}", stddef_h, config.line_ending).unwrap(); + match byte_type { + ByteType::U8 => write!(buf, "{}{}", stdint_h, config.line_ending).unwrap(), + ByteType::I8 => write!(buf, "{}{}", stdint_h, config.line_ending).unwrap(), + _ => (), + }; + } + ContainerType::StdArray(byte_type) => { + match byte_type { + ByteType::U8 => write!(buf, "{}{}", stdint_h, config.line_ending).unwrap(), + ByteType::I8 => write!(buf, "{}{}", stdint_h, config.line_ending).unwrap(), + _ => (), + }; + + write!(buf, "#include {}", config.line_ending).unwrap(); + } + ContainerType::CString => { + write!(buf, "{}{}", stddef_h, config.line_ending).unwrap(); + } + ContainerType::StdString => { + write!(buf, "#include {}", config.line_ending).unwrap(); + } + ContainerType::StdStringView => { + write!(buf, "#include {}", config.line_ending).unwrap(); + } + }; +} + +pub fn definition_modifiers(config: &Config) -> String { + let mut out_str: String = String::with_capacity(20); + + if config.language == Language::Cpp { + write!(out_str, "inline ").unwrap(); + } + + match config.constness { + Constness::Const => write!(out_str, "{} ", "const").unwrap(), + Constness::Constexpr => write!(out_str, "{} ", "constexpr").unwrap(), + Constness::NonConst => (), + }; + + out_str +} + +pub fn declaration_modifiers(config: &Config) -> String { + let mut out_str: String = String::with_capacity(20); + + if config.language == Language::C { + write!(out_str, "extern ").unwrap(); + } + + match config.constness { + Constness::Const => write!(out_str, "{} ", "const").unwrap(), + Constness::Constexpr => write!(out_str, "{} ", "constexpr").unwrap(), + Constness::NonConst => (), + }; + + out_str +} + +pub fn with_definition( + config: &Config, + in_data: &[u8], + buf: &mut String, +) -> Result<(), HeaderGenerationError> { + let length: usize = match config.container_type { + ContainerType::CString => in_data.len() + 1, + _ => in_data.len(), + }; + + let modifiers = definition_modifiers(config); + + match config.container_type { + ContainerType::CArray(_) => { + write!( + buf, + "{}{} = {};{}", + modifiers, + length_data_type_symbol(config), + length, + config.line_ending + ) + .unwrap(); + } + ContainerType::CString => { + write!( + buf, + "{}{} = {};{}", + modifiers, + length_data_type_symbol(config), + length, + config.line_ending + ) + .unwrap(); + } + _ => (), + }; + + write!( + buf, + "{}{} = {};{}", + modifiers, + data_type_and_symbol(config, in_data), + match config.container_type { + ContainerType::CArray(_) => with_array_initialiser(in_data), + ContainerType::StdArray(_) => with_array_initialiser(in_data), + _ => with_string_initialiser(config, in_data)?, + }, + config.line_ending + ) + .unwrap(); + + Ok(()) +} + +pub fn with_declaration(config: &Config, in_data: &[u8], buf: &mut String) { + let modifiers = declaration_modifiers(config); + + match config.container_type { + ContainerType::CArray(_) => { + write!( + buf, + "{}{};{}", + modifiers, + length_data_type_symbol(config), + config.line_ending + ) + .unwrap(); + } + ContainerType::CString => { + write!( + buf, + "{}{};{}", + modifiers, + length_data_type_symbol(config), + config.line_ending + ) + .unwrap(); + } + _ => (), + }; + + write!( + buf, + "{}{};{}", + modifiers, + data_type_and_symbol(config, in_data), + config.line_ending + ) + .unwrap(); +} + +pub fn with_array_initialiser(in_data: &[u8]) -> String { + let mut out_str: String = String::with_capacity(in_data.len() * 6 + 16); + + write!(out_str, "{{").unwrap(); + for b in in_data { + write!(out_str, "{:#x},", b).unwrap(); + } + let _ = out_str.pop(); + write!(out_str, "}}").unwrap(); + + out_str +} + +pub fn with_string_initialiser( + config: &Config, + in_data: &[u8], +) -> Result { + let mut out_str: String = String::with_capacity(in_data.len() + 4); + + let string_repr: String = match String::from_utf8(in_data.into()) { + Ok(s) => s.escape_default().to_string(), + Err(_) => { + return Err(HeaderGenerationError::InvalidUtf8String( + config.input_file_path.to_string_lossy().to_string(), + )); + } + }; + + write!(out_str, "\"{}\"", string_repr).unwrap(); + + Ok(out_str) +} + +pub fn length_data_type_symbol(config: &Config) -> String { + let mut out_str = String::with_capacity(36 + config.symbol_name.len()); + + let size_type = match config.language { + Language::C => "size_t", + Language::Cpp => "std::size_t", + }; + + write!(out_str, "{} {}", size_type, length_symbol(config)).unwrap(); + + out_str +} + +pub fn length_symbol(config: &Config) -> String { + let mut out_str = String::with_capacity(config.symbol_name.len() + 5); + + write!( + out_str, + "{}{}", + config.symbol_name, + if config + .symbol_name + .chars() + .all(|c| c.is_uppercase() || c == '_') + { + "_LEN" + } else { + "_len" + }, + ) + .unwrap(); + + out_str +} + +pub fn data_type_and_symbol(config: &Config, in_data: &[u8]) -> String { + let u8_type = match config.language { + Language::C => "uint8_t", + Language::Cpp => "std::uint8_t", + }; + let i8_type = match config.language { + Language::C => "int8_t", + Language::Cpp => "std::int8_t", + }; + + let length_sym = length_symbol(config); + + match config.container_type { + ContainerType::CArray(byte_type) => match byte_type { + ByteType::UnsignedChar => { + format!("unsigned char {}[{}]", config.symbol_name, length_sym) + } + ByteType::Char => format!("char {}[{}]", config.symbol_name, length_sym), + _ => format!("{} {}[{}]", u8_type, config.symbol_name, length_sym), + }, + ContainerType::StdArray(byte_type) => { + format!( + "std::array<{},{}> {}", + match byte_type { + ByteType::UnsignedChar => "unsigned char", + ByteType::Char => "char", + ByteType::U8 => u8_type, + ByteType::I8 => i8_type, + }, + in_data.len(), + config.symbol_name + ) + } + ContainerType::CString => format!("char {}[{}]", config.symbol_name, length_sym), + ContainerType::StdString => format!("std::string {}", config.symbol_name), + ContainerType::StdStringView => format!("std::string_view {}", config.symbol_name), + } +} diff --git a/src/main.rs b/src/main.rs index f5ceab9..e155675 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,138 +1,52 @@ -use std::{ - fs::{File, OpenOptions}, - io::{self, BufReader, BufWriter, Read, Write}, - path::PathBuf, - process::ExitCode, -}; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::{env, fs::OpenOptions, process::ExitCode}; -use clap::{ArgAction, Parser}; +use thiserror::Error; -#[cfg(windows)] -const LINE_ENDING: &'static str = "\r\n"; -#[cfg(not(windows))] -const LINE_ENDING: &'static str = "\n"; +use crate::cfg::Config; -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct CliArgs { - /// Input file path - #[arg(short, long)] - input_path: PathBuf, - /// Output file path - #[arg(short, long)] - output_path: Option, - /// Name of the C++ symbol - #[arg(short, long)] - symbol_name: Option, - /// Namespace in which to put the symbol - #[arg(short, long)] - namespace: Option, - /// Whether to operate in binary mode as opposed to text mode (default: text mode) - #[arg(short, long, action = ArgAction::SetTrue)] - binary: Option, -} +mod cfg; +mod cli; +mod constants; +mod generate; fn main() -> ExitCode { - let cli_args = CliArgs::parse(); + let args: Vec = env::args().collect(); - if !cli_args.input_path.exists() { - eprintln!( - "file path \"{}\" does not exist", - cli_args.input_path.to_string_lossy() - ); + if args.len() < 2 { + eprintln!("Not enough arguments, see the --help option"); return ExitCode::FAILURE; } - if !cli_args.input_path.is_file() { - eprintln!( - "file path \"{}\" is not a file", - cli_args.input_path.to_string_lossy() - ); - return ExitCode::FAILURE; + if args.contains(&String::from("--help")) || args.contains(&String::from("-h")) { + println!("{}", constants::BIN2HPP_HELP_TEXT); + return ExitCode::SUCCESS; } - // Derive output path from cwd & original filename if not provided in CLI + if args.contains(&String::from("--version")) || args.contains(&String::from("-v")) { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + return ExitCode::SUCCESS; + } - let cwd = match std::env::current_dir() { - Ok(p) => p, - Err(_) => { - eprintln!("environment's current working directory is unavailable"); + let args = match cli::parse_cli_args(&args) { + Ok(args) => args, + Err(e) => { + eprintln!("Failed to parse command line arguments: {}", e); + return ExitCode::FAILURE; + } + }; + let cfg = match cfg::parse_config_from_args(&args) { + Ok(cfg) => cfg, + Err(e) => { + eprintln!("Invalid configuration specified: {}", e); return ExitCode::FAILURE; } }; - let input_filename = match cli_args.input_path.file_name() { - Some(f) => f, - None => { - eprintln!( - "input file path \"{}\" does not contain a valid filename", - cli_args.input_path.to_string_lossy() - ); - return ExitCode::FAILURE; - } - }; - - let output_path = match cli_args.output_path { - Some(p) => p, - None => cwd.join(input_filename).with_extension("hpp"), - }; - - let symbol_name = match cli_args.symbol_name { - Some(s) => s, - None => input_filename - .to_string_lossy() - .to_string() - .chars() - .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) - .collect(), - }; - - let input_file = match OpenOptions::new().read(true).open(cli_args.input_path) { - Ok(f) => f, - Err(error) => { - eprintln!("failed to open input file for reading: {}", error); - return ExitCode::FAILURE; - } - }; - - let buf = match read_file(&input_file) { - Ok(data) => data, - Err(error) => { - eprintln!("failed read input file: {}", error); - return ExitCode::FAILURE; - } - }; - - let formatted = match cli_args.binary { - Some(true) => format_as_binary(&buf), - _ => format_as_text(&buf), - }; - - let out_src = match cli_args.binary { - Some(true) => { - generate_src_for_array(&formatted, buf.len(), &symbol_name, cli_args.namespace) - } - _ => generate_src_for_string(&formatted, &symbol_name, cli_args.namespace), - }; - - let output_file = match OpenOptions::new() - .write(true) - .truncate(true) - .create(true) - .open(output_path) - { - Ok(f) => f, - Err(error) => { - eprintln!("failed to open output file for writing: {}", error); - return ExitCode::FAILURE; - } - }; - - let mut writer = BufWriter::new(output_file); - match writer.write_all(out_src.as_bytes()) { + match write_header(&cfg) { Ok(_) => (), - Err(error) => { - eprintln!("failed to write to output file: {}", error); + Err(e) => { + eprintln!("{}", e); return ExitCode::FAILURE; } }; @@ -140,112 +54,48 @@ fn main() -> ExitCode { return ExitCode::SUCCESS; } -fn read_file(f: &File) -> io::Result> { - let buf_size: u64 = match f.metadata() { - Ok(metadata) => metadata.len(), - Err(_) => 0x1000, // just preallocate 4 KiB otherwise - }; - let mut buf: Vec = Vec::with_capacity(buf_size as usize); - - let mut reader = BufReader::new(f); - reader.read_to_end(&mut buf)?; - - return Ok(buf); +#[derive(Error, Debug)] +pub enum HeaderGenerationError { + #[error("I/O error: \"{0}\"")] + IOError(String), + #[error("input file doesn't contain valid UTF-8: \"{0}\"")] + InvalidUtf8String(String), } -/// Format a slice of bytes into an array-of-bytes initialiser list -fn format_as_binary(data: &[u8]) -> String { - let mut formatted = data - .iter() - .map(|b| format!("{:#x},", b)) - .collect::(); - if !formatted.is_empty() { - formatted.pop().unwrap(); // remove trailing ',' - } +fn write_header(config: &Config) -> Result<(), HeaderGenerationError> { + let in_file = OpenOptions::new() + .read(true) + .write(false) + .open(config.input_file_path.clone()) + .expect("failed to open input file"); - return formatted; -} + let mut buf = String::with_capacity(if let Ok(meta) = in_file.metadata() { + meta.len() as usize + } else { + 0 + }); -/// Format a slice of bytes into a string literal (without quotes) -fn format_as_text(data: &[u8]) -> String { - // FIXME: this will currently panic if the input file was not UTF-8 encoded! - return String::from_utf8(data.to_vec()) - .unwrap() - .escape_default() - .collect(); -} - -fn generate_src_for_array( - array_contents: &str, - array_len: usize, - symbol_name: &str, - ns_name: Option, -) -> String { - // Includes - let mut out_string: String = String::with_capacity(array_contents.len() + 0x100); - out_string.push_str("#include "); - out_string.push_str(LINE_ENDING); - out_string.push_str("#include "); - out_string.push_str(LINE_ENDING); - - // Namespace - match ns_name { - Some(ref namespace) => out_string.push_str(format!("namespace {}{{", namespace).as_str()), - None => (), + let mut reader = BufReader::new(in_file); + let mut in_data: Vec = Vec::new(); + let n_bytes_read = match reader.read_to_end(&mut in_data) { + Ok(n) => n, + Err(e) => return Err(HeaderGenerationError::IOError(e.to_string())), }; - // Array declaration - out_string.push_str( - format!( - "constexpr std::array {}{{{}}};", - array_len, symbol_name, array_contents - ) - .as_str(), - ); + generate::with_header(config, &in_data[..n_bytes_read], &mut buf)?; - // Close namespace (if need be) - match ns_name { - Some(_) => out_string.push_str("}"), - None => (), + let out_file = OpenOptions::new() + .write(true) + .truncate(config.overwrite) + .create_new(!config.overwrite) + .open(config.output_file_path.clone()) + .expect("failed to open input file"); + let mut writer = BufWriter::new(out_file); + + match writer.write_all(buf.as_bytes()) { + Ok(_) => (), + Err(e) => return Err(HeaderGenerationError::IOError(e.to_string())), }; - // Trailing newline - out_string.push_str(LINE_ENDING); - - return out_string; -} - -fn generate_src_for_string( - string_contents: &str, - symbol_name: &str, - ns_name: Option, -) -> String { - // Includes - let mut out_string: String = String::with_capacity(string_contents.len() + 0x100); - - // Namespace - match ns_name { - Some(ref namespace) => out_string.push_str(format!("namespace {}{{", namespace).as_str()), - None => (), - }; - - // String initialisation - out_string.push_str( - format!( - "constexpr const char* {} = \"{}\";", - symbol_name, string_contents - ) - .as_str(), - ); - - // Close namespace (if need be) - match ns_name { - Some(_) => out_string.push_str("}"), - None => (), - }; - - // Trailing newline - out_string.push_str(LINE_ENDING); - - return out_string; + Ok(()) }