0.2.0 release

This commit is contained in:
Adam 2025-08-17 00:44:45 +01:00
parent 039bfcc201
commit e4780cf729
9 changed files with 902 additions and 455 deletions

239
Cargo.lock generated
View File

@ -2,243 +2,64 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "bin2hpp" name = "bin2hpp"
version = "0.1.1" version = "0.2.0"
dependencies = [ 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.93" version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.38" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.98" version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"unicode-ident", "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]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.17" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[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"

View File

@ -1,13 +1,13 @@
[package] [package]
name = "bin2hpp" name = "bin2hpp"
version = "0.1.1" version = "0.2.0"
authors = ["Adam Macdonald"] authors = ["Adam Macdonald"]
edition = "2024" edition = "2024"
license = "Unlicense" license = "GPL-3.0-only"
readme = "README.md" readme = "README.md"
[dependencies] [dependencies]
clap = { version = "4.5.35", features = ["derive"] } thiserror = "2.0.14"
[profile.optimised] [profile.optimised]
inherits = "release" inherits = "release"

33
LICENSE
View File

@ -1,24 +1,15 @@
This is free and unencumbered software released into the public domain. bin2hpp
Copyright (C) 2025 <Adam Macdonald>
Anyone is free to copy, modify, publish, use, compile, sell, or This program is free software: you can redistribute it and/or modify
distribute this software, either in source code form or as a compiled it under the terms of the GNU General Public License as published by
binary, for any purpose, commercial or non-commercial, and by any the Free Software Foundation, either version 3 of the License, or
means. (at your option) any later version.
In jurisdictions that recognize copyright laws, the author or authors This program is distributed in the hope that it will be useful,
of this software dedicate any and all copyright interest in the but WITHOUT ANY WARRANTY; without even the implied warranty of
software to the public domain. We make this dedication for the benefit MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
of the public at large and to the detriment of our heirs and GNU General Public License for more details.
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.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, You should have received a copy of the GNU General Public License
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF along with this program. If not, see <https://www.gnu.org/licenses/>.
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 <https://unlicense.org/>

View File

@ -8,9 +8,30 @@ CLI tool for converting files into header files which one can use to directly em
1. `cargo build` 1. `cargo build`
## Future improvements ## Usage
- Not panicking if the source data is not UTF-8 encoded text when operating in text mode ### Basic usage
- C support
- Choices between std::array, C-style arrays, std::string_view & C strings `$ bin2hpp -i ~/my_image.bmp` will generate an `my_image_bmp.h` file in your current working directory containing something similar to the following:
- Customisable data types & data widths (unsigned vs. signed, uint8_t vs uint16_t, etc.)
```c
// Generated by bin2hpp 0.2.0
#ifndef MY_IMAGE_BMP_H
#define MY_IMAGE_BMP_H
#include <stddef.h>
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.

146
src/cfg.rs Normal file
View File

@ -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<String>,
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<Config, ProgramConfigError> {
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,
})
}

223
src/cli.rs Normal file
View File

@ -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<PathBuf>,
pub(crate) overwrite: bool,
pub(crate) line_ending: LineEnding,
pub(crate) language: Language,
pub(crate) constness: Constness,
pub(crate) symbol_name: Option<String>,
pub(crate) namespace: Option<String>,
pub(crate) guard_name: Option<String>,
pub(crate) container_type: ContainerType,
}
pub fn parse_cli_args(args: &[String]) -> Result<CliArgs, CliParseError> {
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,
});
}

50
src/constants.rs Normal file
View File

@ -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";

345
src/generate.rs Normal file
View File

@ -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 <cstdint>"
} else {
"#include <stdint.h>"
};
let stddef_h = if config.language == Language::Cpp {
"#include <cstddef>"
} else {
"#include <stddef.h>"
};
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 <array>{}", config.line_ending).unwrap();
}
ContainerType::CString => {
write!(buf, "{}{}", stddef_h, config.line_ending).unwrap();
}
ContainerType::StdString => {
write!(buf, "#include <string>{}", config.line_ending).unwrap();
}
ContainerType::StdStringView => {
write!(buf, "#include <string_view>{}", 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<String, HeaderGenerationError> {
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),
}
}

View File

@ -1,138 +1,52 @@
use std::{ use std::io::{BufReader, BufWriter, Read, Write};
fs::{File, OpenOptions}, use std::{env, fs::OpenOptions, process::ExitCode};
io::{self, BufReader, BufWriter, Read, Write},
path::PathBuf,
process::ExitCode,
};
use clap::{ArgAction, Parser}; use thiserror::Error;
#[cfg(windows)] use crate::cfg::Config;
const LINE_ENDING: &'static str = "\r\n";
#[cfg(not(windows))]
const LINE_ENDING: &'static str = "\n";
#[derive(Parser, Debug)] mod cfg;
#[command(version, about, long_about = None)] mod cli;
struct CliArgs { mod constants;
/// Input file path mod generate;
#[arg(short, long)]
input_path: PathBuf,
/// Output file path
#[arg(short, long)]
output_path: Option<PathBuf>,
/// Name of the C++ symbol
#[arg(short, long)]
symbol_name: Option<String>,
/// Namespace in which to put the symbol
#[arg(short, long)]
namespace: Option<String>,
/// Whether to operate in binary mode as opposed to text mode (default: text mode)
#[arg(short, long, action = ArgAction::SetTrue)]
binary: Option<bool>,
}
fn main() -> ExitCode { fn main() -> ExitCode {
let cli_args = CliArgs::parse(); let args: Vec<String> = env::args().collect();
if !cli_args.input_path.exists() { if args.len() < 2 {
eprintln!( eprintln!("Not enough arguments, see the --help option");
"file path \"{}\" does not exist",
cli_args.input_path.to_string_lossy()
);
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
if !cli_args.input_path.is_file() { if args.contains(&String::from("--help")) || args.contains(&String::from("-h")) {
eprintln!( println!("{}", constants::BIN2HPP_HELP_TEXT);
"file path \"{}\" is not a file", return ExitCode::SUCCESS;
cli_args.input_path.to_string_lossy()
);
return ExitCode::FAILURE;
} }
// 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() { let args = match cli::parse_cli_args(&args) {
Ok(p) => p, Ok(args) => args,
Err(_) => { Err(e) => {
eprintln!("environment's current working directory is unavailable"); 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; return ExitCode::FAILURE;
} }
}; };
let input_filename = match cli_args.input_path.file_name() { match write_header(&cfg) {
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()) {
Ok(_) => (), Ok(_) => (),
Err(error) => { Err(e) => {
eprintln!("failed to write to output file: {}", error); eprintln!("{}", e);
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
}; };
@ -140,112 +54,48 @@ fn main() -> ExitCode {
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
fn read_file(f: &File) -> io::Result<Vec<u8>> { #[derive(Error, Debug)]
let buf_size: u64 = match f.metadata() { pub enum HeaderGenerationError {
Ok(metadata) => metadata.len(), #[error("I/O error: \"{0}\"")]
Err(_) => 0x1000, // just preallocate 4 KiB otherwise IOError(String),
}; #[error("input file doesn't contain valid UTF-8: \"{0}\"")]
let mut buf: Vec<u8> = Vec::with_capacity(buf_size as usize); InvalidUtf8String(String),
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 write_header(config: &Config) -> Result<(), HeaderGenerationError> {
fn format_as_binary(data: &[u8]) -> String { let in_file = OpenOptions::new()
let mut formatted = data .read(true)
.iter() .write(false)
.map(|b| format!("{:#x},", b)) .open(config.input_file_path.clone())
.collect::<String>(); .expect("failed to open input file");
if !formatted.is_empty() {
formatted.pop().unwrap(); // remove trailing ','
}
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) let mut reader = BufReader::new(in_file);
fn format_as_text(data: &[u8]) -> String { let mut in_data: Vec<u8> = Vec::new();
// FIXME: this will currently panic if the input file was not UTF-8 encoded! let n_bytes_read = match reader.read_to_end(&mut in_data) {
return String::from_utf8(data.to_vec()) Ok(n) => n,
.unwrap() Err(e) => return Err(HeaderGenerationError::IOError(e.to_string())),
.escape_default()
.collect();
}
fn generate_src_for_array(
array_contents: &str,
array_len: usize,
symbol_name: &str,
ns_name: Option<String>,
) -> String {
// Includes
let mut out_string: String = String::with_capacity(array_contents.len() + 0x100);
out_string.push_str("#include <array>");
out_string.push_str(LINE_ENDING);
out_string.push_str("#include <cstdint>");
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 generate::with_header(config, &in_data[..n_bytes_read], &mut buf)?;
out_string.push_str(
format!(
"constexpr std::array<std::uint8_t,{}> {}{{{}}};",
array_len, symbol_name, array_contents
)
.as_str(),
);
// Close namespace (if need be) let out_file = OpenOptions::new()
match ns_name { .write(true)
Some(_) => out_string.push_str("}"), .truncate(config.overwrite)
None => (), .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 Ok(())
out_string.push_str(LINE_ENDING);
return out_string;
}
fn generate_src_for_string(
string_contents: &str,
symbol_name: &str,
ns_name: Option<String>,
) -> 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;
} }