include_flate_codegen/
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
16extern crate proc_macro;
17
18use std::fs::{self, File};
19use std::io::{Read, Seek};
20use std::path::PathBuf;
21use std::str::{from_utf8, FromStr};
22
23use include_flate_compress::{apply_compression, CompressionMethod};
24use proc_macro::TokenStream;
25use proc_macro2::Span;
26use proc_macro_error::{emit_warning, proc_macro_error};
27use quote::quote;
28use syn::{Error, LitByteStr};
29
30/// `deflate_file!("file")` is equivalent to `include_bytes!("file.gz")`.
31///
32/// # Parameters
33/// This macro accepts exactly one literal parameter that refers to a path relative to
34/// `CARGO_MANIFEST_DIR`. Absolute paths are not supported.
35///
36/// Note that **this is distinct from the behaviour of the builtin `include_bytes!`/`include_str!` macros** —
37/// `includle_bytes!`/`include_str!` paths are relative to the current source file, while `deflate_file!` paths are relative to
38/// `CARGO_MANIFEST_DIR`.
39///
40/// # Returns
41/// This macro expands to a `b"byte string"` literal that contains the deflated form of the file.
42///
43/// # Compile errors
44/// - If the argument is not a single literal
45/// - If the referenced file does not exist or is not readable
46#[proc_macro]
47#[proc_macro_error]
48pub fn deflate_file(ts: TokenStream) -> TokenStream {
49    match inner(ts, false) {
50        Ok(ts) => ts.into(),
51        Err(err) => err.to_compile_error().into(),
52    }
53}
54
55/// This macro is identical to `deflate_file!()`, except it additionally performs UTF-8 validation.
56///
57/// # Compile errors
58/// - The compile errors in `deflate_file!`
59/// - If the file contents are not all valid UTF-8
60#[proc_macro]
61#[proc_macro_error]
62pub fn deflate_utf8_file(ts: TokenStream) -> TokenStream {
63    match inner(ts, true) {
64        Ok(ts) => ts.into(),
65        Err(err) => err.to_compile_error().into(),
66    }
67}
68
69/// An arguments expected provided by the proc-macro.
70///
71/// ```ignore
72/// flate!(pub static DATA: [u8] from "assets/009f.dat"); // default, DEFLATE
73/// flate!(pub static DATA: [u8] from "assets/009f.dat" with zstd); // Use Zstd for this file spcifically
74/// flate!(pub static DATA: [u8] from "assets/009f.dat" with deflate); // Explicitly use DEFLATE.
75/// ```
76struct FlateArgs {
77    path: syn::LitStr,
78    algorithm: Option<CompressionMethodTy>,
79}
80
81impl syn::parse::Parse for FlateArgs {
82    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
83        let path = input.parse()?;
84
85        let algorithm = if input.is_empty() {
86            None
87        } else {
88            let lookahead = input.lookahead1();
89            if lookahead.peek(kw::deflate) {
90                input.parse::<kw::deflate>()?;
91                Some(CompressionMethodTy(CompressionMethod::Deflate))
92            } else if lookahead.peek(kw::zstd) {
93                input.parse::<kw::zstd>()?;
94                Some(CompressionMethodTy(CompressionMethod::Zstd))
95            } else {
96                return Err(lookahead.error());
97            }
98        };
99
100        Ok(Self { path, algorithm })
101    }
102}
103
104mod kw {
105    syn::custom_keyword!(deflate);
106    syn::custom_keyword!(zstd);
107}
108
109#[derive(Debug)]
110struct CompressionMethodTy(CompressionMethod);
111
112fn compression_ratio(original_size: u64, compressed_size: u64) -> f64 {
113    (compressed_size as f64 / original_size as f64) * 100.0
114}
115
116fn inner(ts: TokenStream, utf8: bool) -> syn::Result<impl Into<TokenStream>> {
117    fn emap<E: std::fmt::Display>(error: E) -> Error {
118        Error::new(Span::call_site(), error)
119    }
120
121    let dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").map_err(emap)?);
122
123    let args: FlateArgs = syn::parse2::<FlateArgs>(ts.to_owned().into())?;
124    let path = PathBuf::from_str(&args.path.value()).map_err(emap)?;
125    let algo = args
126        .algorithm
127        .unwrap_or(CompressionMethodTy(CompressionMethod::Deflate));
128
129    if path.is_absolute() {
130        Err(emap("absolute paths are not supported"))?;
131    }
132
133    let target = dir.join(&path);
134
135    let mut file = File::open(&target).map_err(emap)?;
136
137    let mut vec = Vec::<u8>::new();
138    if utf8 {
139        std::io::copy(&mut file, &mut vec).map_err(emap)?;
140        from_utf8(&vec).map_err(emap)?;
141    }
142
143    let mut compressed_buffer = Vec::<u8>::new();
144
145    {
146        let mut compressed_cursor = std::io::Cursor::new(&mut compressed_buffer);
147        let mut source: Box<dyn Read> = if utf8 {
148            Box::new(std::io::Cursor::new(vec))
149        } else {
150            file.seek(std::io::SeekFrom::Start(0)).map_err(emap)?;
151            Box::new(&file)
152        };
153
154        apply_compression(&mut source, &mut compressed_cursor, algo.0).map_err(emap)?;
155    }
156
157    let bytes = LitByteStr::new(&compressed_buffer, Span::call_site());
158    let result = quote!(#bytes);
159
160    #[cfg(not(feature = "no-compression-warnings"))]
161    {
162        let compression_ratio = compression_ratio(
163            fs::metadata(&target).map_err(emap)?.len(),
164            compressed_buffer.len() as u64,
165        );
166
167        if compression_ratio < 10.0f64 {
168            emit_warning!(
169            &args.path,
170            "Detected low compression ratio ({:.2}%) for file {:?} with `{:?}`. Consider using other compression methods.",
171            compression_ratio,
172            path.display(),
173            algo.0,
174        );
175        }
176    }
177
178    Ok(result)
179}