prest_embed_utils/
lib.rs

1#![forbid(unsafe_code)]
2
3use sha2::Digest;
4use std::borrow::Cow;
5use std::path::Path;
6use std::time::SystemTime;
7use std::{fs, io};
8
9/// Utility used for debug and lazy embeddings
10#[doc(hidden)]
11#[cfg_attr(debug_assertions, allow(unused))]
12pub struct FileEntry {
13    pub rel_path: String,
14    pub full_canonical_path: String,
15}
16
17#[doc(hidden)]
18pub fn is_path_included(rel_path: &str, includes: &[&str], excludes: &[&str]) -> bool {
19    use globset::Glob;
20
21    // ignore path matched by exclusion pattern
22    for exclude in excludes {
23        let pattern = Glob::new(exclude)
24            .unwrap_or_else(|_| panic!("invalid exclude pattern '{}'", exclude))
25            .compile_matcher();
26
27        if pattern.is_match(rel_path) {
28            return false;
29        }
30    }
31
32    // accept path if no includes provided
33    if includes.is_empty() {
34        return true;
35    }
36
37    // accept path if matched by inclusion pattern
38    for include in includes {
39        let pattern = Glob::new(include)
40            .unwrap_or_else(|_| panic!("invalid include pattern '{}'", include))
41            .compile_matcher();
42
43        if pattern.is_match(rel_path) {
44            return true;
45        }
46    }
47
48    false
49}
50
51/// Utility that finds files to embed
52#[doc(hidden)]
53#[cfg_attr(debug_assertions, allow(unused))]
54pub fn get_files<'patterns>(
55    folder_path: String,
56    includes: &'patterns [&str],
57    excludes: &'patterns [&str],
58) -> impl Iterator<Item = FileEntry> + 'patterns {
59    walkdir::WalkDir::new(&folder_path)
60        .follow_links(true)
61        .sort_by_file_name()
62        .into_iter()
63        // ignore errors
64        .filter_map(|e| e.ok())
65        // only files
66        .filter(|e| e.file_type().is_file())
67        // respect includes and excludes
68        .filter_map(move |e| {
69            let rel_path = path_to_str(
70                e.path()
71                    .strip_prefix(&folder_path)
72                    .expect("Paths should be convertible to strings"),
73            );
74            let full_canonical_path =
75                path_to_str(std::fs::canonicalize(e.path()).expect("Could not get canonical path"));
76
77            let rel_path = if std::path::MAIN_SEPARATOR == '\\' {
78                rel_path.replace('\\', "/")
79            } else {
80                rel_path
81            };
82
83            if is_path_included(&rel_path, includes, excludes) {
84                Some(FileEntry {
85                    rel_path,
86                    full_canonical_path,
87                })
88            } else {
89                None
90            }
91        })
92}
93
94/// A file embedded into the binary
95#[derive(Clone)]
96pub struct EmbeddedFile {
97    pub data: Cow<'static, [u8]>,
98    pub metadata: EmbeddedFileMetadata,
99}
100
101/// EmbeddedFileMetadata about an embedded file
102#[doc(hidden)]
103#[derive(Clone)]
104pub struct EmbeddedFileMetadata {
105    hash: [u8; 32],
106    last_modified: Option<u64>,
107    mimetype: Cow<'static, str>,
108}
109
110impl EmbeddedFileMetadata {
111    #[doc(hidden)]
112    pub const fn __rust_embed_new(
113        hash: [u8; 32],
114        last_modified: Option<u64>,
115        mimetype: &'static str,
116    ) -> Self {
117        Self {
118            hash,
119            last_modified,
120            mimetype: Cow::Borrowed(mimetype),
121        }
122    }
123
124    /// The SHA256 hash of the file
125    pub fn sha256_hash(&self) -> [u8; 32] {
126        self.hash
127    }
128
129    /// The last modified date in seconds since the UNIX epoch. If the underlying
130    /// platform/file-system does not support this, None is returned.
131    pub fn last_modified(&self) -> Option<u64> {
132        self.last_modified
133    }
134
135    /// The mime type of the file
136    pub fn mimetype(&self) -> &str {
137        &self.mimetype
138    }
139}
140
141#[doc(hidden)]
142pub fn read_file_from_fs(file_path: &Path) -> io::Result<EmbeddedFile> {
143    let data = fs::read(file_path)?;
144    let data = Cow::from(data);
145
146    let mut hasher = sha2::Sha256::new();
147    hasher.update(&data);
148    let hash: [u8; 32] = hasher.finalize().into();
149
150    let source_date_epoch = match std::env::var("SOURCE_DATE_EPOCH") {
151        Ok(value) => value.parse::<u64>().map_or(None, |v| Some(v)),
152        Err(_) => None,
153    };
154
155    let last_modified = fs::metadata(file_path)?
156        .modified()
157        .ok()
158        .map(|last_modified| {
159            last_modified
160                .duration_since(SystemTime::UNIX_EPOCH)
161                .expect("Time before the UNIX epoch is unsupported")
162                .as_secs()
163        });
164
165    let mimetype = mime_guess::from_path(file_path)
166        .first_or_octet_stream()
167        .to_string();
168
169    Ok(EmbeddedFile {
170        data,
171        metadata: EmbeddedFileMetadata {
172            hash,
173            last_modified: source_date_epoch.or(last_modified),
174            mimetype: mimetype.into(),
175        },
176    })
177}
178
179fn path_to_str<P: AsRef<std::path::Path>>(p: P) -> String {
180    p.as_ref()
181        .to_str()
182        .expect("Path does not have a string representation")
183        .to_owned()
184}