Files
jaarg/examples/bin2h.rs
2025-11-01 16:19:38 +11:00

239 lines
6.7 KiB
Rust

/* bin2c - jaarg example application
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
use jaarg::{Opt, Opts, ParseControl, ParseResult};
use std::fs::File;
use std::io::Write;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf;
use std::process::ExitCode;
/// Strip disallowed characters from a C preprocessor label
fn sanitise_label(ident: &str) -> String {
let mut out = String::new();
out.reserve(ident.len());
// Prevent leading underscore
let mut last = '_';
for mut i in ident.chars() {
if !out.is_empty() || !i.is_ascii_digit() {
if !i.is_alphanumeric() {
i = '_';
}
if i != '_' || last != '_' {
out.push(i);
}
last = i;
}
}
// Prevent trailing underscore
if last == '_' {
out.pop();
}
out
}
/// Turn filename into an include guard label
fn guard_name(name: &str) -> String {
let mut out = "BIN2H_".to_owned();
out.reserve(name.len());
out.extend(sanitise_label(name).chars().flat_map(|c| c.to_uppercase()));
// Ensure guard ends with _H
if !out.ends_with("_H") {
out += "_H";
}
out
}
/// If the job is for a plain text file or a binary file
enum JobType {
Binary,
Text,
}
/// Structure for reading jobs, containing the path and type of job
struct Job {
job_type: JobType,
path: PathBuf
}
struct Arguments {
out: PathBuf,
whitespace: String,
}
impl Default for Arguments {
fn default() -> Self {
Self {
out: PathBuf::new(),
whitespace: "\t".into(),
}
}
}
/// Write an array from a binary file
fn bin2h(name: &str, mut file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
let ident = sanitise_label(name);
// Write length
let length = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(0))?;
writeln!(out, "#define {}_SIZE {}", ident.to_uppercase(), length)?;
// Write signature
writeln!(out, "static const unsigned char {ident}[{length}] = {{")?;
// Write values
let mut reader = BufReader::with_capacity(16, file);
let mut first_line = true;
loop {
// Get the next row of bytes
let bytes = reader.fill_buf()?;
let bytes_len = bytes.len();
if bytes.is_empty() {
writeln!(out)?;
break;
}
// Terminate the previous row
if first_line {
first_line = false;
} else {
writeln!(out, ",")?;
}
// Write row as hex bytes
for (col, byte) in bytes.iter().enumerate() {
let prefix = if col == 0 { whitespace } else { ", " };
write!(out, "{prefix}0x{byte:02X}")?;
}
reader.consume(bytes_len);
}
// Write array terminator
writeln!(out, "}};")?;
Ok(())
}
/// Write a C-string from a plain text file
fn txt2h(name: &str, file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
let ident = sanitise_label(name);
// Write signature
writeln!(out, "static const char* const {ident} =")?;
// Write lines
let mut reader = BufReader::new(file);
let mut line = String::new();
let mut first_line = true;
loop {
if reader.read_line(&mut line)? == 0 {
// End of file
writeln!(out, ";")?;
break;
}
// Separate lines
if first_line {
first_line = false;
} else {
writeln!(out)?;
}
// Write line
write!(out, "{whitespace}\"")?;
for c in line.chars() {
match c {
// Escape backslash and double-quotes
'\\' => write!(out, "\\\\")?,
'"' => write!(out, "\\\"")?,
// Write control codes as character escapes
'\x07' => write!(out, "\\a")?,
'\x08' => write!(out, "\\b")?,
'\x0C' => write!(out, "\\f")?,
'\n' => write!(out, "\\n")?,
'\r' => write!(out, "\\r")?,
'\t' => write!(out, "\\t")?,
'\x0B' => write!(out, "\\v")?,
// Write ASCII control codes that don't have C character escapes as hex codes
_ if c.is_ascii_control() => write!(out, "\\x{:02X}", c as u32)?,
// Write remaining ASCII characters verbatim
_ if c.is_ascii() => write!(out, "{c}")?,
// Write non-ASCII characters as unicode escapes
..'\u{10000}' => write!(out, "\\u{:04X}", c as u32)?,
_ => write!(out, "\\U{:08X}", c as u32)?,
}
}
write!(out, "\"")?;
line.clear();
}
Ok(())
}
/// Generates and writes out a header file
fn write_h<'a, I: Iterator<Item = &'a Job>>(opt: &Arguments, jobs: I) -> std::io::Result<()> {
let mut out = File::create(&opt.out)?;
let guard = guard_name(&opt.out.file_name().unwrap().to_string_lossy());
writeln!(out, "/*DO NOT EDIT")?;
writeln!(out, " * Autogenerated by bin2h")?;
writeln!(out, " */")?;
writeln!(out)?;
writeln!(out, "#ifndef {guard}")?;
writeln!(out, "#define {guard}")?;
writeln!(out)?;
for job in jobs {
let name = job.path.file_stem().unwrap().to_string_lossy();
let file = File::open(&job.path)?;
match job.job_type {
JobType::Binary => bin2h(&name, file, &mut out, &opt.whitespace)?,
JobType::Text => txt2h(&name, file, &mut out, &opt.whitespace)?,
}
writeln!(out)?;
}
writeln!(out, "#endif/*{guard}*/")?;
Ok(())
}
pub fn main() -> ExitCode {
// Program arguments
let mut arguments = Arguments::default();
let mut jobs = vec![];
// Read & parse arguments from the command line, store results into the above structure
enum Arg { Out, Bin, Txt, Whitespace, Help }
const OPTIONS: Opts<Arg> = Opts::new(&[
Opt::flag(Arg::Help, &["--help", "-h"], "Show this help message and exit"),
Opt::positional_required(Arg::Out, "out", "Path to generated header file"),
Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin", "Add a binary file"),
Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt", "Add a text file"),
Opt::value(Arg::Whitespace, &["--whitespace"], "\" \"", "Emitted indentation (Default: \"\\t\")"),
]);
match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| {
match id {
Arg::Out => { arguments.out = arg.into(); }
Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: arg.into() }); }
Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: arg.into() }); }
Arg::Whitespace => { arguments.whitespace = arg.into(); }
Arg::Help => {
OPTIONS.print_full_help(program_name);
return Ok(ParseControl::Quit);
}
}
Ok(ParseControl::Continue)
}) {
ParseResult::ContinueSuccess => {
// Generate header
match write_h(&arguments, jobs.iter()) {
Ok(_) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
ExitCode::FAILURE
}
}
},
ParseResult::ExitSuccess => { ExitCode::SUCCESS }
ParseResult::ExitError => { ExitCode::FAILURE }
}
}