commit f4fc652ed330543c1f49b696bdec16923080415b Author: Adam Macdonald <72780006+twokilohertz@users.noreply.github.com> Date: Tue Apr 8 19:00:24 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36a351c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Cargo +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3f2e00f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,244 @@ +# This file is automatically @generated by Cargo. +# 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.0" +dependencies = [ + "clap", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.17" +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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..aac8bfa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bin2hpp" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.35", features = ["derive"] } + +[profile.optimised] +inherits = "release" +opt-level = 3 +strip = true +lto = true +codegen-units = 1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..941fdb4 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# bin2hpp + +> _One day we'll get #embed and std::embed, but today is not that day._ + +CLI tool for converting files into header files which one can use to directly embed data in their C++ projects. + +## Building + +1. `cargo build` + +## Future improvements + +- 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.) diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..11c07aa --- /dev/null +++ b/src/main.rs @@ -0,0 +1,250 @@ +use std::{ + fs::{File, OpenOptions}, + io::{self, BufReader, BufWriter, Read, Write}, + path::PathBuf, + process::ExitCode, +}; + +use clap::{ArgAction, Parser}; + +#[cfg(windows)] +const LINE_ENDING: &'static str = "\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &'static str = "\n"; + +#[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, +} + +fn main() -> ExitCode { + let cli_args = CliArgs::parse(); + + if !cli_args.input_path.exists() { + eprintln!( + "file path \"{}\" does not exist", + cli_args.input_path.to_string_lossy() + ); + 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; + } + + // Derive output path from cwd & original filename if not provided in CLI + + let cwd = match std::env::current_dir() { + Ok(p) => p, + Err(_) => { + eprintln!("environment's current working directory is unavailable"); + 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) + .create_new(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()) { + Ok(_) => (), + Err(error) => { + eprintln!("failed to write to output file: {}", error); + return ExitCode::FAILURE; + } + }; + + 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); +} + +/// 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 ',' + } + + return formatted; +} + +/// 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 => (), + }; + + // Array declaration + out_string.push_str( + format!( + "constexpr std::array {}{{{}}};", + array_len, symbol_name, array_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; +} + +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; +}