/* 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>(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 = 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 } } }