Skip to main content

include_flate/
lib.rs

1// include-flate
2// Copyright (C) SOFe
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! A variant of `include_bytes!`/`include_str!` with compile-time deflation and runtime lazy inflation.
17//!
18//! ## Why?
19//! `include_bytes!`/`include_str!` are great for embedding resources into an executable/library
20//! without involving the complex logistics of maintaining an assets manager.
21//! However, they are copied as-is into the artifact, leading to unnecessarily large binary size.
22//! This library automatically compresses the resources and lazily decompresses them at runtime,
23//! allowing smaller binary sizes.
24//!
25//! Nevertheless, this inevitably leads to wasting RAM to store both the compressed and decompressed data,
26//! which might be undesirable if the data are too large.
27//! An actual installer is still required if the binary involves too many resources that do not need to be kept in RAM all time.
28
29/// The low-level macros used by this crate.
30pub use include_flate_codegen as codegen;
31use include_flate_compress::apply_decompression;
32use std::string::FromUtf8Error;
33
34pub use include_flate_compress::CompressionMethod;
35
36/// This macro is like [`include_bytes!`][1] or [`include_str!`][2], but compresses at compile time
37/// and lazily decompresses at runtime.
38///
39/// # Parameters
40/// The macro can be used like this:
41/// ```ignore
42/// flate!($meta $vis static $name: $type from $file);
43/// ```
44///
45/// - `$meta` is zero or more `#[...]` attributes that can be applied to the generated `static` [`LazyLock<...>`][3].
46/// - `$vis` is a visibility modifier (e.g. `pub`, `pub(crate)`) or empty.
47/// - `$name` is the name of the static variable..
48/// - `$type` can be either `[u8]` or `str`. However, the actual type created would dereference
49///   into `Vec<u8>` and `String` (although they are `AsRef<[u8]>` and `AsRef<str>`) respectively.
50/// - `$file` is a path relative to the current [`CARGO_MANIFEST_DIR`][4]. Absolute paths are not supported.
51///   Note that **this is distinct from the behaviour of the builtin `include_bytes!`/`include_str!`
52///   macros** &mdash; `includle_bytes!`/`include_str!` paths are relative to the current source file,
53///   while `flate!` paths are relative to `CARGO_MANIFEST_DIR`.
54///
55/// # Returns
56/// The macro expands to a `static` [`LazyLock<...>`][3], which lazily inflates the compressed bytes.
57///
58/// # Compile errors
59/// - If the input format is incorrect
60/// - If the referenced file does not exist or is not readable
61/// - If `$type` is `str` but the file is not fully valid UTF-8
62///
63/// # Algorithm
64/// Compression and decompression use the DEFLATE algorithm from [`libflate`][5].
65///
66/// # Examples
67/// Below are some basic examples. For actual compiled examples, see the [`tests`][6] directory.
68///
69/// ```ignore
70/// // This declares a `static VAR_NAME: impl Deref<Vec<u8>>`
71/// flate!(static VAR_NAME: [u8] from "binary-file.dat");
72///
73/// // This declares a `static VAR_NAME: impl Deref<String>`
74/// flate!(static VAR_NAME: str from "text-file.txt");
75///
76/// // Visibility modifiers can be added in the front
77/// flate!(pub static VAR_NAME: str from "public-file.txt");
78///
79/// // Meta attributes can also be added
80/// flate!(#[allow(unused)]
81///        #[doc = "Example const"]
82///        pub static VAR_NAME: str from "file.txt");
83/// ```
84///
85///   [1]: https://doc.rust-lang.org/std/macro.include_bytes.html
86///   [2]: https://doc.rust-lang.org/std/macro.include_str.html
87///   [3]: https://doc.rust-lang.org/std/sync/struct.LazyLock.html
88///   [4]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
89///   [5]: https://docs.rs/libflate/0.1.26/libflate/
90///   [6]: https://github.com/SOF3/include-flate/tree/master/tests
91#[macro_export]
92macro_rules! flate {
93    ($(#[$meta:meta])*
94        $(pub $(($($vis:tt)+))?)? static $name:ident: [u8] from $path:literal $(with $algo:ident)?) => {
95        // HACK: workaround to make cargo auto rebuild on modification of source file
96        const _: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path));
97
98        $(#[$meta])*
99        $(pub $(($($vis)+))?)? static $name: ::std::sync::LazyLock<::std::vec::Vec<u8>> = ::std::sync::LazyLock::new(|| {
100            let algo = $crate::__parse_algo!($($algo)?);
101            $crate::decode($crate::codegen::deflate_file!($path $($algo)?), Some(algo))
102        });
103    };
104    ($(#[$meta:meta])*
105        $(pub $(($($vis:tt)+))?)? static $name:ident: str from $path:literal $(with $algo:ident)?) => {
106        // HACK: workaround to make cargo auto rebuild on modification of source file
107        const _: &'static str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path));
108
109        $(#[$meta])*
110        $(pub $(($($vis)+))?)? static $name: ::std::sync::LazyLock<::std::string::String> = ::std::sync::LazyLock::new(|| {
111            let algo = $crate::__parse_algo!($($algo)?);
112            $crate::decode_string($crate::codegen::deflate_utf8_file!($path $($algo)?), Some(algo))
113        });
114    };
115    ($(#[$meta:meta])*
116        $(pub $(($($vis:tt)+))?)? static $name:ident: IFlate from $path:literal $(with $algo:ident)?) => {
117        // HACK: workaround to make cargo auto rebuild on modification of source file
118        const _: &'static [u8] = include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path));
119
120        $(#[$meta])*
121        $(pub $(($($vis)+))?)? static $name: ::std::sync::LazyLock<$crate::IFlate> = ::std::sync::LazyLock::new(|| {
122            let algo = $crate::__parse_algo!($($algo)?);
123            let compressed = $crate::codegen::deflate_file!($path $($algo)?);
124            $crate::IFlate::new(compressed, algo)
125        });
126    };
127}
128
129/// Helper macro to parse the user-specified algorithm.
130#[doc(hidden)]
131#[macro_export]
132macro_rules! __parse_algo {
133    () => {
134        $crate::CompressionMethod::default()
135    };
136    (deflate) => {
137        $crate::CompressionMethod::Deflate
138    };
139    (zstd) => {
140        $crate::CompressionMethod::Zstd
141    };
142    ($other:ident) => {
143        compile_error!("Unknown compression algorithm: {}", stringify!($other))
144    };
145}
146
147#[derive(Debug)]
148pub struct IFlate {
149    compressed: &'static [u8],
150    algo: CompressionMethod,
151}
152
153impl IFlate {
154    #[doc(hidden)]
155    pub fn new(compressed: &'static [u8], algo: CompressionMethod) -> Self {
156        Self { compressed, algo }
157    }
158
159    pub fn compressed(&self) -> &[u8] {
160        self.compressed
161    }
162
163    pub fn decoded(&self) -> Vec<u8> {
164        decode(self.compressed, Some(self.algo))
165    }
166
167    pub fn decode_string(&self) -> Result<String, FromUtf8Error> {
168        String::from_utf8(self.decoded())
169    }
170
171    pub fn algo(&self) -> CompressionMethod {
172        self.algo
173    }
174}
175
176#[doc(hidden)]
177#[allow(private_interfaces)]
178pub fn decode(bytes: &[u8], algo: Option<CompressionMethod>) -> Vec<u8> {
179    use std::io::Cursor;
180
181    let algo = algo.unwrap_or_default();
182    let mut source = Cursor::new(bytes);
183    let mut ret = Vec::new();
184
185    match apply_decompression(&mut source, &mut ret, algo) {
186        Ok(_) => {}
187        Err(err) => panic!("Compiled `{:?}` buffer was corrupted: {:?}", algo, err),
188    }
189
190    ret
191}
192
193#[doc(hidden)]
194#[allow(private_interfaces)]
195pub fn decode_string(bytes: &[u8], algo: Option<CompressionMethod>) -> String {
196    // We should have checked for utf8 correctness in encode_utf8_file!
197    String::from_utf8(decode(bytes, algo))
198        .expect("flate_str has malformed UTF-8 despite checked at compile time")
199}