embed_bytes/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
use bytes::Bytes;
use std::fs::{self, File};
use std::io::{self, BufWriter, Write};
use std::path::Path;
/// Writes byte arrays to separate `.bin` files in a specified directory and generates
/// a `.rs` file adjacent to the directory. The `.rs` file uses `include_bytes!` to
/// reference the `.bin` files.
///
/// # Arguments
/// * `index_output_path` - The path to the directory where the `.bin` files will be saved.
/// This directory must exist or will be created by the function.
/// * `byte_arrays` - A vector of tuples `(name, content)`, where:
/// - `name` is the name of the static variable to be generated in the `.rs` file.
/// - `content` is a `Bytes` object containing the binary data.
///
/// # Behavior
/// 1. Ensures the `index_output_path` exists or creates it if it does not.
/// 2. For each entry in `byte_arrays`, writes the binary data to a `.bin` file inside
/// the specified directory.
/// 3. Generates a `.rs` file adjacent to the directory. The `.rs` file includes static
/// variable declarations that use `include_bytes!` to reference the corresponding `.bin` files.
///
/// # Errors
/// Returns an `io::Error` if:
/// - The specified `index_output_path` is not a directory.
/// - A file operation (e.g., creating or writing files) fails.
///
/// # Example
/// ```rust
/// use bytes::Bytes;
/// use std::path::Path;
/// use your_crate::write_byte_arrays;
///
/// // Define the output directory
/// let output_dir = Path::new("embed");
///
/// // Define the byte arrays to write
/// let byte_arrays = vec![
/// ("ARRAY_ONE", Bytes::from(vec![1, 2, 3, 4])),
/// ("ARRAY_TWO", Bytes::from(vec![5, 6, 7, 8])),
/// ];
///
/// // Write the byte arrays and generate the Rust file
/// write_byte_arrays(output_dir, byte_arrays).unwrap();
/// ```
///
/// # Output
/// If `index_output_path` is `embed`, the following structure will be created:
/// ```plaintext
/// embed/
/// ├── ARRAY_ONE.bin
/// ├── ARRAY_TWO.bin
/// embed.rs
/// ```
///
/// The content of `embed.rs` will look like:
/// ```rust
/// // Automatically generated file. Do not edit.
/// // Generated by build-resource-byte-arrays crate.
///
/// pub static ARRAY_ONE: &[u8] = include_bytes!("embed/ARRAY_ONE.bin");
/// pub static ARRAY_TWO: &[u8] = include_bytes!("embed/ARRAY_TWO.bin");
/// ```
pub fn write_byte_arrays(
index_output_path: &Path,
byte_arrays: Vec<(&str, Bytes)>,
) -> io::Result<()> {
// Get and sanitize the directory name
let directory_name = index_output_path
.file_name()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid directory name"))?
.to_string_lossy();
let sanitized_name = sanitize_to_valid_rust_identifier(&directory_name)?;
// Ensure the output path is a directory
if !index_output_path.exists() {
fs::create_dir_all(index_output_path)?;
} else if !index_output_path.is_dir() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"index_output_path must be a directory.",
));
}
// Prepare the path for the Rust file (adjacent to the directory)
let rs_file_path = index_output_path
.with_extension("rs")
.file_name()
.map(|_| {
index_output_path
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!("{}.rs", sanitized_name))
})
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid directory name"))?;
// Open the Rust file and wrap it with a buffered writer
let rs_file = File::create(&rs_file_path)?;
let mut rs_writer = BufWriter::new(rs_file);
// Write a header to the Rust file
writeln!(
rs_writer,
"// Automatically generated file. Do not edit.\n\
// Generated by build-resource-byte-arrays crate.\n"
)?;
// Write `include_bytes!` statements for each byte array
for (name, content) in &byte_arrays {
let file_path = index_output_path.join(format!("{name}.bin"));
// Write the binary content to a separate file
let mut bin_file = File::create(&file_path)?;
bin_file.write_all(content)?;
// Write the `include_bytes!` statement using a path relative to the directory
writeln!(
rs_writer,
"pub static {name}: &[u8] = include_bytes!(\"{}/{}\");",
sanitized_name,
format!("{name}.bin")
)?;
}
Ok(())
}
fn sanitize_to_valid_rust_identifier(name: &str) -> Result<String, io::Error> {
if name.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Directory name cannot be empty.",
));
}
// Replace invalid characters with `_`
let sanitized: String = name
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
// Ensure it doesn't start with a digit
if sanitized.chars().next().unwrap().is_ascii_digit() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"Invalid directory name: '{}'. Directory names cannot start with a digit.",
name
),
));
}
// Check if the sanitized name is valid
if sanitized
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
Ok(sanitized)
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("Unable to sanitize directory name: '{}'.", name),
))
}
}