catconf/
lib.rs

1// catconf
2// Copyright (C) 2023 Andrew Rioux
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # Catconf
18//!
19//! For when you want:
20//! 1. Runtime configuration for after the binary is compiled
21//! 2. A single file binary
22//!
23//! This library allows for taking the final result binary, and just concatenating the configuration to the end:
24//!
25//! `cat target/debug/binary <(echo -n "CATCONF") conf > confedbinary`
26//!
27//! Great, but how to get the configuration out and use it in the code? catconf!
28//!
29//! It's use is pretty simple:
30//!
31//! ```
32//! use catconf::ConfReaderOptions;
33//!
34//! # fn main () -> std::io::Result<()> {
35//! let conf_reader = ConfReaderOptions::new(b"CATCONF".to_vec()).read_from_exe()?;
36//! # Ok(())
37//! # }
38//! ```
39//!
40//! This returns a <code>[Vec]\<u8></code> which can be transformed further, by converting to UTF-8 and
41//! combined with Serde, decompressing with zlib, etc
42
43use std::{
44    env,
45    fs::OpenOptions,
46    io::{self, prelude::*, SeekFrom},
47};
48
49/// Internal function used to just reference the current executable
50pub(crate) fn open_current_exe() -> io::Result<std::fs::File> {
51    OpenOptions::new().read(true).open(env::current_exe()?)
52}
53
54/// Builder struct to allow for configuring the eventual call to read from a file
55/// It has two primary properties:
56///
57/// 1. Magic bytes: the bytes used to
58/// 2. Window size: the size of the window used to scan the file. This library
59///     will read in twice the window size to fill its internal buffer
60///
61/// # Example
62///
63/// ```
64/// use catconf::ConfReaderOptions;
65///
66/// # fn main() -> std::io::Result<()> {
67/// let conf = ConfReaderOptions::new(b"CATCONF".to_vec()).read_from_exe()?;
68/// # Ok(())
69/// # }
70/// ```
71pub struct ConfReaderOptions {
72    magic_bytes_opt: Vec<u8>,
73    window_size_opt: i64,
74}
75
76impl ConfReaderOptions {
77    /// Create a new ConfReaderOptions builder with the magic bytes specified.
78    ///
79    /// The window size is initially set to 2048
80    ///
81    /// # Example
82    ///
83    /// ```
84    /// use catconf::ConfReaderOptions;
85    ///
86    /// # fn main() -> std::io::Result<()> {
87    /// let mut options = ConfReaderOptions::new(b"CATCONF".to_vec());
88    /// let conf = options.window_size(4096).read_from_exe()?;
89    /// # Ok(())
90    /// # }
91    /// ```
92    pub fn new(bytes: Vec<u8>) -> Self {
93        ConfReaderOptions {
94            magic_bytes_opt: bytes,
95            window_size_opt: 2048,
96        }
97    }
98
99    /// Set the magic bytes to a different value
100    ///
101    /// # Example
102    ///
103    /// ```
104    /// use catconf::ConfReaderOptions;
105    ///
106    /// # fn main() -> std::io::Result<()> {
107    /// let options = ConfReaderOptions::new(b"CATCONF".to_vec())
108    ///     .magic_bytes(b"NOTCATCONF".to_vec())
109    ///     .read_from_exe()?;
110    /// # Ok(())
111    /// # }
112    /// ```
113    pub fn magic_bytes(&mut self, bytes: Vec<u8>) -> &mut Self {
114        self.magic_bytes_opt = bytes;
115        self
116    }
117
118    /// Sets the window size, influencing the amount of reads that are performed on disk
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use catconf::ConfReaderOptions;
124    ///
125    /// # fn main() -> std::io::Result<()> {
126    /// let conf = ConfReaderOptions::new(b"CATCONF".to_vec())
127    ///     .window_size(4096)
128    ///     .read_from_exe()?;
129    /// # Ok(())
130    /// # }
131    /// ```
132    pub fn window_size(&mut self, size: u32) -> &mut Self {
133        self.window_size_opt = size as i64;
134        self
135    }
136
137    /// Takes the configuration options provided and actually reads from the input file to
138    /// gather the configuration
139    ///
140    /// # Example
141    ///
142    /// ```
143    /// use catconf::ConfReaderOptions;
144    ///
145    /// # fn main() -> std::io::Result<()> {
146    /// # let buff = vec![0;4096];
147    /// # let mut input = std::io::Cursor::new(&buff);
148    /// let conf = ConfReaderOptions::new(b"CATCONF".to_vec())
149    ///     .read(&mut input);
150    /// # Ok(())
151    /// # }
152    /// ```
153    pub fn read<F>(&self, input: &mut F) -> io::Result<Vec<u8>>
154    where
155        F: Seek + Read,
156    {
157        read_from_file(&self.magic_bytes_opt, self.window_size_opt, input)
158    }
159
160    /// Helper method to go along with [`ConfReaderOptions::read`] in order to read from the
161    /// program currently checking for configuration
162    ///
163    /// Functionally equivalent to:
164    ///
165    /// ```
166    /// use catconf::ConfReaderOptions;
167    ///
168    /// # fn main() -> std::io::Result<()> {
169    /// let mut current_exe = std::fs::OpenOptions::new().read(true).open(std::env::current_exe()?)?;
170    /// let conf = ConfReaderOptions::new(b"CATCONF".to_vec()).read(&mut current_exe)?;
171    /// # Ok(())
172    /// # }
173    /// ```
174    ///
175    /// # Example
176    ///
177    /// ```
178    /// use catconf::ConfReaderOptions;
179    ///
180    /// # fn main() -> std::io::Result<()> {
181    /// let conf = ConfReaderOptions::new(b"CATCONF".to_vec()).read_from_exe()?;
182    /// # Ok(())
183    /// # }
184    /// ```
185    pub fn read_from_exe(&self) -> io::Result<Vec<u8>> {
186        let mut cur_exe = open_current_exe()?;
187        self.read(&mut cur_exe)
188    }
189}
190
191/// Useful if you just want to read from the current exe without bothering to use the builder
192///
193/// # Example
194///
195/// ```
196/// use catconf::read_from_exe;
197///
198/// # fn main() -> std::io::Result<()> {
199/// let conf = read_from_exe(b"CATCONF", 4096)?;
200/// # Ok(())
201/// # }
202/// ```
203pub fn read_from_exe(magic_bytes: &[u8], window_size: i64) -> io::Result<Vec<u8>> {
204    let mut cur_exe = open_current_exe()?;
205    read_from_file(magic_bytes, window_size, &mut cur_exe)
206}
207
208/// Allows for reading for configuration from the end of a file by looking for magic bytes
209///
210/// # Example
211///
212/// ```no_run
213/// use catconf::read_from_file;
214///
215/// # fn main() -> std::io::Result<()> {
216/// # let buff = vec![0; 4096];
217/// # let mut input = std::io::Cursor::new(&buff);
218/// let conf = read_from_file(b"CATCONF", 2048, &mut input)?;
219/// # Ok(())
220/// # }
221/// ```
222pub fn read_from_file<F>(magic_bytes: &[u8], window_size: i64, input: &mut F) -> io::Result<Vec<u8>>
223where
224    F: Seek + Read,
225{
226    let buffer_size = window_size * 2;
227    let mut current_window_index: i64 = 1;
228    let mut current_read_buffer = vec![0u8; buffer_size as usize];
229
230    loop {
231        input.seek(SeekFrom::End(-((current_window_index + 1) * window_size)))?;
232        let bytes_read = input.read(&mut current_read_buffer[..])?;
233
234        if bytes_read < window_size as usize {
235            break Err(io::Error::new(
236                io::ErrorKind::UnexpectedEof,
237                "reached beginning of the file without finding magic bytes",
238            ));
239        }
240
241        if let Some(pos) = current_read_buffer
242            .windows(magic_bytes.len())
243            .position(|window| window == magic_bytes)
244        {
245            let conf_buffer_size = window_size - pos as i64 - magic_bytes.len() as i64
246                + (current_window_index * window_size);
247            let mut conf_buffer = vec![0; conf_buffer_size as usize];
248
249            input.seek(SeekFrom::End(-conf_buffer_size))?;
250            input.read(&mut conf_buffer[..])?;
251
252            break Ok(conf_buffer);
253        }
254
255        current_window_index += 1;
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::io::Cursor;
262
263    use super::*;
264
265    /// Simplest use case
266    #[test]
267    fn pulls_basic_data() {
268        let input_data = [
269            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1,
270            1, 1, 1,
271        ];
272        let header = [1, 2, 3, 4];
273        let data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
274
275        let mut buf = Cursor::new(&input_data);
276
277        assert_eq!(&read_from_file(&header, 16, &mut buf).unwrap(), &data);
278    }
279
280    /// Check to make sure reads occur when going across window boundaries. For instance, in
281    /// this test the boundary will be split such that the first read reads "2,3,4,1,1,1...", missing
282    /// the boundary
283    #[test]
284    fn pulls_data_over_boundary() {
285        let input_data = [
286            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1,
287            1, 1, 1,
288        ];
289        let header = [1, 2, 3, 4];
290        let data = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
291
292        let mut buf = Cursor::new(&input_data);
293
294        assert_eq!(&read_from_file(&header, 15, &mut buf).unwrap(), &data);
295    }
296}