/home/runner/work/MathCAT/MathCAT/src/shim_filesystem.rs
Line | Count | Source |
1 | | #![allow(clippy::needless_return)] |
2 | | //! This is used to paste over normal reading of the Rules files and building them into the code for web assembly (WASM) which |
3 | | //! can't do file system access. For the latter, the Rules directory is zipped up. |
4 | | |
5 | | use std::path::{Path, PathBuf}; |
6 | | use crate::errors::*; |
7 | | use cfg_if::cfg_if; |
8 | | |
9 | | #[allow(unused_imports)] |
10 | | use log::{debug}; |
11 | | |
12 | | |
13 | | // The zipped files are needed by WASM builds. |
14 | | // However, they are also useful for other builds because there really isn't another good way to get at the rules. |
15 | | // Other build scripts can extract these files and unzip to their needed locations. |
16 | | // I'm not thrilled with this solution as it seems hacky, but I don't know another way for crates to allow for each access to data. |
17 | | cfg_if! { |
18 | | if #[cfg(any(target_family = "wasm", feature = "include-zip"))] { |
19 | | // For the include-zip builds, we build a fake file system based on ZIPPED_RULE_FILES. |
20 | | // That stream encodes other zip files that must be unzipped. |
21 | | // Only one level of embedded zip files is supported. |
22 | | use zip::ZipArchive; |
23 | | pub static ZIPPED_RULE_FILES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"),"/rules.zip")); |
24 | | |
25 | | /// Struct to indicate where a file is located in the zip archive(s) |
26 | | #[derive(Debug, Copy, Clone)] |
27 | | struct ArchivePath { |
28 | | main: usize, // index into ZIPPED_RULE_FILES |
29 | | zipped: Option<usize>, // if Some, index into embedded zip file, None if top-level zip file |
30 | | } |
31 | | |
32 | | use std::cell::RefCell; |
33 | | use std::io::Cursor; |
34 | | use std::io::Read; |
35 | | use std::collections::{HashMap, HashSet}; |
36 | | thread_local! { |
37 | | // mapping the file names to whether they are a directory or a file |
38 | | // Note: these are always stored with "/" as the path separator |
39 | | static DIRECTORIES: RefCell<HashSet<String>> = RefCell::new(HashSet::with_capacity(127)); |
40 | | // if a file, we note whether it is in ZIPPED_RULE_FILES or the index of a zipped file within ZIPPED_RULE_FILES |
41 | | static FILES: RefCell<HashMap<String, ArchivePath>> = RefCell::new(HashMap::with_capacity(1023)); |
42 | | } |
43 | | |
44 | | /// Canonicalize path separators to "/" |
45 | | fn canonicalize_path_separators(path: &Path) -> String { |
46 | | return path.to_str().unwrap_or_default().replace("\\", "/"); |
47 | | } |
48 | | |
49 | | /// Return a zip archive given the zip bytes |
50 | | fn get_zip_archive(zip_bytes: &[u8]) -> Result<ZipArchive<Cursor<&[u8]>>> { |
51 | | let buf_reader = Cursor::new(zip_bytes); |
52 | | let archive = match zip::ZipArchive::new(buf_reader) { |
53 | | Err(e) => bail!("get_zip_archive: failed to create ZipArchive: {}", e), |
54 | | Ok(archive) => archive, |
55 | | }; |
56 | | return Ok(archive); |
57 | | } |
58 | | |
59 | | /// Read ZIPPED_RULE_FILES and build up the FILES and DIRECTORIES static variables. |
60 | | /// This is called lazily when the first file or directory check is done. |
61 | | fn initialize_static_vars() -> Result<()> { |
62 | | let mut archive = get_zip_archive(ZIPPED_RULE_FILES)?; |
63 | | read_zip_file("", &mut archive, None)?; |
64 | | |
65 | | // Because of Rust's borrow checker, we can't recursively unzip contained zip files (FILES, etc., are borrowed mut) |
66 | | // Here we gather up the zip files that were found and iterate over them non-recursively. |
67 | | // Note: there shouldn't be embedded zip files in these files (if there are, they won't be unzipped) |
68 | | let zip_files = FILES.with(|files| files.borrow().iter() |
69 | | .filter_map(|(name, archive_path)| if name.ends_with(".zip") { Some((name.clone(), *archive_path)) } else { None } ) |
70 | | .collect::<Vec<_>>() |
71 | | ); |
72 | | // debug!("Found {:?} embedded zip files", zip_files); |
73 | | for (zip_file_name, archive_path) in zip_files.iter() { |
74 | | let bytes = get_bytes_from_index(&mut archive, archive_path.main)?; |
75 | | let mut inner_archive = get_zip_archive(bytes.as_slice())?; |
76 | | // debug!(" internal zip file {} has {} files", zip_file_name, inner_archive.len()); |
77 | | let new_containing_dir = zip_file_name.rsplit_once("/").map(|(before, _)| before).unwrap_or(""); |
78 | | read_zip_file(new_containing_dir, &mut inner_archive, Some(archive_path.main))?; |
79 | | } |
80 | | // FILES.with(|files| { |
81 | | // let files = files.borrow(); |
82 | | // debug!("{} files={:?}", files.len(), files); |
83 | | // }); |
84 | | return Ok(()); |
85 | | } |
86 | | |
87 | | /// Get the bytes for a file in the zip archive (intended for embedded zip files) |
88 | | fn get_bytes_from_index(archive: &mut ZipArchive<Cursor<&[u8]>>, index: usize) -> Result<Vec<u8>> { |
89 | | let mut file = archive.by_index(index) |
90 | | .map_err(|e| anyhow!(format!("Error getting index={} from zip archive: {}", index, e)) )?; |
91 | | let mut contents = Vec::new(); |
92 | | file.read_to_end(&mut contents) |
93 | | .map_err(|e| anyhow!(format!("Error reading index={} from zip archive: {}", index, e)) )?; |
94 | | return Ok(contents); |
95 | | } |
96 | | /// Unzip the zip file (given by zip_archive) and record the file and dir names |
97 | | /// 'containing_dir' is the rule dir (RulesDir or a subdir) and establishes a full path for unzipped file(s) |
98 | | /// embedded_zip_file is index into ZIPPED_RULE_FILES if this is an embedded zip file, None if it is the top-level zip file |
99 | | fn read_zip_file(containing_dir: &str, zip_archive: &mut ZipArchive<Cursor<&[u8]>>, embedded_zip_file: Option<usize>) -> Result<()> { |
100 | | // debug!("read_zip_file: containing_dir='{}', zip_archive.len()={}", containing_dir, zip_archive.len()); |
101 | | return FILES.with(|files| { |
102 | | let mut files = files.borrow_mut(); |
103 | | return DIRECTORIES.with(|dirs| { |
104 | | let mut dirs = dirs.borrow_mut(); |
105 | | for i in 0..zip_archive.len() { |
106 | | let file = zip_archive.by_index(i).unwrap(); |
107 | | // A little bit of safety/sanity checking |
108 | | let path = match file.enclosed_name() { |
109 | | Some(path) => PathBuf::from(containing_dir).join(path), |
110 | | None => { |
111 | | bail!("Entry {} has a suspicious path (outside of archive)", file.name()); |
112 | | } |
113 | | }; |
114 | | // debug!("read_zip_file: file path='{}'", path.display()); |
115 | | // add all the dirs up to the containing dir -- skip the first one as that is a file |
116 | | // for files like unicode.yaml, this loop is a no-op, but for files in the Shared folder, it will go one time. |
117 | | for parent in path.ancestors().skip(1) { |
118 | | if parent.to_str().unwrap_or_default() == containing_dir { |
119 | | break; |
120 | | } |
121 | | dirs.insert(canonicalize_path_separators(parent)); |
122 | | } |
123 | | let file_name = canonicalize_path_separators(&path); |
124 | | if file.is_file() { |
125 | | let archive_path = match embedded_zip_file { |
126 | | None => ArchivePath{ main: i, zipped: None }, |
127 | | Some(main) => ArchivePath{ main, zipped: Some(i) }, |
128 | | }; |
129 | | files.insert(file_name, archive_path); |
130 | | } else if file.is_dir() { |
131 | | dirs.insert(file_name); |
132 | | } else { |
133 | | bail!("read_zip_file: {} is neither a file nor a directory", path.display()); |
134 | | } |
135 | | }; |
136 | | // debug!("{} files={:?}", files.len(), files); |
137 | | // debug!("{} dirs={:?}", dirs.len(), dirs); |
138 | | return Ok::<(), Error>( () ); |
139 | | }); |
140 | | }); |
141 | | } |
142 | | |
143 | | pub fn is_file_shim(path: &Path) -> bool { |
144 | | if FILES.with(|files| files.borrow().is_empty()) { |
145 | | let _ignore_result = initialize_static_vars(); |
146 | | } |
147 | | return FILES.with(|files| files.borrow().contains_key(&canonicalize_path_separators(path)) ); |
148 | | } |
149 | | |
150 | | pub fn is_dir_shim(path: &Path) -> bool { |
151 | | if FILES.with(|files| files.borrow().is_empty()) { |
152 | | let _ignore_result = initialize_static_vars(); |
153 | | } |
154 | | return DIRECTORIES.with(|dirs| dirs.borrow().contains(&canonicalize_path_separators(path)) ); |
155 | | } |
156 | | |
157 | | /// Find files in 'dir' that end with 'ending' (e.g., "_Rules.yaml") |
158 | | pub fn find_files_in_dir_that_ends_with_shim(dir: &Path, ending: &str) -> Vec<String> { |
159 | | // FIX: this is very inefficient because it looks through all the files -- maybe dirs should list the files in them? |
160 | | // look for files that have 'path' as a prefix |
161 | | return FILES.with(|files| { |
162 | | let files = files.borrow(); |
163 | | let mut answer = Vec::new(); |
164 | | |
165 | | let dir_name = canonicalize_path_separators(dir); |
166 | | for file_name in files.keys() { |
167 | | if let Some(dir_relative_name) = file_name.strip_prefix(&dir_name) && |
168 | | file_name.ends_with(ending) |
169 | | { |
170 | | // this could be (e.g.) xxx_Rules.yaml or it could be subdir/xxx_Rules.yaml |
171 | | let file_name = dir_relative_name.split_once("/").map(|(_, after)| after).unwrap_or(dir_relative_name); |
172 | | answer.push(file_name.to_string()); |
173 | | } |
174 | | } |
175 | | // debug!("find_files_in_dir_that_ends_with_shim: in dir '{}' found {:?}", dir.display(), answer); |
176 | | return answer; |
177 | | }); |
178 | | } |
179 | | |
180 | | |
181 | | pub fn find_all_dirs_shim(dir: &Path, found_dirs: &mut Vec<PathBuf> ) { |
182 | | return DIRECTORIES.with(|dirs| { |
183 | | let dirs = dirs.borrow(); |
184 | | |
185 | | let common_dir_name = canonicalize_path_separators(dir); |
186 | | for dir_name in dirs.iter() { |
187 | | if dir_name.starts_with(&common_dir_name) && !dir_name.contains("SharedRules") { |
188 | | found_dirs.push(PathBuf::from(&dir_name)); |
189 | | }; |
190 | | } |
191 | | }); |
192 | | } |
193 | | |
194 | | |
195 | | pub fn canonicalize_shim(path: &Path) -> std::io::Result<PathBuf> { |
196 | | use std::ffi::OsStr; |
197 | | let dot_dot = OsStr::new(".."); |
198 | | let mut result = PathBuf::new(); |
199 | | for part in path.iter() { |
200 | | if dot_dot == part { |
201 | | result.pop(); |
202 | | } else { |
203 | | result.push(part); |
204 | | } |
205 | | } |
206 | | return Ok(result); |
207 | | } |
208 | | |
209 | | /// Read the file at 'path' and return its contents as a String |
210 | | pub fn read_to_string_shim(path: &Path) -> Result<String> { |
211 | | let path = canonicalize_shim(path).unwrap(); // can't fail |
212 | | let file_name = canonicalize_path_separators(&path); |
213 | | // Is this the debugging override? |
214 | | if let Some(contents) = OVERRIDE_FILE_NAME.with(|override_name| { |
215 | | if file_name == override_name.borrow().as_str() { |
216 | | // debug!("override read_to_string_shim: {}",file_name); |
217 | | return OVERRIDE_FILE_CONTENTS.with(|contents| return Some(contents.borrow().clone())); |
218 | | } else { |
219 | | return None; |
220 | | } |
221 | | }) { |
222 | | return Ok(contents); |
223 | | }; |
224 | | |
225 | | let file_name = file_name.replace('\\', "/"); // zip files always use forward slash |
226 | | // top-level zip file or embedded zip file |
227 | | return FILES.with(|files| { |
228 | | let files = files.borrow(); |
229 | | let inner_bytes; |
230 | | let (bytes, index) = match files.get(&file_name) { |
231 | | Some(archive_path) => { |
232 | | match &archive_path.zipped { |
233 | | None => (ZIPPED_RULE_FILES, archive_path.main), |
234 | | Some(i) => { |
235 | | // debug!("read_to_string_shim: reading embedded zip file {} at index {}", file_name, *i); |
236 | | let mut archive = get_zip_archive(ZIPPED_RULE_FILES)?; |
237 | | inner_bytes = get_bytes_from_index(&mut archive, archive_path.main)?; // need to hold temp value |
238 | | (inner_bytes.as_slice(), *i) |
239 | | } |
240 | | } |
241 | | }, |
242 | | None => bail!("read_to_string_shim: didn't find {} in zip archive", file_name), |
243 | | }; |
244 | | let mut archive = get_zip_archive(bytes)?; |
245 | | let mut file = match archive.by_index(index) { |
246 | | Ok(file) => { |
247 | | // debug!("read_to_string_shim: want {}; name of zipped file={:?}", file_name, file.enclosed_name().unwrap()); |
248 | | file |
249 | | }, |
250 | | Err(..) => { |
251 | | bail!("Didn't find {} in zip archive", file_name); |
252 | | } |
253 | | }; |
254 | | |
255 | | let mut contents = String::new(); |
256 | | if let Err(e) = file.read_to_string(&mut contents) { |
257 | | bail!("read_to_string: {}", e); |
258 | | } |
259 | | return Ok(contents); |
260 | | }); |
261 | | } |
262 | | |
263 | | pub fn zip_extract_shim(dir: &Path, zip_file_name: &str) -> Result<bool> { |
264 | | let zip_file_path = dir.join(zip_file_name); |
265 | | let full_zip_file_name = canonicalize_path_separators(&zip_file_path); |
266 | | match FILES.with(|files| files.borrow().contains_key(full_zip_file_name.as_str()) ) { |
267 | | true => Ok(true), |
268 | | false => bail!("zip_extract_shim: didn't find {} in zip archive", full_zip_file_name), |
269 | | } |
270 | | } |
271 | | |
272 | | thread_local! { |
273 | | // For debugging rules files (mainly nav file) via MathCATDemo |
274 | | static OVERRIDE_FILE_NAME: RefCell<String> = RefCell::new("".to_string()); |
275 | | static OVERRIDE_FILE_CONTENTS: RefCell<String> = RefCell::new("".to_string()); |
276 | | } |
277 | | pub fn override_file_for_debugging_rules(file_name: &str, file_contents: &str) { |
278 | | // file_name should be path name starting at Rules dir: e.g, "Rules/en/navigate.yaml" |
279 | | OVERRIDE_FILE_NAME.with(|name| *name.borrow_mut() = file_name.to_string().replace("/", "\\")); |
280 | | OVERRIDE_FILE_CONTENTS.with(|contents| *contents.borrow_mut() = file_contents.to_string()); |
281 | | crate::interface::set_rules_dir("Rules").unwrap(); // force reinitialization after the change |
282 | | } |
283 | | } else { |
284 | 116k | pub fn is_file_shim(path: &Path) -> bool { |
285 | 116k | return path.is_file(); |
286 | 116k | } |
287 | | |
288 | 135k | pub fn is_dir_shim(path: &Path) -> bool { |
289 | 135k | return path.is_dir(); |
290 | 135k | } |
291 | | |
292 | 12.9k | pub fn find_files_in_dir_that_ends_with_shim(dir: &Path, ending: &str) -> Vec<String> { |
293 | 12.9k | match dir.read_dir() { |
294 | 0 | Err(_) => return vec![], // empty |
295 | 12.9k | Ok(read_dir) => { |
296 | 12.9k | let mut answer = Vec::new(); |
297 | 78.5k | for dir_entry in read_dir12.9k .flatten12.9k () { |
298 | 78.5k | let file_name = dir_entry.file_name(); |
299 | 78.5k | let file_name = file_name.to_string_lossy().to_string(); |
300 | 78.5k | if file_name.ends_with(ending) { |
301 | | // this could be (e.g.) xxx_Rules.yaml or it could be subdir/xxx_Rules.yaml |
302 | 64.4k | let file_name = file_name.split_once(std::path::MAIN_SEPARATOR).map(|(_, after)| after).unwrap_or(&file_name); |
303 | 64.4k | answer.push( file_name.to_string() ); |
304 | 14.0k | } |
305 | | } |
306 | 12.9k | return answer; |
307 | | } |
308 | | } |
309 | 12.9k | } |
310 | | |
311 | 2.90k | pub fn find_all_dirs_shim(dir: &Path, found_dirs: &mut Vec<PathBuf> ) { |
312 | | // FIX: this doesn't work for subdirectories that haven't been unzipped yet |
313 | 2.90k | assert!(dir.is_dir(), "find_all_dirs_shim called with non-directory path: {}", dir0 .display0 ()); |
314 | 2.90k | let mut found_rules_file = false; |
315 | 2.90k | if let Ok(entries) = std::fs::read_dir(dir) { |
316 | 13.0k | for entry in entries2.90k .flatten2.90k () { |
317 | 13.0k | let path = entry.path(); |
318 | 13.0k | if path.is_dir() { |
319 | | // skip "SharedRules" directory |
320 | 2.90k | if let Some(dir_name) = path.file_name() && |
321 | 2.90k | dir_name.to_str().unwrap_or_default() != "SharedRules" { |
322 | 1.45k | find_all_dirs_shim(&path, found_dirs); |
323 | 1.45k | }1.44k |
324 | | } else { |
325 | 10.1k | let file_name = path.file_name().unwrap_or_default().to_str().unwrap_or_default(); |
326 | 10.1k | if !found_rules_file && |
327 | 1.46k | (file_name.starts_with("unicode") || file_name1.45k .starts_with1.45k ("definitions") || file_name0 .ends_with0 ("_Rules.yaml") || file_name0 .ends_with0 (".zip")) { |
328 | 1.46k | found_dirs.push(path.parent().unwrap().to_path_buf()); |
329 | | // FIX: hack to get around not unzipping files and having zh/tw not found |
330 | 1.46k | if file_name == "zh.zip" { |
331 | 0 | let tw_dir = path.parent().unwrap().join("tw"); |
332 | 0 | if !found_dirs.contains(&tw_dir) { |
333 | 0 | found_dirs.push(tw_dir.to_path_buf()); |
334 | 0 | } |
335 | 1.46k | } |
336 | 1.46k | found_rules_file = true; |
337 | 8.71k | } |
338 | | } |
339 | | } |
340 | 0 | } |
341 | 2.90k | } |
342 | | |
343 | 73.2k | pub fn canonicalize_shim(path: &Path) -> std::io::Result<PathBuf> { |
344 | 73.2k | return path.canonicalize(); |
345 | 73.2k | } |
346 | | |
347 | 60.6k | pub fn read_to_string_shim(path: &Path) -> Result<String> { |
348 | 60.6k | let path = match path.canonicalize() { |
349 | 60.6k | Ok(path) => path, |
350 | 0 | Err(e) => bail!("Read error while trying to canonicalize in read_to_string_shim {}: {}", path.display(), e), |
351 | | }; |
352 | 60.6k | debug!("Reading file '{}'", &path.display()0 ); |
353 | 60.6k | match std::fs::read_to_string(&path) { |
354 | 60.6k | Ok(str) => return Ok(str), |
355 | 0 | Err(e) => bail!("Read error while trying to read {}: {}", &path.display(), e), |
356 | | } |
357 | 60.6k | } |
358 | | |
359 | 12.6k | pub fn zip_extract_shim(dir: &Path, zip_file_name: &str) -> Result<bool> { |
360 | 12.6k | let zip_file = dir.join(zip_file_name); |
361 | 12.6k | return match std::fs::read(zip_file) { |
362 | 12.6k | Err(e) => { |
363 | | // no zip file? -- maybe started out with all the files unzipped? See if there is a .yaml file |
364 | 12.6k | let yaml_files = find_files_in_dir_that_ends_with_shim(dir, ".yaml"); |
365 | 12.6k | if yaml_files.is_empty() { |
366 | 1.44k | bail!("{}", e) |
367 | | } else { |
368 | 11.2k | Ok(false) |
369 | | } |
370 | | }, |
371 | 0 | Ok(contents) => { |
372 | 0 | let archive = std::io::Cursor::new(contents); |
373 | 0 | let mut zip_archive = zip::ZipArchive::new(archive).unwrap(); |
374 | 0 | zip_archive.extract(dir).expect("Zip extraction failed"); |
375 | 0 | Ok(true) |
376 | | }, |
377 | | }; |
378 | 12.6k | } |
379 | | } |
380 | | } |