1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
pub use ini::Ini;
use std::env::var;
use std::fs;
use std::path::{Component, Path, PathBuf};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("Error parsing openmw.cfg: {0}")]
    IniError(ini::Error),
    #[error("FileNotFound: {0}")]
    FileNotFound(PathBuf),
    #[error("ci_exists cannot take an aboslute path as an argument")]
    AbsolutePathError,
    #[error("Encoding was missing from openmw.cfg")]
    MissingEncoding,
    #[error("IOError: {0}")]
    IOError(std::io::Error),
}

impl From<ini::Error> for Error {
    fn from(error: ini::Error) -> Self {
        Self::IniError(error)
    }
}

impl From<std::io::Error> for Error {
    fn from(error: std::io::Error) -> Self {
        Self::IOError(error)
    }
}

/// Returns the path of openmw.cfg
///
/// Defaults:
/// - Linux: ~/.config/openmw/openmw.cfg
/// - macOS: ~/Library/Preferences/openmw/openmw.cfg
/// - Windows: %Documents%/My Games/openmw/openmw.cfg
///
/// The OPENMW_CONFIG environment variable can be used to
/// override this location.
pub fn config_path() -> PathBuf {
    // Allow OPENMW_CONFIG env var to override default
    if let Ok(path) = var("OPENMW_CONFIG") {
        path.into()
    } else {
        cfg_if! {
            if #[cfg(target_os = "windows")] {
                dirs::document_dir().unwrap().join("My Games").join("openmw").join("openmw.cfg")
            } else {
                dirs::config_dir().unwrap().join("openmw").join("openmw.cfg")
            }
        }
    }
}

/// Returns an Ini object for openmw.cfg
///
/// The result is intended to be passed to other functions,
/// rather than used directly.
pub fn get_config() -> Result<Ini, Error> {
    let path = config_path();
    let conf = Ini::load_from_file_noescape(path)?;
    Ok(conf)
}

/// Returns the data directories listed in openmw.cfg
pub fn get_data_dirs(conf: &Ini) -> Result<Vec<String>, Error> {
    let section = conf
        .section::<String>(None)
        .expect("Default section is not present!");

    Ok(section.get_all("data").map(|x| x.to_string()).collect())
}

/// Returns the absolute paths of the plugins listed in openmw.cfg
pub fn get_plugins(conf: &Ini) -> Result<Vec<PathBuf>, Error> {
    let section = conf
        .section::<String>(None)
        .expect("Default section is not present!");

    let mut paths = vec![];
    for plugin_name in section.get_all("content") {
        let plugin_path = find_file(&conf, plugin_name)?;
        paths.push(plugin_path);
    }
    Ok(paths)
}

/// Case sensitive existence function.
/// Returns the (case-insensitive) path of the file if it exists.
pub fn ci_exists(root: &Path, path: &Path) -> Result<PathBuf, Error> {
    let mut partial_path = root.to_path_buf().canonicalize()?;

    for component in path.components() {
        match component {
            Component::Normal(component) => {
                let mut direntries = fs::read_dir(&partial_path)?;
                let result = direntries.find(|entry| {
                    if let Ok(entry) = entry {
                        if entry.file_name().to_str().unwrap().to_lowercase()
                            == component.to_str().unwrap().to_lowercase()
                        {
                            partial_path = partial_path.join(entry.file_name());
                            true
                        } else {
                            false
                        }
                    } else {
                        false
                    }
                });
                if result.is_none() {
                    return Err(Error::FileNotFound(partial_path));
                }
            }
            Component::CurDir => (),
            Component::ParentDir => {
                partial_path.pop();
            }
            Component::RootDir | Component::Prefix(_) => return Err(Error::AbsolutePathError),
        }
    }

    if partial_path.exists() {
        Ok(partial_path)
    } else {
        Err(Error::FileNotFound(root.join(path)))
    }
}

/// Searches the openmw vfs for the given file
/// Returns the absolute path of the file within the VFS
pub fn find_file(ini: &Ini, filename: &str) -> Result<PathBuf, Error> {
    let dirs = get_data_dirs(ini)?;
    let dirs = dirs.into_iter().rev();
    for dir in dirs {
        if let Ok(path) = ci_exists(Path::new(&dir), Path::new(filename)) {
            return Ok(path);
        }
    }

    Err(Error::FileNotFound(Path::new(filename).to_path_buf()))
}

/// Fetches encoding from openmw.cfg
pub fn get_encoding() -> Result<String, Error> {
    let path = config_path();
    let conf = Ini::load_from_file_noescape(path)?;
    let section = conf
        .section::<String>(None)
        .expect("Default section is not present!");

    section
        .get("encoding")
        .map(|x| x.to_string())
        .ok_or(Error::MissingEncoding)
}