## Coverage Report

#### Created: 2026-05-25 08:50

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /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
    }