Files
Starlight_Lancher/packages/app-lib/src/mod_metadata/icon.rs
T

77 lines
2.5 KiB
Rust

//! Extract icon artwork from inside mod JARs and resource pack ZIPs so
//! unmatched content files can display real icons.
use bytes::Bytes;
use std::io::{Read, Seek};
use std::path::Path;
use zip::ZipArchive;
/// Entries larger than this are almost certainly not pack icons.
const MAX_ICON_BYTES: u64 = 2 * 1024 * 1024;
/// Returns the matched entry name and validated image bytes for a mod JAR.
///
/// Prefers the icon declared by the embedded mod metadata, then falls back
/// to the in-game `pack.png` convention at the archive root.
pub fn extract_mod_icon(
bytes: &Bytes,
metadata: Option<&crate::mod_metadata::LocalModMetadata>,
) -> Option<(String, Vec<u8>)> {
let cursor = std::io::Cursor::new(&**bytes);
let mut archive = ZipArchive::new(cursor).ok()?;
if let Some(icon_path) = metadata.and_then(|meta| meta.icon_path.as_deref())
&& let Some(icon) = read_entry(&mut archive, icon_path)
{
return Some(icon);
}
read_entry(&mut archive, "pack.png")
}
/// Returns the matched entry name and validated image bytes for a resource
/// pack ZIP. Resource packs declare their icon as `pack.png` at the root.
pub fn extract_resource_pack_icon(path: &Path) -> Option<(String, Vec<u8>)> {
let file = std::fs::File::open(path).ok()?;
let mut archive = ZipArchive::new(file).ok()?;
read_entry(&mut archive, "pack.png")
}
fn read_entry<R: Read + Seek>(
archive: &mut ZipArchive<R>,
entry_name: &str,
) -> Option<(String, Vec<u8>)> {
let normalized = entry_name.trim_start_matches('/').replace('\\', "/");
let entry = open_entry(archive, &normalized)?;
if !entry.is_file() || entry.size() > MAX_ICON_BYTES {
return None;
}
let entry_name = entry.name().to_string();
let mut data = Vec::with_capacity(entry.size() as usize);
entry.take(MAX_ICON_BYTES + 1).read_to_end(&mut data).ok()?;
is_supported_image(&data).then_some((entry_name, data))
}
fn open_entry<'a, R: Read + Seek>(
archive: &'a mut ZipArchive<R>,
normalized: &str,
) -> Option<zip::read::ZipFile<'a, R>> {
if archive.by_name(normalized).is_ok() {
archive.by_name(normalized).ok()
} else {
let names: Vec<String> =
archive.file_names().map(ToOwned::to_owned).collect();
let matched = names
.iter()
.find(|name| name.eq_ignore_ascii_case(normalized))?;
archive.by_name(matched).ok()
}
}
fn is_supported_image(data: &[u8]) -> bool {
data.starts_with(&[0x89, b'P', b'N', b'G'])
|| data.starts_with(&[0xFF, 0xD8, 0xFF])
}