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),
        ))
    }
}