brdb 0.5.0

A library for reading and writing Brickadia's World files.
Documentation
use std::ops::Deref;

use indexmap::IndexMap;

use crate::{
    BrFsReader, Brz, BrzIndexData, FoundFile,
    errors::BrFsError,
    fs::BrFs,
    tables::{BrBlob, BrFile, BrFolder},
};

pub struct BrzIndex<T: AsRef<Brz>> {
    pub archive: T,
    /// A map of folder parents and folder names to their indices
    folder_lut: IndexMap<(i32, String), usize>,
    /// A map of file parents and file names to their indices
    files_lut: IndexMap<(i32, String), usize>,
}

impl<T: AsRef<Brz>> From<T> for BrzIndex<T> {
    fn from(brz: T) -> Self {
        Self::new(brz)
    }
}

impl<T: AsRef<Brz>> BrzIndex<T> {
    pub fn new(brz: T) -> Self {
        let mut folder_lut = IndexMap::new();
        for (i, name) in brz.as_ref().index_data.folder_names.iter().enumerate() {
            let parent = brz
                .as_ref()
                .index_data
                .folder_parent_ids
                .get(i)
                .copied()
                .unwrap_or(-1);
            folder_lut.insert((parent, name.clone()), i);
        }

        let mut files_lut = IndexMap::new();
        for (i, name) in brz.as_ref().index_data.file_names.iter().enumerate() {
            let parent = brz
                .as_ref()
                .index_data
                .file_parent_ids
                .get(i)
                .copied()
                .unwrap_or(-1);
            files_lut.insert((parent, name.clone()), i);
        }

        Self {
            archive: brz,
            folder_lut,
            files_lut,
        }
    }
}

impl<T: AsRef<Brz>> Deref for BrzIndex<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.archive
    }
}

fn file_of(index_data: &BrzIndexData, parent: i32, id: i32) -> Result<BrFile, BrFsError> {
    Ok(BrFile {
        name: index_data
            .file_names
            .get(id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("file name for {id}")))?
            .clone(),
        content_id: index_data
            .file_content_ids
            .get(id as usize)
            .map(|i| (*i > -1).then_some(*i as i64))
            .ok_or_else(|| BrFsError::NotFound(format!("file content id for {id}")))?,
        created_at: 0,
        deleted_at: None,
        file_id: id as i64,
        parent_id: (parent > -1).then_some(parent as i64),
    })
}

fn folder_of(index_data: &BrzIndexData, parent: i32, id: i32) -> Result<BrFolder, BrFsError> {
    Ok(BrFolder {
        name: index_data
            .folder_names
            .get(id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("folder name for {id}")))?
            .clone(),
        folder_id: id as i64,
        parent_id: (parent > -1).then_some(parent as i64),
        created_at: 0,
        deleted_at: None,
    })
}

fn children_of(
    index_data: &BrzIndexData,
    files_by_parent: &IndexMap<i32, Vec<i32>>,
    folders_by_parent: &IndexMap<i32, Vec<i32>>,
    id: i32,
) -> Result<IndexMap<String, BrFs>, BrFsError> {
    let mut children = IndexMap::default();

    if let Some(folders) = folders_by_parent.get(&id) {
        for &folder_id in folders {
            let folder = folder_of(index_data, id, folder_id)
                .map_err(|e| e.wrap(format!("folder {folder_id}")))?;
            children.insert(
                folder.name.clone(),
                BrFs::Folder(
                    folder,
                    children_of(index_data, files_by_parent, folders_by_parent, folder_id)?,
                ),
            );
        }
    }

    if let Some(files) = files_by_parent.get(&id) {
        for &file_id in files {
            let file =
                file_of(index_data, id, file_id).map_err(|e| e.wrap(format!("file {file_id}")))?;
            children.insert(file.name.clone(), BrFs::File(file));
        }
    }

    Ok(children)
}

impl<T: AsRef<Brz>> BrFsReader for BrzIndex<T> {
    fn get_fs(&self) -> Result<BrFs, BrFsError> {
        let index_data = &self.archive.as_ref().index_data;
        let mut files_by_parent = IndexMap::<i32, Vec<i32>>::default();
        for i in 0..index_data.num_files {
            let parent = index_data
                .file_parent_ids
                .get(i as usize)
                .copied()
                .unwrap_or(-1);
            files_by_parent.entry(parent).or_default().push(i);
        }

        let mut folders_by_parent = IndexMap::<i32, Vec<i32>>::default();
        for i in 0..index_data.num_folders {
            let parent = index_data
                .folder_parent_ids
                .get(i as usize)
                .copied()
                .unwrap_or(-1);
            folders_by_parent.entry(parent).or_default().push(i);
        }

        Ok(BrFs::Root(children_of(
            index_data,
            &files_by_parent,
            &folders_by_parent,
            -1,
        )?))
    }

    fn find_folder(&self, parent_id: Option<i64>, name: &str) -> Result<Option<i64>, BrFsError> {
        let parent = parent_id.map(|id| id as i32).unwrap_or(-1);
        if let Some(&index) = self.folder_lut.get(&(parent, name.to_string())) {
            Ok(Some(index as i64))
        } else {
            Ok(None)
        }
    }

    fn find_file(
        &self,
        parent_id: Option<i64>,
        name: &str,
    ) -> Result<Option<FoundFile>, BrFsError> {
        let parent = parent_id.map(|id| id as i32).unwrap_or(-1);
        if let Some(&index) = self.files_lut.get(&(parent, name.to_string())) {
            // Get the blob id for the file
            Ok(self
                .archive
                .as_ref()
                .index_data
                .file_content_ids
                .get(index)
                .and_then(|&id| (id > -1).then_some(FoundFile::new(id as i64, 0))))
        } else {
            Ok(None)
        }
    }

    fn find_file_at_revision(
        &self,
        parent_id: Option<i64>,
        name: &str,
        _date: i64,
    ) -> Result<Option<FoundFile>, BrFsError> {
        // BRZ files don't support revisions, so just call find_file
        self.find_file(parent_id, name)
    }

    fn find_blob(&self, blob_id: i64) -> Result<BrBlob, BrFsError> {
        let index_data = &self.archive.as_ref().index_data;
        let content_id = blob_id as i32;
        if content_id < 0 || content_id >= index_data.num_blobs {
            return Err(BrFsError::NotFound(format!("blob {content_id}")));
        }

        let compression_method = *index_data
            .compression_methods
            .get(content_id as usize)
            .ok_or_else(|| {
                BrFsError::NotFound(format!("compression method for blob {content_id}"))
            })?;

        let size_uncompressed = index_data
            .sizes_uncompressed
            .get(content_id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("uncompressed size for blob {content_id}")))?
            .clone() as i64;
        let size_compressed = index_data
            .sizes_compressed
            .get(content_id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("compressed size for blob {content_id}")))?
            .clone() as i64;
        let hash = index_data
            .blob_hashes
            .get(content_id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("hash for blob {content_id}")))?
            .clone();

        let (blob_start_index, blob_end_index) = index_data
            .blob_ranges
            .get(content_id as usize)
            .ok_or_else(|| BrFsError::NotFound(format!("range for blob {content_id}")))?
            .clone();

        let content = self
            .archive
            .as_ref()
            .blob_data
            .get(blob_start_index..blob_end_index)
            .ok_or_else(|| {
                BrFsError::NotFound(format!(
                    "data for blob {content_id} in range [{blob_start_index}, {blob_end_index})"
                ))
            })?
            .to_owned();

        Ok(BrBlob {
            blob_id,
            compression: compression_method as u8 as i64,
            size_uncompressed,
            size_compressed,
            delta_base_id: None,
            hash: hash.to_vec(),
            content,
        })
    }
}