1use crate::{Error, Result};
5use std::path::PathBuf;
6
7pub struct FileCheck {
10 path: std::path::PathBuf,
11 _file: Option<std::fs::File>,
12}
13
14impl FileCheck {
15 pub fn path(&self) -> &std::path::Path {
17 &self.path
18 }
19}
20
21fn get_cache_dir() -> Result<std::path::PathBuf> {
26 match std::env::var("TX5_CACHE_DIRECTORY") {
27 Ok(cache_dir) => {
28 let path = PathBuf::from(cache_dir);
29 if path.is_dir() {
30 Ok(path)
31 } else {
32 Err(std::io::Error::other("env variable TX5_CACHE_DIRECTORY is set, but it is not a valid path to an existing directory"))
33 }
34 }
35 Err(_) => app_dirs2::app_root(
36 app_dirs2::AppDataType::UserCache,
37 &app_dirs2::AppInfo {
38 name: "host.holo.tx5",
39 author: "host.holo.tx5",
40 },
41 )
42 .map_err(std::io::Error::other),
43 }
44}
45
46pub fn file_check(
48 file_data: &[u8],
49 file_hash: &str,
50 file_name_prefix: &str,
51 file_name_ext: &str,
52) -> Result<FileCheck> {
53 let file_name = format!("{file_name_prefix}-{file_hash}{file_name_ext}");
54 let tmp_dir = get_cache_dir()?;
55
56 let mut pref_path = tmp_dir.clone();
57 pref_path.push(&file_name);
58
59 if let Ok(file) = validate(&pref_path, file_hash) {
60 return Ok(FileCheck {
61 path: pref_path.clone(),
62 _file: Some(file),
63 });
64 }
65
66 let mut tmp = write(tmp_dir, file_data)?;
67
68 match tmp.persist_noclobber(pref_path.clone()) {
72 Ok(mut file) => {
73 set_perms(&mut file)?;
74
75 drop(file);
76
77 let file = validate(&pref_path, file_hash)?;
78
79 return Ok(FileCheck {
80 path: pref_path.clone(),
81 _file: Some(file),
82 });
83 }
84 Err(err) => {
85 let tempfile::PersistError { file, .. } = err;
86 tmp = file;
87 }
88 }
89
90 if let Ok(file) = validate(&pref_path, file_hash) {
93 let _ = tmp.close();
95
96 return Ok(FileCheck {
97 path: pref_path.clone(),
98 _file: Some(file),
99 });
100 }
101
102 let path = tmp.path().to_owned();
106 let tmp = tmp.into_temp_path();
107
108 std::mem::forget(tmp);
112
113 let file = validate(&path, file_hash)?;
114
115 Ok(FileCheck {
116 path,
117 _file: Some(file),
118 })
119}
120
121fn validate(path: &std::path::PathBuf, hash: &str) -> Result<std::fs::File> {
123 use std::io::Read;
124
125 let mut file = std::fs::OpenOptions::new().read(true).open(path)?;
126
127 let mut data = Vec::new();
128 file.read_to_end(&mut data).expect("failed to read lib");
129
130 use sha2::Digest;
131 let mut hasher = sha2::Sha256::new();
132 hasher.update(data);
133
134 use base64::Engine;
135 let on_disk_hash = base64::engine::general_purpose::URL_SAFE_NO_PAD
136 .encode(hasher.finalize());
137
138 if on_disk_hash != hash {
139 return Err(Error::err(format!("FileCheckHashMiss({path:?})")));
140 }
141
142 let perms = file
143 .metadata()
144 .expect("failed to get lib metadata")
145 .permissions();
146
147 if !perms.readonly() {
148 return Err(Error::err(format!("FileCheckNotReadonly({path:?})")));
149 }
150
151 tracing::trace!("success correct file_check: {path:?}");
152
153 Ok(file)
154}
155
156fn write(
158 parent_dir: PathBuf,
159 file_data: &[u8],
160) -> Result<tempfile::NamedTempFile> {
161 use std::io::Write;
162
163 let mut tmp = tempfile::NamedTempFile::new_in(parent_dir)?;
164
165 tmp.as_file_mut().write_all(file_data)?;
166 tmp.as_file_mut().flush()?;
167
168 set_perms(tmp.as_file_mut())?;
169
170 Ok(tmp)
171}
172
173fn set_perms(file: &mut std::fs::File) -> Result<()> {
175 let mut perms = file.metadata()?.permissions();
176
177 perms.set_readonly(true);
178 #[cfg(unix)]
179 std::os::unix::fs::PermissionsExt::set_mode(&mut perms, 0o500);
180
181 file.set_permissions(perms)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::sync::Arc;
188
189 #[tokio::test(flavor = "multi_thread")]
190 async fn file_check_stress() {
191 use rand::Rng;
192 let mut data = vec![0; 1024 * 1024 * 10]; rand::rng().fill(&mut data[..]);
194 let data = Arc::new(data);
195
196 use sha2::Digest;
197 let mut hasher = sha2::Sha256::new();
198 hasher.update(&data[..]);
199
200 use base64::Engine;
201 let hash = base64::engine::general_purpose::URL_SAFE_NO_PAD
202 .encode(hasher.finalize());
203
204 let mut task_list = Vec::new();
205
206 const COUNT: usize = 3;
207
208 let barrier = Arc::new(std::sync::Barrier::new(COUNT));
209
210 for _ in 0..3 {
211 let data = data.clone();
212 let hash = hash.clone();
213 let barrier = barrier.clone();
214 task_list.push(tokio::task::spawn_blocking(move || {
215 barrier.wait();
216
217 file_check(
218 data.as_slice(),
219 &hash,
220 "tx5-core-file-check-test",
221 ".data",
222 )
223 }));
224 }
225
226 let mut tmp = Vec::new();
228 for task in task_list {
229 tmp.push(task.await.unwrap().unwrap());
230 }
231
232 for tmp in tmp {
234 let path = tmp.path().to_owned();
235 drop(tmp);
236 let _ = std::fs::remove_file(&path);
237 }
238 }
239
240 #[test]
241 fn file_check_env_variable_override() {
242 let _ = tempfile::tempdir().unwrap();
243 let tmpdir = tempfile::tempdir().unwrap();
244 let tmpdir_path = tmpdir.path();
245 let original_tx5_cache_directory = std::env::var("TX5_CACHE_DIRECTORY");
246 std::env::set_var("TX5_CACHE_DIRECTORY", tmpdir_path.as_os_str());
247
248 use rand::Rng;
249 let mut data = vec![0; 1024 * 1024 * 10]; rand::rng().fill(&mut data[..]);
251 let data = Arc::new(data);
252
253 use sha2::Digest;
254 let mut hasher = sha2::Sha256::new();
255 hasher.update(&data[..]);
256
257 use base64::Engine;
258 let hash = base64::engine::general_purpose::URL_SAFE_NO_PAD
259 .encode(hasher.finalize());
260
261 let data = data.clone();
262 let hash = hash.clone();
263
264 let res = file_check(
265 data.as_slice(),
266 &hash,
267 "tx5-core-file-check-test",
268 ".data",
269 )
270 .unwrap();
271
272 assert!(res.path.starts_with(tmpdir_path));
273
274 let path = res.path().to_owned();
276 match original_tx5_cache_directory {
277 Ok(dir) => std::env::set_var("TX5_CACHE_DIRECTORY", dir),
278 Err(_) => std::env::remove_var("TX5_CACHE_DIRECTORY"),
279 };
280 drop(res);
281 let _ = std::fs::remove_file(path);
282 }
283}