oliver 0.5.1

Lightweight CLI mod development tool for Baldur's Gate 3 on Linux
Documentation
use crate::{read_only_guard::ReadOnlyGuard, settings::Settings};
use anyhow::{bail, Result};
use std::{
    ffi::OsString,
    fs::File,
    io::{BufReader, BufWriter, Read, Seek, SeekFrom},
    os::unix::ffi::OsStringExt,
    path::Path,
};
use zstd::stream::copy_decode;

impl Settings {
    pub(crate) fn import_helper(&self, source: &Path) -> Result<()> {
        let mut source_file = File::open(source)?;
        source_file.seek(SeekFrom::End(0))?;

        loop {
            let length_of_entry = read_length(&source_file)?;

            let Ok(entry_offset) = i64::try_from(length_of_entry) else {
                bail!("found length of {length_of_entry} bytes, which is too large to be correct");
            };

            source_file.seek(SeekFrom::Current(-entry_offset))?;

            if source_file.stream_position()? == 0 {
                return import_file(&self.settings_path, source_file, length_of_entry);
            }

            let Ok(length_to_decode) = usize::try_from(length_of_entry) else {
                bail!("found length of {length_of_entry}, which is too long for this platform");
            };

            let file_name_bytes = read_bytes(&source_file, length_to_decode)?;
            let file_name = OsString::from_vec(file_name_bytes);

            let num_content_bytes = read_length(&source_file)?;

            let Ok(content_offset) = i64::try_from(num_content_bytes) else {
                bail!(
                    "found length of {num_content_bytes} bytes, which is too large to be correct"
                );
            };

            source_file.seek(SeekFrom::Current(-content_offset))?;

            let destination_path = self.get_mod_file_path(file_name);

            import_file(destination_path, &source_file, num_content_bytes)?;
        }
    }
}

fn import_file(
    destination_path: impl AsRef<Path>,
    mut source: impl Read + Seek,
    num_bytes: u64,
) -> Result<()> {
    let reset_point = SeekFrom::Start(source.stream_position()?);

    let destination_file = ReadOnlyGuard::new(destination_path, false)?;

    copy_decode(
        BufReader::new(Read::take(&mut source, num_bytes)),
        BufWriter::new(destination_file),
    )?;

    source.seek(reset_point)?;

    Ok(())
}

fn read_length(mut source: impl Read + Seek) -> Result<u64> {
    source.seek(std::io::SeekFrom::Current(-8))?;
    let reset_point = SeekFrom::Start(source.stream_position()?);

    let mut length_bytes = [0_u8; 8];
    source.read_exact(&mut length_bytes)?;
    let num_bytes = u64::from_le_bytes(length_bytes);

    source.seek(reset_point)?;
    Ok(num_bytes)
}

fn read_bytes(mut source: impl Read + Seek, num_bytes: usize) -> Result<Vec<u8>> {
    let reset_point = SeekFrom::Start(source.stream_position()?);

    let mut bytes = vec![0_u8; num_bytes];
    source.read_exact(&mut bytes)?;

    source.seek(reset_point)?;
    Ok(bytes)
}