libmathcat/
shim_filesystem.rs

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 should be zipped up.
4//! 
5//! Note: if files are added or removed, the directory structure needs to be reflected here. This could be automated,
6//! but changes are pretty rare and it didn't seem worth it (this may need to be revisited).
7
8use std::path::{Path, PathBuf};
9use crate::errors::*;
10
11
12// The zipped files are needed by WASM builds.
13// However, they are also useful for other builds because there really isn't another good way to get at the rules.
14// Other build scripts can extract these files and unzip to their needed locations.
15// 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.
16#[cfg(feature = "include-zip")]
17pub static ZIPPED_RULE_FILES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"),"/rules.zip"));
18
19
20cfg_if! {
21    if #[cfg(any(target_family = "wasm", feature = "include-zip"))] {
22        // For the WASM build, we build a fake file system based on ZIPPED_RULE_FILES.
23        // That stream encodes other zip files that must be unzipped.
24
25        // We have a problem in that ZIPPED_RULE_FILES has a static lifetime but the contained zip files, when unzipped, are on the stack with a different lifetime.
26        // One solution would be to introduce an enum that forks between the two.
27        // The slightly hacky but slightly less code solution that is adopted is to use Option<>, with None representing the static case
28        // Note: Rc is used because there are borrowing/lifetime issues without being able to clone the data that goes into the HashMap
29        use std::cell::RefCell;
30        use std::rc::Rc;
31        use std::io::Cursor;
32        use std::io::Read;
33        use std::collections::{HashSet, HashMap};
34
35        #[derive(Debug)]
36        struct FilesEntry {
37            data: Rc<Option<Vec<u8>>>,
38            index: usize,
39        }
40        thread_local! {
41            // mapping the file names to whether they are are directory or a file (if a file, where to find it in the zip archive)
42            static DIRECTORIES: RefCell<HashSet<String>> = RefCell::new(HashSet::with_capacity(63));
43            static FILES: RefCell<HashMap< String, FilesEntry >> = RefCell::new(HashMap::with_capacity(1023));
44        }
45        
46        fn read_zip_file(containing_dir: &Path, zip_file: Option<Vec<u8>>) -> Result<()> {
47            // Return "file" or "dir" if a match, otherwise None
48            let zip_file = Rc::new(zip_file);
49            FILES.with(|files| {
50                let mut files = files.borrow_mut();
51                DIRECTORIES.with(|dirs| {
52                    let mut dirs = dirs.borrow_mut();
53                    let mut archive = match zip_file.as_ref() {
54                        None => {
55                            let buf_reader = Cursor::new(ZIPPED_RULE_FILES);
56                            zip::ZipArchive::new(buf_reader).unwrap()
57                        },
58                        Some(zip_file) => {
59                            let buf_reader = Cursor::new((zip_file).as_ref());
60                            match zip::ZipArchive::new(buf_reader) {
61                                Err(e) => bail!("read_zip_file: failed to create ZipArchive in dir {}: {}", containing_dir.display(), e),
62                                Ok(archive) => archive,
63                            }
64                        }
65                    };
66                    for i in 0..archive.len() {
67                        let file = archive.by_index(i).unwrap();
68                        // A little bit of safety/sanity checking
69                        let path = match file.enclosed_name() {
70                            Some(path) => containing_dir.to_path_buf().join(path),
71                            None => {
72                                bail!("Entry {} has a suspicious path (outside of archive)", file.name());
73                            }
74                        };
75                        // debug!("read_zip_file: file path='{}'", path.display());
76                        // add all the dirs up to the containing dir -- skip the first one as that is a file
77                        // for files like unicode.yaml, this loop is a no-op, but for files in the Shared folder, it will go one time.
78                        for parent in path.ancestors().skip(1) {
79                            if parent == containing_dir {
80                                break;
81                            }
82                            dirs.insert(parent.to_str().unwrap_or_default().replace("/", std::path::MAIN_SEPARATOR_STR));
83                        }
84                        if file.is_file() {
85                            files.insert(path.to_str().unwrap_or_default().replace("/", std::path::MAIN_SEPARATOR_STR), FilesEntry{ data: zip_file.clone(), index: i});
86                        } else if file.is_dir() {
87                            dirs.insert(path.to_str().unwrap_or_default().replace("/", std::path::MAIN_SEPARATOR_STR));
88                        } else {
89                            bail!("read_zip_file: {} is neither a file nor a directory", path.display());
90                        }
91                    };
92                    // debug!("files={:?}", files.keys());
93                    // debug!("dirs={:?}", dirs);
94                    return Ok( () );
95                })
96            })
97        }
98        
99        pub fn is_file_shim(path: &Path) -> bool {
100            if FILES.with(|files| files.borrow().is_empty()) {
101                let empty_path = PathBuf::new();
102                read_zip_file(&empty_path, None).unwrap_or(());
103            }
104            return FILES.with(|files| files.borrow().contains_key(path.to_str().unwrap_or_default()) );
105        }
106        
107        pub fn is_dir_shim(path: &Path) -> bool {
108            if FILES.with(|files| files.borrow().is_empty()) {
109                let empty_path = PathBuf::new();
110                read_zip_file(&empty_path, None).unwrap_or(());
111            }
112            return DIRECTORIES.with(|dirs| dirs.borrow().contains(path.to_str().unwrap_or_default()) );
113        }
114
115        pub fn find_files_in_dir_that_ends_with_shim(dir: &Path, ending: &str) -> Vec<String> {
116            // FIX: this is very inefficient -- maybe gather up all the info in read_zip_file()?
117            // look for files that have 'path' as a prefix
118            return FILES.with(|files| {
119                let files = files.borrow();
120                let mut answer = Vec::new();
121
122                let dir_name = dir.to_str().unwrap_or_default();
123                for file_name in files.keys() {
124                    if let Some(dir_relative_name) = file_name.strip_prefix(dir_name) {
125                        if file_name.ends_with(ending) {
126                            // this could be (e.g.) xxx_Rules.yaml or it could be subdir/xxx_Rules.yaml
127                            let file_name = dir_relative_name.split_once(std::path::MAIN_SEPARATOR).map(|(_, after)| after).unwrap_or(dir_relative_name);
128                            answer.push( file_name.to_string() );
129                        }
130                    }
131                }
132                return answer;
133            });
134        }
135        
136        pub fn find_all_dirs_shim(dir: &Path, found_dirs: &mut Vec<PathBuf> ) {
137            return DIRECTORIES.with(|dirs| {
138                let dirs = dirs.borrow();
139
140                let common_dir_name = dir.to_str().unwrap_or_default();
141                for dir_name in dirs.iter() {
142                    if dir_name.starts_with(common_dir_name) && !dir_name.contains("SharedRules") {
143                        found_dirs.push(PathBuf::from(dir_name));
144                    };
145                }
146            });
147        }
148
149        
150        pub fn canonicalize_shim(path: &Path) -> std::io::Result<PathBuf> {
151            use std::ffi::OsStr;
152            let dot_dot = OsStr::new("..");
153            let mut result = PathBuf::new();
154            for part in path.iter() {
155                if dot_dot == part {
156                    result.pop();
157                } else {
158                    result.push(part);
159                }
160            }
161            return Ok(result);
162        }
163        
164        pub fn read_to_string_shim(path: &Path) -> Result<String> {
165            let path = canonicalize_shim(path).unwrap();        // can't fail
166            let file_name = path.to_str().unwrap_or_default();
167            // Is this the debugging override?
168            if let Some(contents) = OVERRIDE_FILE_NAME.with(|override_name| {
169                if file_name == override_name.borrow().as_str() {
170                    // debug!("override read_to_string_shim: {}",file_name);
171                    return OVERRIDE_FILE_CONTENTS.with(|contents| return Some(contents.borrow().clone()));
172                } else {
173                    return None;
174                }
175            }) {
176                return Ok(contents);
177            };
178
179            // debug!("read_to_string_shim: {}",file_name);
180
181            return FILES.with(|files| {
182                let files = files.borrow();
183                let zip_file = match files.get(file_name) {
184                    None => bail!("Didn't find file '{}'", file_name),
185                    Some(data) => data,
186                };
187                let mut archive = match zip_file.data.as_ref() {
188                    None => {
189                        let buf_reader = Cursor::new(ZIPPED_RULE_FILES);
190                        zip::ZipArchive::new(buf_reader).unwrap()
191                    },
192                    Some(zip_file) => {
193                        let buf_reader = Cursor::new((zip_file).as_ref());
194                        zip::ZipArchive::new(buf_reader).unwrap()
195                    }
196                };
197                // for name in archive.file_names() {
198                //     debug!(" File: {}", name);
199                // };
200                let mut file = match archive.by_index(zip_file.index) {
201                    Ok(file) => file,
202                    Err(..) => {
203                        panic!("Didn't find {} in zip archive", file_name);
204                    }
205                };
206    
207                let mut contents = String::new();
208                if let Err(e) = file.read_to_string(&mut contents) {
209                    bail!("read_to_string: {}", e);
210                }
211                return Ok(contents);
212            });
213        }
214
215        pub fn zip_extract_shim(dir: &Path, zip_file_name: &str) -> Result<bool> {
216            let zip_file_path = dir.join(zip_file_name);
217            let full_zip_file_name = zip_file_path.to_str().unwrap_or_default().replace(std::path::MAIN_SEPARATOR_STR, "/");
218
219            // first, extract full_zip_file_name from ZIPPED_RULE_FILES
220            let buf_reader = Cursor::new(ZIPPED_RULE_FILES);
221            let mut archive = zip::ZipArchive::new(buf_reader).unwrap();
222            let mut file = match archive.by_name(&full_zip_file_name) {
223                Ok(file) => file,
224                Err(..) => {
225                    bail!("Didn't find {} in dir {} in zip archive", zip_file_name, dir.display());
226                }
227            };
228
229            // now add them to FILES
230            let mut zip_file_bytes: Vec<u8> = Vec::with_capacity(file.size() as usize);
231            if let Err(e) = file.read_to_end(&mut zip_file_bytes) {
232                bail!("Failed to extract file {} (size={}): {}", zip_file_path.display(), file.size(), e);
233            }
234            read_zip_file(dir, Some(zip_file_bytes))?;
235            return Ok(true);
236        }
237
238        thread_local! {
239            // For debugging rules files (mainly nav file)
240            static OVERRIDE_FILE_NAME: RefCell<String> = RefCell::new("".to_string());
241            static OVERRIDE_FILE_CONTENTS: RefCell<String> = RefCell::new("".to_string());
242        }
243        pub fn override_file_for_debugging_rules(file_name: &str, file_contents: &str) {
244            // file_name should be path name starting at Rules dir: e.g, "Rules/en/navigate.yaml"
245            OVERRIDE_FILE_NAME.with(|name| *name.borrow_mut() = file_name.to_string().replace("/", "\\"));
246            OVERRIDE_FILE_CONTENTS.with(|contents| *contents.borrow_mut() = file_contents.to_string());
247            crate::interface::set_rules_dir("Rules".to_string()).unwrap();       // force reinitialization after the change
248        }
249    } else {
250        pub fn is_file_shim(path: &Path) -> bool {
251            return path.is_file();
252        }
253        
254        pub fn is_dir_shim(path: &Path) -> bool {
255            return path.is_dir();
256        }
257        
258        pub fn find_files_in_dir_that_ends_with_shim(dir: &Path, ending: &str) ->  Vec<String> {
259            match dir.read_dir() {
260                Err(_) => return vec![],    // empty
261                Ok(read_dir) => {
262                    let mut answer = Vec::new();
263                    for dir_entry in read_dir.flatten() {
264                        let file_name = dir_entry.file_name();
265                        let file_name = file_name.to_string_lossy().to_string();
266                        if file_name.ends_with(ending) {
267                            // this could be (e.g.) xxx_Rules.yaml or it could be subdir/xxx_Rules.yaml
268                            let file_name = file_name.split_once(std::path::MAIN_SEPARATOR).map(|(_, after)| after).unwrap_or(&file_name);
269                            answer.push( file_name.to_string() );
270                        }
271                    }
272                    return answer;
273                }
274            }
275        }
276
277        pub fn find_all_dirs_shim(dir: &Path, found_dirs: &mut Vec<PathBuf> ) {
278            // FIX: this doesn't work for subdirectories that haven't been unzipped yet
279            assert!(dir.is_dir(), "find_all_dirs_shim called with non-directory path: {}", dir.display());
280            let mut found_rules_file = false;
281            if let Ok(entries) = std::fs::read_dir(dir) {
282                for entry in entries.flatten() {
283                    let path = entry.path();
284                    if path.is_dir() {
285                        // skip "SharedRules" directory
286                        if let Some(dir_name) = path.file_name() {
287                            if dir_name.to_str().unwrap_or_default() != "SharedRules" {
288                                find_all_dirs_shim(&path, found_dirs);
289                            }
290                        }
291                    } else {
292                        let file_name = path.file_name().unwrap_or_default().to_str().unwrap_or_default();
293                        if !found_rules_file && (file_name.starts_with("unicode") || file_name.ends_with("_Rules.yaml") || file_name.ends_with(".zip")) {
294                            found_dirs.push(path.parent().unwrap().to_path_buf());
295                            found_rules_file = true;
296                        }
297                    }
298                }
299            }
300        }
301        
302        pub fn canonicalize_shim(path: &Path) -> std::io::Result<PathBuf> {
303            return path.canonicalize();
304        }
305        
306        pub fn read_to_string_shim(path: &Path) -> Result<String> {
307            let path = match path.canonicalize() {
308                Ok(path) => path,
309                Err(e) => bail!("Read error while trying to canonicalize in read_to_string_shim {}: {}", path.display(), e),
310            };
311            info!("Reading file '{}'", &path.display());
312            match std::fs::read_to_string(&path) {
313                Ok(str) => return Ok(str),
314                Err(e) => bail!("Read error while trying to read {}: {}", &path.display(), e),
315            }
316        }
317
318        pub fn zip_extract_shim(dir: &Path, zip_file_name: &str) -> Result<bool> {
319            let zip_file = dir.join(zip_file_name);
320            return match std::fs::read(zip_file) {
321                Err(e) => {
322                    // no zip file? -- maybe started out with all the files unzipped? See if there is a .yaml file
323                    let yaml_files = find_files_in_dir_that_ends_with_shim(dir, ".yaml");
324                    if yaml_files.is_empty() {
325                        bail!("{}", e)
326                    } else {
327                        Ok(false)
328                    }
329                },
330                Ok(contents) => {
331                    let archive = std::io::Cursor::new(contents);
332                    let mut zip_archive = zip::ZipArchive::new(archive).unwrap();
333                    zip_archive.extract(dir).expect("Zip extraction failed");
334                    Ok(true)
335                },
336            };
337        }
338    }
339}