mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 07:20:18 +00:00
239 lines
6.7 KiB
Rust
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::help_flag(Arg::Help, &["--help", "-h"]).help_text("Show this help message and exit"),
|
|
Opt::positional(Arg::Out, "out").help_text("Path to generated header file").required(),
|
|
Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin").help_text("Add a binary file"),
|
|
Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt").help_text("Add a text file"),
|
|
Opt::value(Arg::Whitespace, &["--whitespace"], "\" \"").help_text("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 }
|
|
}
|
|
}
|