Skip to main content

dream_ini/
lib.rs

1// SPDX-License-Identifier: GPL-3.0-only
2
3//! Library support for importing Morrowind INI settings into OpenMW-style configuration data.
4//!
5//! The crate exposes the same core importer used by the `dream-ini` CLI. Configuration data is
6//! represented as a multimap (`key -> Vec<value>`) so duplicate cfg keys such as `data`, `content`,
7//! and `fallback` are preserved without special cases.
8//! Path values exposed through cfg text, Lua tables, and import events are UTF-8 strings;
9//! non-UTF-8 operating-system paths are outside the supported API contract.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use std::path::Path;
15//!
16//! use dream_ini::{ImportOptions, IniImporter};
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let importer = IniImporter::new(ImportOptions::default());
20//! let result = importer.import_optional_cfg_path(
21//!     Path::new("Morrowind.ini"),
22//!     Some(Path::new("openmw.cfg")),
23//! )?;
24//!
25//! for warning in &result.warnings {
26//!     eprintln!("Warning: {warning}");
27//! }
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! Enable the `lua` feature to expose an embedding-oriented Lua API via [`lua::create_module`].
33
34use std::collections::BTreeMap;
35use std::fmt;
36use std::io;
37use std::path::PathBuf;
38
39use encoding_rs::{Encoding, WINDOWS_1250, WINDOWS_1251, WINDOWS_1252};
40
41mod content_files;
42mod events;
43mod fallback_keys;
44mod importer;
45#[cfg(feature = "lua")]
46pub mod lua;
47mod openmw_cfg;
48mod parser;
49mod plugin;
50#[cfg(test)]
51mod test_support;
52mod warnings;
53
54pub use events::ImportEvent;
55pub use importer::{ImportOptions, ImportReport, ImportResult, IniImporter};
56pub use openmw_cfg::{
57    PreservedCfgUpdate, apply_preserved_cfg_update, load_cfg_document, save_cfg_output_to_path,
58    save_preserved_cfg_document_to_path, save_resolved_cfg_to_path,
59    save_resolved_configuration_to_path, serialize_cfg_output, serialize_preserved_cfg_document,
60    serialize_resolved_cfg, serialize_resolved_configuration,
61};
62pub use parser::{
63    ParsedIni, parse_cfg_str, parse_ini_bytes, parse_ini_bytes_with_warnings, parse_ini_str,
64    parse_ini_str_with_warnings, serialize_cfg,
65};
66pub use plugin::{PluginHeader, read_plugin_header};
67pub use warnings::ImportWarning;
68
69pub type MultiMap = BTreeMap<String, Vec<String>>;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum Game {
73    Morrowind,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum PluginFormat {
78    Tes3,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum TextEncoding {
83    Win1250,
84    Win1251,
85    Win1252,
86}
87
88impl TextEncoding {
89    /// Parses an `OpenMW` encoding label.
90    ///
91    /// # Errors
92    /// Returns [`ImportError::UnsupportedEncoding`] if `value` is not supported.
93    pub fn parse(value: &str) -> Result<Self, ImportError> {
94        match value.to_ascii_lowercase().as_str() {
95            "win1250" | "windows-1250" => Ok(Self::Win1250),
96            "win1251" | "windows-1251" => Ok(Self::Win1251),
97            "win1252" | "windows-1252" => Ok(Self::Win1252),
98            _ => Err(ImportError::UnsupportedEncoding(value.to_owned())),
99        }
100    }
101
102    pub(crate) fn as_label(self) -> &'static str {
103        match self {
104            Self::Win1250 => "win1250",
105            Self::Win1251 => "win1251",
106            Self::Win1252 => "win1252",
107        }
108    }
109
110    pub(crate) fn encoding_rs(self) -> &'static Encoding {
111        match self {
112            Self::Win1250 => WINDOWS_1250,
113            Self::Win1251 => WINDOWS_1251,
114            Self::Win1252 => WINDOWS_1252,
115        }
116    }
117}
118
119#[derive(Debug)]
120#[non_exhaustive]
121pub enum ImportError {
122    Io {
123        path: PathBuf,
124        source: io::Error,
125    },
126    UnsupportedEncoding(String),
127    InvalidPluginHeader {
128        path: PathBuf,
129        message: String,
130    },
131    MissingContentFiles {
132        files: Vec<String>,
133        searched_paths: Vec<PathBuf>,
134    },
135    MissingArchives {
136        files: Vec<String>,
137        searched_paths: Vec<PathBuf>,
138    },
139    InvalidContentFileName(String),
140    InvalidArchiveName(String),
141    OpenMwConfig(String),
142}
143
144impl fmt::Display for ImportError {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            Self::Io { path, source } => write!(f, "{}: {}", path.display(), source),
148            Self::UnsupportedEncoding(value) => write!(f, "unsupported encoding: {value}"),
149            Self::InvalidPluginHeader { path, message } => {
150                write!(f, "invalid plugin header in {}: {message}", path.display())
151            }
152            Self::MissingContentFiles {
153                files,
154                searched_paths,
155            } => {
156                write!(f, "content files not found: {}", files.join(", "))?;
157                if !searched_paths.is_empty() {
158                    write!(
159                        f,
160                        "; searched: {}",
161                        searched_paths
162                            .iter()
163                            .map(|path| path.display().to_string())
164                            .collect::<Vec<_>>()
165                            .join(", ")
166                    )?;
167                }
168                write!(f, "; pass --data or add data=... to the cfg")
169            }
170            Self::MissingArchives {
171                files,
172                searched_paths,
173            } => {
174                write!(f, "fallback archives not found: {}", files.join(", "))?;
175                if !searched_paths.is_empty() {
176                    write!(
177                        f,
178                        "; searched: {}",
179                        searched_paths
180                            .iter()
181                            .map(|path| path.display().to_string())
182                            .collect::<Vec<_>>()
183                            .join(", ")
184                    )?;
185                }
186                write!(f, "; pass --data or add data=... to the cfg")
187            }
188            Self::InvalidContentFileName(file) => write!(
189                f,
190                "invalid content file name: {file}; content entries must be plugin filenames, not paths"
191            ),
192            Self::InvalidArchiveName(file) => write!(
193                f,
194                "invalid fallback archive name: {file}; archive entries must be BSA filenames, not paths"
195            ),
196            Self::OpenMwConfig(message) => write!(f, "OpenMW config error: {message}"),
197        }
198    }
199}
200
201impl std::error::Error for ImportError {}
202
203#[must_use]
204pub fn known_fallback_keys() -> &'static [&'static str] {
205    fallback_keys::MORROWIND_FALLBACK_KEYS
206}
207
208#[cfg(test)]
209mod lib_tests;