oliver 0.1.2

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
use anyhow::{bail, Result};
use std::{
    ffi::OsString,
    fs::{File, OpenOptions},
    io::{BufReader, BufWriter, Read, Seek, SeekFrom},
    os::unix::ffi::OsStringExt,
    path::Path,
};
use zstd::stream::copy_decode;

use crate::common::{mods_directory_path, settings_file_path};

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 = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open(destination_path)?;

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

pub fn import(app_data_path: &str, 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(settings_file_path(app_data_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 = mods_directory_path(app_data_path).join(file_name);

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