elio 1.0.0

Terminal-native file manager with rich previews, inline images, and mouse support.
Documentation
use super::super::{common::read_zip_entry, metadata::DocumentMetadata};
use super::{
    EPUB_PACKAGE_CACHE_LIMIT,
    assets::{EpubAssetDescriptor, build_epub_asset_descriptor},
    parse::{parse_epub_package_document, parse_epub_rootfile_path, resolve_epub_cover_item},
    system_time_key,
    toc::{EpubSection, build_epub_sections},
};
use std::{
    collections::{HashMap, VecDeque},
    io::Read,
    path::{Path, PathBuf},
    sync::{Arc, Mutex, OnceLock},
};
use zip::ZipArchive;

#[derive(Clone, Debug)]
pub(super) struct CachedEpubPackage {
    pub(super) metadata: DocumentMetadata,
    pub(super) sections: Vec<EpubSection>,
    pub(super) cover_asset: Option<EpubAssetDescriptor>,
}

#[derive(Debug, Default)]
struct EpubPackageCache {
    packages: HashMap<EpubPackageCacheKey, Arc<CachedEpubPackage>>,
    order: VecDeque<EpubPackageCacheKey>,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct EpubPackageCacheKey {
    path: PathBuf,
    size: u64,
    modified: Option<(u64, u32)>,
}

pub(super) fn load_epub_package<R: Read + std::io::Seek>(
    archive: &mut ZipArchive<R>,
    path: &Path,
) -> Option<Arc<CachedEpubPackage>> {
    let cache_key = epub_package_cache_key(path);
    if let Some(cache_key) = cache_key.as_ref()
        && let Some(cached) = cached_epub_package(cache_key)
    {
        return Some(cached);
    }

    let container_xml = read_zip_entry(archive, "META-INF/container.xml")?;
    let package_path = parse_epub_rootfile_path(&container_xml)?;
    let package_xml = read_zip_entry(archive, &package_path)?;
    #[cfg(test)]
    record_epub_package_parse(path);
    let package = parse_epub_package_document(&package_xml);
    let sections = build_epub_sections(archive, &package, &package_path);
    let cover_asset = resolve_epub_cover_item(&package)
        .and_then(|item| build_epub_asset_descriptor(&package_path, item));
    let cached = Arc::new(CachedEpubPackage {
        metadata: package.metadata,
        sections,
        cover_asset,
    });
    if let Some(cache_key) = cache_key {
        cache_epub_package(cache_key, Arc::clone(&cached));
    }
    Some(cached)
}

fn epub_package_cache() -> &'static Mutex<EpubPackageCache> {
    static CACHE: OnceLock<Mutex<EpubPackageCache>> = OnceLock::new();
    CACHE.get_or_init(|| Mutex::new(EpubPackageCache::default()))
}

fn epub_package_cache_key(path: &Path) -> Option<EpubPackageCacheKey> {
    let metadata = std::fs::metadata(path).ok()?;
    Some(EpubPackageCacheKey {
        path: path.to_path_buf(),
        size: metadata.len(),
        modified: metadata.modified().ok().and_then(system_time_key),
    })
}

fn cached_epub_package(key: &EpubPackageCacheKey) -> Option<Arc<CachedEpubPackage>> {
    let mut cache = epub_package_cache()
        .lock()
        .expect("epub package cache lock");
    let package = cache.packages.get(key).cloned();
    if package.is_some() {
        cache.order.retain(|cached| cached != key);
        cache.order.push_back(key.clone());
    }
    package
}

fn cache_epub_package(key: EpubPackageCacheKey, package: Arc<CachedEpubPackage>) {
    let mut cache = epub_package_cache()
        .lock()
        .expect("epub package cache lock");
    cache.packages.insert(key.clone(), package);
    cache.order.retain(|cached| cached != &key);
    cache.order.push_back(key);
    while cache.order.len() > EPUB_PACKAGE_CACHE_LIMIT {
        if let Some(stale_key) = cache.order.pop_front() {
            cache.packages.remove(&stale_key);
        }
    }
}

#[cfg(test)]
fn epub_package_parse_counts() -> &'static Mutex<HashMap<PathBuf, usize>> {
    static COUNTS: OnceLock<Mutex<HashMap<PathBuf, usize>>> = OnceLock::new();
    COUNTS.get_or_init(|| Mutex::new(HashMap::new()))
}

#[cfg(test)]
fn record_epub_package_parse(path: &Path) {
    let mut counts = epub_package_parse_counts()
        .lock()
        .expect("epub package parse count lock");
    *counts.entry(path.to_path_buf()).or_insert(0) += 1;
}

#[cfg(test)]
pub(super) fn reset_epub_package_parse_count(path: &Path) {
    epub_package_parse_counts()
        .lock()
        .expect("epub package parse count lock")
        .remove(path);
}

#[cfg(test)]
pub(super) fn epub_package_parse_count(path: &Path) -> usize {
    epub_package_parse_counts()
        .lock()
        .expect("epub package parse count lock")
        .get(path)
        .copied()
        .unwrap_or(0)
}

#[cfg(test)]
pub(super) fn clear_epub_package_cache() {
    let mut cache = epub_package_cache()
        .lock()
        .expect("epub package cache lock");
    cache.packages.clear();
    cache.order.clear();
}