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.
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"

View File

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

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
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 <https://unlicense.org/>
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -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 <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::{
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<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>,
}
mod cfg;
mod cli;
mod constants;
mod generate;
fn main() -> ExitCode {
let cli_args = CliArgs::parse();
let args: Vec<String> = 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<Vec<u8>> {
let buf_size: u64 = match f.metadata() {
Ok(metadata) => metadata.len(),
Err(_) => 0x1000, // just preallocate 4 KiB otherwise
};
let mut buf: Vec<u8> = 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::<String>();
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>,
) -> 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 => (),
let mut reader = BufReader::new(in_file);
let mut in_data: Vec<u8> = 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<std::uint8_t,{}> {}{{{}}};",
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>,
) -> 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(())
}