tx5_core/
file_check.rs

1//! A couple crates that depend on tx5-core need to be able to write/verify
2//! files on system. Enable this `file_check` feature to provide that ability.
3
4use crate::{Error, Result};
5use std::path::PathBuf;
6
7/// A handle to a verified system file. Keep this instance in memory as
8/// long as you intend to keep using the validated file.
9pub struct FileCheck {
10    path: std::path::PathBuf,
11    _file: Option<std::fs::File>,
12}
13
14impl FileCheck {
15    /// Get the path of the validated FileCheck file.
16    pub fn path(&self) -> &std::path::Path {
17        &self.path
18    }
19}
20
21/// Get a path to a cache directory where the dependency Pion library can be written
22///
23/// Defaults to the UserCache directory returned by app_dirs2, specific to the target platform.
24/// Overridable via the env variable TX5_CACHE_DIRECTORY
25fn 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
46/// Write a temp file if needed, verify the file, and return a handle to that file.
47pub 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    // NOTE: This is NOT atomic, nor secure, but being able to validate the
69    //       file hash post-op mitigates this a bit. And we can let the os
70    //       clean up a dangling tmp file if it failed to unlink.
71    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    // before we go on to just using the tmp file,
91    // check to see if a different process wrote correctly
92    if let Ok(file) = validate(&pref_path, file_hash) {
93        // we no longer need the tmp file, clean it up
94        let _ = tmp.close();
95
96        return Ok(FileCheck {
97            path: pref_path.clone(),
98            _file: Some(file),
99        });
100    }
101
102    // we're just going to use the tmp file, do what we need to
103    // do to make sure it isn't deleted when the handle drops.
104
105    let path = tmp.path().to_owned();
106    let tmp = tmp.into_temp_path();
107
108    // This seems wrong, but it is how tempfile internally goes
109    // about doing persist/keep, so we're using it already,
110    // and it's only once-ish per process...
111    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
121/// Validate a file.
122fn 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
156/// Write a temp file in the given directory
157fn 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
173/// Set file permissions.
174fn 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]; // 10 MiB
193        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        // make sure they're not dropped until the test is over
227        let mut tmp = Vec::new();
228        for task in task_list {
229            tmp.push(task.await.unwrap().unwrap());
230        }
231
232        // cleanup
233        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]; // 10 MiB
250        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        // cleanup
275        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}