Skip to main content

boytacean_common/
util.rs

1//! Assorted utility functions and structures.
2//!
3//! This module contains various utility functions and structures
4//! that are used throughout the Boytacean codebase.
5
6use std::{
7    cell::RefCell,
8    fs::File,
9    io::{BufWriter, Read, Write},
10    path::Path,
11    rc::Rc,
12    sync::{Arc, Mutex},
13};
14
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use crate::error::Error;
19
20/// Shared mutable type able to be passed between types
21/// allowing for circular referencing and interior mutability.
22pub type SharedMut<T> = Rc<RefCell<T>>;
23
24/// Shared thread type able to be passed between threads.
25///
26/// Significant performance overhead compared to `SharedMut`.
27pub type SharedThread<T> = Arc<Mutex<T>>;
28
29/// The size of a BMP file header in bytes.
30const BMP_HEADER_SIZE: u32 = 54;
31
32/// Reads the contents of the file at the given path into
33/// a vector of bytes.
34pub fn read_file(path: &str) -> Result<Vec<u8>, Error> {
35    let mut file =
36        File::open(path).map_err(|_| Error::CustomError(format!("Failed to open file: {path}")))?;
37    let mut data = Vec::new();
38    file.read_to_end(&mut data)
39        .map_err(|_| Error::CustomError(format!("Failed to read from file: {path}")))?;
40    Ok(data)
41}
42
43/// Writes the given data to the file at the given path.
44pub fn write_file(path: &str, data: &[u8], flush: Option<bool>) -> Result<(), Error> {
45    let mut file = File::create(path)
46        .map_err(|_| Error::CustomError(format!("Failed to create file: {path}")))?;
47    file.write_all(data)
48        .map_err(|_| Error::CustomError(format!("Failed to write to file: {path}")))?;
49    if flush.unwrap_or(true) {
50        file.flush()
51            .map_err(|_| Error::CustomError(format!("Failed to flush file: {path}")))?;
52    }
53    Ok(())
54}
55
56/// Replaces the extension in the given path with the provided extension.
57///
58/// This function allows for simple associated file discovery.
59pub fn replace_ext(path: &str, new_extension: &str) -> Option<String> {
60    let file_path = Path::new(path);
61    let parent_dir = file_path.parent()?;
62    let file_stem = file_path.file_stem()?;
63    let file_extension = file_path.extension()?;
64    if file_stem == file_extension {
65        return None;
66    }
67    let new_file_name = format!("{}.{}", file_stem.to_str()?, new_extension);
68    let new_file_path = parent_dir.join(new_file_name);
69    Some(String::from(new_file_path.to_str()?))
70}
71
72/// Capitalizes the first character in the provided string.
73pub fn capitalize(string: &str) -> String {
74    let mut chars = string.chars();
75    match chars.next() {
76        None => String::new(),
77        Some(chr) => chr.to_uppercase().collect::<String>() + chars.as_str(),
78    }
79}
80
81/// Saves the pixel data as a BMP file at the specified path.
82/// The pixel data should be in RGB format, with each pixel
83/// represented by three bytes (red, green, blue).
84///
85/// This is a raw implementation of BMP file saving, not using any
86/// external libraries. It writes the BMP file header and pixel data
87/// directly to the file in the correct format.
88pub fn save_bmp(path: &str, pixels: &[u8], width: u32, height: u32) -> Result<(), Error> {
89    let file = File::create(path)
90        .map_err(|_| Error::CustomError(format!("Failed to create file: {path}")))?;
91    let mut writer = BufWriter::new(file);
92
93    // calculates the size of the BMP file header and the pixel data
94    // according to the BMP file format specification
95    let row_bytes = (width * 3 + 3) & !3;
96    let image_size = row_bytes * height;
97    let file_size = BMP_HEADER_SIZE + image_size;
98
99    // writes the BMP file header into the writer
100    writer.write_all(&[0x42, 0x4d]).unwrap(); // "BM" magic number
101    writer.write_all(&file_size.to_le_bytes()).unwrap(); // file size
102    writer.write_all(&[0x00, 0x00]).unwrap(); // reserved
103    writer.write_all(&[0x00, 0x00]).unwrap(); // reserved
104    writer.write_all(&[0x36, 0x00, 0x00, 0x00]).unwrap(); // offset to pixel data
105    writer.write_all(&[0x28, 0x00, 0x00, 0x00]).unwrap(); // DIB header size
106    writer.write_all(&(width as i32).to_le_bytes()).unwrap(); // image width
107    writer.write_all(&(height as i32).to_le_bytes()).unwrap(); // image height
108    writer.write_all(&[0x01, 0x00]).unwrap(); // color planes
109    writer.write_all(&[0x18, 0x00]).unwrap(); // bits per pixel
110    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // compression method
111    writer.write_all(&image_size.to_le_bytes()).unwrap(); // image size
112    writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // horizontal resolution (72 DPI)
113    writer.write_all(&[0x13, 0x0b, 0x00, 0x00]).unwrap(); // vertical resolution (72 DPI)
114    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // color palette
115    writer.write_all(&[0x00, 0x00, 0x00, 0x00]).unwrap(); // important colors
116
117    // iterates over the complete array of pixels in reverse order
118    // to account for the fact that BMP files are stored upside down
119    for y in (0..height).rev() {
120        for x in 0..width {
121            let [r, g, b] = [
122                pixels[((y * width + x) * 3) as usize],
123                pixels[((y * width + x) * 3 + 1) as usize],
124                pixels[((y * width + x) * 3 + 2) as usize],
125            ];
126            writer.write_all(&[b, g, r]).unwrap();
127        }
128        let padding = (4 - ((width * 3) % 4)) % 4;
129        for _ in 0..padding {
130            writer.write_all(&[0x00]).unwrap();
131        }
132    }
133
134    Ok(())
135}
136
137/// Copies the contents of the source slice into the destination slice.
138///
139/// This function is optimized for performance and uses pointer-based
140/// operations to copy the data as fast as possible.
141pub fn copy_fast(src: &[u8], dst: &mut [u8], count: usize) {
142    assert!(src.len() >= count);
143    assert!(dst.len() >= count);
144
145    unsafe {
146        let src_ptr = src.as_ptr();
147        let dst_ptr = dst.as_mut_ptr();
148        std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, count);
149    }
150}
151
152/// Interleaves two arrays of bytes into a single array using
153/// a pointer-based approach for performance reasons.
154pub fn interleave_arrays(a: &[u8], b: &[u8], output: &mut [u8]) {
155    assert_eq!(a.len(), b.len());
156    assert_eq!(output.len(), a.len() + b.len());
157
158    let len = a.len();
159
160    unsafe {
161        let mut out_ptr = output.as_mut_ptr();
162        let mut a_ptr = a.as_ptr();
163        let mut b_ptr = b.as_ptr();
164
165        for _ in 0..len {
166            std::ptr::write(out_ptr, *a_ptr);
167            out_ptr = out_ptr.add(1);
168            a_ptr = a_ptr.add(1);
169
170            std::ptr::write(out_ptr, *b_ptr);
171            out_ptr = out_ptr.add(1);
172            b_ptr = b_ptr.add(1);
173        }
174    }
175}
176
177/// Flips a 2D array of pixels vertically, in place.
178///
179/// This function is optimized for performance and uses pointer-based
180/// operations to flip the pixels as fast as possible.
181pub fn flip_vertical(pixels: &[u8], width: usize, height: usize, channels: usize) -> Vec<u8> {
182    let row_len = width * channels;
183    let mut flipped = vec![0u8; pixels.len()];
184    for y in 0..height {
185        let src = &pixels[y * row_len..(y + 1) * row_len];
186        let dst = &mut flipped[(height - 1 - y) * row_len..(height - y) * row_len];
187        dst.copy_from_slice(src);
188    }
189    flipped
190}
191
192/// Returns the current timestamp in seconds since the UNIX epoch.
193///
194/// This function has different implementations depending on whether
195/// the `wasm` feature is enabled or not.
196#[cfg(not(feature = "wasm"))]
197pub fn timestamp() -> u64 {
198    use std::time::{SystemTime, UNIX_EPOCH};
199
200    let now = SystemTime::now();
201    now.duration_since(UNIX_EPOCH).unwrap().as_secs()
202}
203
204/// Returns the current timestamp in seconds since the UNIX epoch.
205///
206/// This function has different implementations depending on whether
207/// the `wasm` feature is enabled or not.
208///
209/// WASM implementation is a static one using `js_sys::Date`.
210#[cfg(feature = "wasm")]
211#[cfg_attr(feature = "wasm", wasm_bindgen)]
212pub fn timestamp() -> u64 {
213    use js_sys::Date;
214
215    (Date::now() / 1000.0) as u64
216}
217
218#[cfg(test)]
219mod tests {
220    use std::{
221        env::temp_dir,
222        fs::{read, remove_file},
223        path::Path,
224    };
225
226    use super::{capitalize, replace_ext, save_bmp};
227
228    #[test]
229    fn test_change_extension() {
230        let new_path = replace_ext("/path/to/file.txt", "dat").unwrap();
231        assert_eq!(
232            new_path,
233            Path::new("/path/to").join("file.dat").to_str().unwrap()
234        );
235
236        let new_path = replace_ext("/path/to/file.with.multiple.dots.txt", "dat").unwrap();
237        assert_eq!(
238            new_path,
239            Path::new("/path/to")
240                .join("file.with.multiple.dots.dat")
241                .to_str()
242                .unwrap()
243        );
244
245        let new_path = replace_ext("/path/to/file.without.extension", "dat").unwrap();
246        assert_eq!(
247            new_path,
248            Path::new("/path/to")
249                .join("file.without.dat")
250                .to_str()
251                .unwrap()
252        );
253
254        let new_path = replace_ext("/path/to/directory/", "dat");
255        assert_eq!(new_path, None);
256    }
257
258    #[test]
259    fn test_capitalize_empty_string() {
260        let result = capitalize("");
261        assert_eq!(result, "");
262    }
263
264    #[test]
265    fn test_capitalize_single_character() {
266        let result = capitalize("a");
267        assert_eq!(result, "A");
268    }
269
270    #[test]
271    fn test_capitalize_multiple_characters() {
272        let result = capitalize("hello, world!");
273        assert_eq!(result, "Hello, world!");
274    }
275
276    #[test]
277    fn test_bmp_le_bytes() {
278        // according to the BMP file format specification, both the file size
279        // and the image size fields are stored using little-endian encoding.
280        let path = temp_dir().join("boytacean_le_test.bmp");
281        save_bmp(path.to_str().unwrap(), &[255, 0, 0], 1, 1).expect("Failed to save BMP file");
282        let data: Vec<u8> = read(&path).unwrap();
283        assert_eq!(&data[2..6], &(58u32).to_le_bytes());
284        assert_eq!(&data[34..38], &(4u32).to_le_bytes());
285        remove_file(path).unwrap();
286    }
287
288    #[test]
289    fn test_bmp_file_structure() {
290        // Creates a 2x2 image and verifies that the BMP header follows the
291        // expected structure as defined in the specification.
292        let path = temp_dir().join("boytacean_spec_test.bmp");
293        let pixels = [
294            255, 0, 0, // red
295            0, 255, 0, // green
296            0, 0, 255, // blue
297            255, 255, 0, // yellow
298        ];
299        save_bmp(path.to_str().unwrap(), &pixels, 2, 2).expect("Failed to save BMP file");
300        let data = read(&path).unwrap();
301
302        // header checks
303        assert_eq!(&data[0..2], b"BM");
304        assert_eq!(&data[2..6], &(70u32).to_le_bytes());
305        assert_eq!(&data[10..14], &(54u32).to_le_bytes());
306        assert_eq!(&data[14..18], &(40u32).to_le_bytes());
307        assert_eq!(&data[18..22], &(2i32).to_le_bytes());
308        assert_eq!(&data[22..26], &(2i32).to_le_bytes());
309        assert_eq!(&data[26..28], &(1u16).to_le_bytes());
310        assert_eq!(&data[28..30], &(24u16).to_le_bytes());
311        assert_eq!(&data[34..38], &(16u32).to_le_bytes());
312
313        remove_file(path).unwrap();
314    }
315}