Skip to main content

oliver/
lib.rs

1pub use larian_formats;
2
3use anyhow::{Result, anyhow, bail};
4use clap::ValueEnum;
5use larian_formats::{
6    lsf::LsfData,
7    lspk::{self, Lspk, ModuleInfo, is_override},
8};
9use std::{
10    collections::VecDeque,
11    ffi::OsStr,
12    fs::{File, read_dir},
13    path::{Path, PathBuf},
14};
15
16/// Specifies the type of data file.
17#[derive(Debug, ValueEnum, Clone, Copy)]
18pub enum DataFileType {
19    Lsf,
20    Lsx,
21}
22
23impl DataFileType {
24    fn from_path(path: &Path) -> Option<Self> {
25        match path.extension().map(|s| s.to_string_lossy()).as_deref() {
26            Some("lsf") => Some(Self::Lsf),
27            Some("lsx") => Some(Self::Lsx),
28            _ => None,
29        }
30    }
31
32    const fn extension(self) -> &'static str {
33        match self {
34            Self::Lsf => "lsf",
35            Self::Lsx => "lsx",
36        }
37    }
38}
39
40macro_rules! convert_inner {
41    (
42        $from: expr,
43        $to: expr,
44        $read_fn: expr,
45        $write_fn: expr $(,)?
46    ) => {
47        let mut input = File::open($from)?;
48        let data = $read_fn(&mut input)
49            .map_err(|e| anyhow!("Failed to parse {}: {e}", $from.display()))?;
50        let mut out = File::options()
51            .create(true)
52            .write(true)
53            .truncate(true)
54            .open($to)?;
55        $write_fn(&data, &mut out).map_err(|e| {
56            anyhow!("Failed to serialize data from {} into {}: {e}", $from.display(), $to.display())
57        })?;
58        eprintln!("Converted {} to {}", $from.display(), $to.display());
59    };
60}
61
62/// Lists the file paths contained in the mod file.
63///
64/// # Errors
65///
66/// Returns an error if the mod file header could not be parsed.
67#[cfg(unix)]
68pub fn contents(path: &Path) -> Result<()> {
69    use std::os::unix::ffi::OsStrExt;
70
71    let paths = larian_formats::lspk::list_mod_pack_files(path)?;
72
73    for bytes in paths.iter() {
74        println!("{}", OsStr::from_bytes(bytes.as_ref()).display());
75    }
76
77    Ok(())
78}
79
80/// Extracts a single file from a mod.
81///
82/// # Errors
83///
84/// Returns an error if the target path is not relative, if the mod file header could not be parsed,
85/// if the target file path does not exist in the mod, or the file could not be decompressed and
86/// read.
87#[cfg(unix)]
88pub fn extract_file(from: &Path, internal: &Path, to: &Path) -> Result<()> {
89    use anyhow::Context;
90
91    let Some(bytes) = larian_formats::raw::extract_file_from_pak(internal, from)? else {
92        bail!("{} not found in {}", internal.display(), from.display());
93    };
94
95    std::fs::write(to, bytes).with_context(|| {
96        format!(
97            "could not write {} from {} to {}",
98            internal.display(),
99            from.display(),
100            to.display()
101        )
102    })
103}
104
105/// Recursively converts all files from one type to another in the given directory and its
106/// subdirectories.
107///
108/// # Errors
109///
110/// Returns an error if conversion fails for any file.
111pub fn convert_all(path: PathBuf, from: DataFileType, to: DataFileType) -> Result<()> {
112    let mut dirs = VecDeque::from_iter([path]);
113
114    while let Some(dir) = dirs.pop_front() {
115        for entry in read_dir(dir)? {
116            let e = entry?;
117            let file_type = e.file_type()?;
118
119            if file_type.is_dir() {
120                dirs.push_back(e.path());
121            } else if file_type.is_file() &&
122                e.path().extension() == Some(OsStr::new(from.extension()))
123            {
124                let mut dest = e.path().clone();
125                dest.set_extension(to.extension());
126                convert(&e.path(), &dest)?;
127            }
128        }
129    }
130
131    Ok(())
132}
133
134/// Converts a single file from from one type to another.
135///
136/// # Errors
137///
138/// Returns an error if conversion fails.
139pub fn convert(from: &Path, to: &Path) -> Result<()> {
140    let Some(from_type) = DataFileType::from_path(from) else {
141        bail!("`{}` does not have either `.lsf` or `.lsx` extension", from.display());
142    };
143
144    let Some(to_type) = DataFileType::from_path(to) else {
145        bail!("`{}` does not have either `.lsf` or `.lsx` extension", to.display());
146    };
147
148    match (from_type, to_type) {
149        (DataFileType::Lsf, DataFileType::Lsx) => {
150            convert_inner!(from, to, LsfData::read_lsf, LsfData::write_lsx);
151        }
152        (DataFileType::Lsx, DataFileType::Lsf) => {
153            convert_inner!(from, to, LsfData::read_lsx, LsfData::write_lsf);
154        }
155        (..) => {
156            std::fs::copy(from, to)?;
157        }
158    }
159
160    Ok(())
161}
162
163/// Packs the loose mod files in the given directory into an LSPK file.
164///
165/// # Errors
166///
167/// Returns an error if packing fails.
168pub fn pack(mod_files_root: PathBuf, destination: Option<PathBuf>) -> Result<()> {
169    lspk::write(mod_files_root, destination)?;
170    Ok(())
171}
172
173/// Unpacks the given LSPK file into loose mod files.
174///
175/// # Errors
176///
177/// Returns an error if the LSPK file is invalid or if creating the loose files fails.
178pub fn unpack(mod_file_path: &Path, destination: Option<PathBuf>) -> Result<()> {
179    let data = Lspk::from_file(mod_file_path)?;
180
181    let prefix_dir = match destination {
182        Some(unpack_dir) => {
183            std::fs::create_dir_all(&unpack_dir)?;
184            unpack_dir
185        }
186        None => "./".into(),
187    };
188
189    for file in data.files {
190        let path = prefix_dir.join(file.path);
191
192        if let Some(parent) = path.parent() {
193            std::fs::create_dir_all(parent)?;
194        }
195
196        std::fs::write(path, file.contents)?;
197    }
198
199    Ok(())
200}
201
202/// Parses the given paths, printing LSPK metadata if verbose is enabled.
203///
204/// # Errors
205///
206/// Returns an error if a valid LSPK file cannot be parsed at one of the given paths.
207pub fn parse(paths: impl IntoIterator<Item = PathBuf>, verbose: bool) -> Result<()> {
208    for path in paths {
209        let displayed_path = path.display().to_string();
210        let data = Lspk::from_file(&path)?;
211
212        print!("{displayed_path}");
213
214        if is_override(path) {
215            print!(" (override)");
216        }
217
218        println!();
219
220        for _ in displayed_path.chars() {
221            print!("-");
222        }
223
224        println!();
225
226        print_lspk(&data, verbose)?;
227
228        println!();
229    }
230
231    Ok(())
232}
233
234fn print_lspk(data: &Lspk, verbose: bool) -> Result<()> {
235    let meta_lsx = data.deserialize_meta_lsx()?;
236
237    let ModuleInfo {
238        author,
239        description,
240        folder,
241        md5,
242        name,
243        num_players,
244        module_type,
245        uuid,
246        version,
247        ..
248    } = meta_lsx.module_info;
249
250    println!("Name         : {name}");
251    println!("Folder       : {folder}");
252    println!("Version64    : {version}");
253    println!("UUID         : {uuid}");
254
255    if !verbose {
256        return Ok(());
257    }
258
259    println!("Author       : {author}");
260    println!("Description  : {description}");
261    println!("MD5          : {md5}");
262    println!("NumPlayers   : {num_players}");
263    println!("Type         : {module_type}");
264
265    Ok(())
266}