aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-inspection
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-inspection')
-rw-r--r--crates/fparkan-inspection/Cargo.toml18
-rw-r--r--crates/fparkan-inspection/src/lib.rs286
2 files changed, 304 insertions, 0 deletions
diff --git a/crates/fparkan-inspection/Cargo.toml b/crates/fparkan-inspection/Cargo.toml
new file mode 100644
index 0000000..4f35ecd
--- /dev/null
+++ b/crates/fparkan-inspection/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "fparkan-inspection"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+
+[dependencies]
+fparkan-msh = { path = "../fparkan-msh" }
+fparkan-nres = { path = "../fparkan-nres" }
+fparkan-rsli = { path = "../fparkan-rsli" }
+fparkan-resource = { path = "../fparkan-resource" }
+fparkan-terrain-format = { path = "../fparkan-terrain-format" }
+fparkan-texm = { path = "../fparkan-texm" }
+fparkan-vfs = { path = "../fparkan-vfs" }
+
+[lints]
+workspace = true
diff --git a/crates/fparkan-inspection/src/lib.rs b/crates/fparkan-inspection/src/lib.rs
new file mode 100644
index 0000000..0b35ad6
--- /dev/null
+++ b/crates/fparkan-inspection/src/lib.rs
@@ -0,0 +1,286 @@
+#![forbid(unsafe_code)]
+//! Shared inspection helpers for format-backed tooling.
+
+use fparkan_msh::{decode_msh, validate_msh};
+use fparkan_nres::{decode as decode_nres, NresDocument, ReadProfile};
+use fparkan_resource::{archive_path, resource_name, CachedResourceRepository};
+use fparkan_rsli::decode as decode_rsli;
+use fparkan_terrain_format::{decode_land_map, decode_land_msh};
+use fparkan_texm::decode_texm;
+use fparkan_vfs::{DirectoryVfs, Vfs};
+use std::fs;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+
+/// Archive inspection variants.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum ArchiveInspection {
+ /// NRes inspection summary.
+ Nres {
+ /// Archive entry count.
+ entries: usize,
+ /// Lookup order validity.
+ lookup_order_valid: bool,
+ /// Entry samples (subject to request limit).
+ sample: Vec<NresEntrySummary>,
+ },
+ /// RsLi inspection summary.
+ Rsli {
+ /// Archive entry count.
+ entries: usize,
+ },
+ /// Unknown/unsupported archive magic.
+ Unsupported,
+}
+
+/// NRes entry summary.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct NresEntrySummary {
+ /// ASCII/legacy resource name.
+ pub name: String,
+ /// Entry type identifier.
+ pub type_id: u32,
+ /// Declared entry payload size.
+ pub data_size: u32,
+}
+
+/// Model inspection payload.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ModelInspection {
+ /// Terrain stream/document stream count.
+ pub streams: usize,
+ /// Node count.
+ pub nodes: usize,
+ /// Slot count.
+ pub slots: usize,
+ /// Position count.
+ pub positions: usize,
+ /// Index count.
+ pub indices: usize,
+ /// Batch count.
+ pub batches: usize,
+}
+
+/// Texture inspection payload.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct TextureInspection {
+ /// Width.
+ pub width: u32,
+ /// Height.
+ pub height: u32,
+ /// Texture format debug text.
+ pub format: String,
+ /// Mip level count.
+ pub mips: usize,
+ /// Total page rectangles.
+ pub pages: usize,
+}
+
+/// Land map/msh inspection payload.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MapInspection {
+ /// Mapped mesh stream count.
+ pub streams: usize,
+ /// Slot count.
+ pub slots: usize,
+ /// Position count.
+ pub positions: usize,
+ /// Face count.
+ pub faces: usize,
+ /// Terrain areals.
+ pub areals: usize,
+ /// Declared areal count from map metadata.
+ pub declared_areals: u32,
+ /// Map grid width.
+ pub grid_width: u32,
+ /// Map grid height.
+ pub grid_height: u32,
+}
+
+/// Supported land file kinds.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum LandFileKind {
+ /// `land.msh` payload.
+ LandMsh,
+ /// `land.map` payload.
+ LandMap,
+}
+
+/// Inspects a format archive.
+pub fn inspect_archive_file(path: &Path, sample_limit: usize) -> Result<ArchiveInspection, String> {
+ let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
+ inspect_archive_bytes(&bytes, sample_limit, Some(path))
+}
+
+/// Inspects archive bytes and returns a typed summary.
+fn inspect_archive_bytes(
+ bytes: &[u8],
+ sample_limit: usize,
+ source: Option<&Path>,
+) -> Result<ArchiveInspection, String> {
+ if bytes.starts_with(b"NRes") {
+ let document = decode_nres(
+ Arc::from(bytes.to_vec().into_boxed_slice()),
+ ReadProfile::Compatible,
+ )
+ .map_err(|err| err.to_string())?;
+ let mut sample = Vec::new();
+ for entry in document.entries().iter().take(sample_limit) {
+ sample.push(NresEntrySummary {
+ name: String::from_utf8_lossy(entry.name_bytes()).to_string(),
+ type_id: entry.meta().type_id,
+ data_size: entry.meta().data_size,
+ });
+ }
+ Ok(ArchiveInspection::Nres {
+ entries: document.entries().len(),
+ lookup_order_valid: document.lookup_order_valid(),
+ sample,
+ })
+ } else if bytes.get(0..4) == Some(b"NL\0\x01") {
+ let document = decode_rsli(Arc::from(bytes.to_vec().into_boxed_slice()), fparkan_rsli::ReadProfile::Compatible)
+ .map_err(|err| err.to_string())?;
+ Ok(ArchiveInspection::Rsli {
+ entries: document.entries().len(),
+ })
+ } else {
+ match source {
+ Some(path) => Err(format!("{}: unsupported archive magic", path.display())),
+ None => Err("unsupported archive magic".to_string()),
+ }
+ }
+}
+
+/// Inspects a model through repository-backed resource lookup.
+pub fn inspect_model_from_root(
+ root: &Path,
+ archive: &str,
+ resource: &str,
+) -> Result<ModelInspection, String> {
+ let bytes = read_resource_bytes(root, archive, resource)?;
+ let document = decode_nres(bytes, ReadProfile::Compatible).map_err(|err| err.to_string())?;
+ let msh = decode_msh(&document).map_err(|err| err.to_string())?;
+ let validated = validate_msh(&msh).map_err(|err| err.to_string())?;
+ Ok(ModelInspection {
+ streams: msh.streams().len(),
+ nodes: validated.node_count,
+ slots: validated.slots.len(),
+ positions: validated.positions.len(),
+ indices: validated.indices.len(),
+ batches: validated.batches.len(),
+ })
+}
+
+/// Inspects a texture through repository-backed resource lookup.
+pub fn inspect_texture_from_root(
+ root: &Path,
+ archive: &str,
+ resource: &str,
+) -> Result<TextureInspection, String> {
+ let bytes = read_resource_bytes(root, archive, resource)?;
+ let document = decode_texm(bytes).map_err(|err| err.to_string())?;
+ Ok(TextureInspection {
+ width: document.width(),
+ height: document.height(),
+ format: format!("{:?}", document.format()),
+ mips: document.mip_count(),
+ pages: document.page_rects().len(),
+ })
+}
+
+/// Inspects a terrain land file by path.
+pub fn inspect_land_file(path: &Path, kind: LandFileKind) -> Result<MapInspection, String> {
+ let bytes = fs::read(path).map_err(|err| format!("{}: {err}", path.display()))?;
+ let document = decode_nres(
+ Arc::from(bytes.into_boxed_slice()),
+ ReadProfile::Compatible,
+ )
+ .map_err(|err| err.to_string())?;
+ match kind {
+ LandFileKind::LandMsh => inspect_land_msh(&document),
+ LandFileKind::LandMap => inspect_land_map(&document),
+ }
+}
+
+fn inspect_land_msh(document: &NresDocument) -> Result<MapInspection, String> {
+ let land_msh = decode_land_msh(document).map_err(|err| err.to_string())?;
+ Ok(MapInspection {
+ streams: land_msh.streams.len(),
+ slots: land_msh.slots.slots_raw.len(),
+ positions: land_msh.positions.len(),
+ faces: land_msh.faces.len(),
+ areals: 0,
+ declared_areals: 0,
+ grid_width: 0,
+ grid_height: 0,
+ })
+}
+
+fn inspect_land_map(document: &NresDocument) -> Result<MapInspection, String> {
+ let land_map = decode_land_map(document).map_err(|err| err.to_string())?;
+ Ok(MapInspection {
+ streams: 0,
+ slots: 0,
+ positions: 0,
+ faces: 0,
+ areals: land_map.areals.len(),
+ declared_areals: land_map.areal_count,
+ grid_width: land_map.grid.cells_x,
+ grid_height: land_map.grid.cells_y,
+ })
+}
+
+fn read_resource_bytes(root: &Path, archive: &str, name: &str) -> Result<Arc<[u8]>, String> {
+ let repository = CachedResourceRepository::new(Arc::new(DirectoryVfs::new(root)));
+ let archive_path = archive_path(archive.as_bytes()).map_err(|err| err.to_string())?;
+ let resource_name = resource_name(name.as_bytes());
+ let archive_handle = repository
+ .open_archive(&archive_path)
+ .map_err(|err| format!("{err}"))?;
+ let Some(handle) = repository
+ .find(archive_handle, &resource_name)
+ .map_err(|err| format!("{err}"))?
+ else {
+ return Err(format!(
+ "resource not found: {archive}/{}",
+ String::from_utf8_lossy(name.as_bytes())
+ ));
+ };
+ let bytes = repository.read(handle).map_err(|err| format!("{err}"))?;
+ Ok(Arc::from(bytes.into_owned()))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Write as _;
+
+ #[test]
+ fn inspect_rsli_counts_entries() {
+ let dir = temp_dir("inspect");
+ let path = dir.join("test.rsli");
+ let mut file = fs::File::create(&path).expect("file");
+ file.write_all(b"NL\0\x01").expect("magic");
+ drop(file);
+
+ let inspection = inspect_archive_file(&path, 0).expect("inspect");
+ assert!(matches!(inspection, ArchiveInspection::Rsli { entries: 0 }));
+ }
+
+ #[test]
+ fn nres_entry_summary_fields_are_readable() {
+ let dir = temp_dir("inspect-nres");
+ let archive = dir.join("test.nres");
+ let payload = Vec::from("NRes\x00\x00\x00\x00");
+ fs::write(&archive, &payload).expect("nres");
+
+ let _ = inspect_archive_file(&archive, 2);
+ }
+
+ fn temp_dir(name: &str) -> PathBuf {
+ let base = PathBuf::from("/tmp").join("fparkan-inspection-tests").join(name);
+ let _ = fs::remove_dir_all(&base);
+ fs::create_dir_all(&base).expect("tmp dir");
+ base
+ }
+}