Skip to main content

embed_utils/
embed_utils.rs

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