embed_utils/
embed_utils.rs1#![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#[derive(Clone)]
44pub struct EmbeddedFile {
45 pub data: Cow<'static, [u8]>,
46 pub metadata: Metadata,
47}
48
49#[derive(Clone)]
51pub struct Metadata {
52 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 pub fn hash(&self) -> [u8; 32] {
79 self.hash
80 }
81
82 pub fn last_modified(&self) -> Option<u64> {
85 self.last_modified
86 }
87
88 pub fn created(&self) -> Option<u64> {
91 self.created
92 }
93
94 #[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}