aboutsummaryrefslogtreecommitdiff
path: root/crates/unitdat/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/unitdat/src/lib.rs')
-rw-r--r--crates/unitdat/src/lib.rs180
1 files changed, 0 insertions, 180 deletions
diff --git a/crates/unitdat/src/lib.rs b/crates/unitdat/src/lib.rs
deleted file mode 100644
index 6414e66..0000000
--- a/crates/unitdat/src/lib.rs
+++ /dev/null
@@ -1,180 +0,0 @@
-use encoding_rs::WINDOWS_1251;
-use std::fmt;
-use std::fs;
-use std::path::Path;
-
-const MIN_SIZE: usize = 0x48;
-const MAGIC: u32 = 0x0000_F0F1;
-
-pub type Result<T> = core::result::Result<T, Error>;
-
-#[derive(Debug)]
-pub enum Error {
- Io(std::io::Error),
- TooSmall { got: usize },
- InvalidMagic { got: u32 },
- MissingArchiveName,
- MissingModelKey,
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Io(err) => write!(f, "{err}"),
- Self::TooSmall { got } => write!(f, "unit .dat is too small: {got} bytes"),
- Self::InvalidMagic { got } => write!(f, "invalid .dat magic: 0x{got:08X}"),
- Self::MissingArchiveName => write!(f, "unit .dat has empty archive name"),
- Self::MissingModelKey => write!(f, "unit .dat has empty model key"),
- }
- }
-}
-
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Io(err) => Some(err),
- _ => None,
- }
- }
-}
-
-impl From<std::io::Error> for Error {
- fn from(value: std::io::Error) -> Self {
- Self::Io(value)
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct UnitDat {
- pub magic: u32,
- pub flags: u32,
- pub archive_name: String,
- pub model_key: String,
-}
-
-pub fn parse_path(path: impl AsRef<Path>) -> Result<UnitDat> {
- let bytes = fs::read(path.as_ref())?;
- parse_bytes(&bytes)
-}
-
-pub fn parse_bytes(bytes: &[u8]) -> Result<UnitDat> {
- if bytes.len() < MIN_SIZE {
- return Err(Error::TooSmall { got: bytes.len() });
- }
-
- let magic = read_u32(bytes, 0).ok_or(Error::TooSmall { got: bytes.len() })?;
- if magic != MAGIC {
- return Err(Error::InvalidMagic { got: magic });
- }
-
- let flags = read_u32(bytes, 4).ok_or(Error::TooSmall { got: bytes.len() })?;
- let archive_name = decode_c_string_fixed(&bytes[0x08..0x28]);
- if archive_name.is_empty() {
- return Err(Error::MissingArchiveName);
- }
-
- let model_key = decode_c_string_fixed(&bytes[0x28..0x48]);
- if model_key.is_empty() {
- return Err(Error::MissingModelKey);
- }
-
- Ok(UnitDat {
- magic,
- flags,
- archive_name,
- model_key,
- })
-}
-
-fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
- let end = offset.checked_add(4)?;
- let chunk = bytes.get(offset..end)?;
- Some(u32::from_le_bytes(chunk.try_into().ok()?))
-}
-
-fn decode_c_string_fixed(bytes: &[u8]) -> String {
- let used = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
- let (decoded, _, _) = WINDOWS_1251.decode(&bytes[..used]);
- decoded.trim().to_string()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use common::collect_files_recursive;
- use std::path::{Path, PathBuf};
-
- fn game_root() -> Option<PathBuf> {
- let root = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("..")
- .join("..")
- .join("testdata")
- .join("Parkan - Iron Strategy");
- root.is_dir().then_some(root)
- }
-
- #[test]
- fn parses_known_dat_files() {
- let Some(root) = game_root() else {
- eprintln!("skipping: game root missing");
- return;
- };
-
- let samples = [
- root.join("UNITS/UNITS/HERO/tut1_p.dat"),
- root.join("UNITS/UNITS/BATTLE/l_targ.dat"),
- root.join("UNITS/BUILDS/BRIDGE/m_bridge.dat"),
- ];
-
- for path in samples {
- if !path.is_file() {
- eprintln!("skipping missing sample {}", path.display());
- continue;
- }
- let dat = parse_path(&path)
- .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()));
- assert_eq!(dat.magic, MAGIC);
- assert!(dat.archive_name.to_ascii_lowercase().ends_with(".rlb"));
- assert!(dat.model_key.contains('_'));
- }
- }
-
- #[test]
- fn parses_retail_dat_corpus() {
- let Some(root) = game_root() else {
- eprintln!("skipping: game root missing");
- return;
- };
-
- let units_root = root.join("UNITS");
- let mut files = Vec::new();
- collect_files_recursive(&units_root, &mut files);
- files.sort();
-
- let mut parsed = 0usize;
- for path in files {
- if !path
- .extension()
- .and_then(|ext| ext.to_str())
- .is_some_and(|ext| ext.eq_ignore_ascii_case("dat"))
- {
- continue;
- }
- let dat = parse_path(&path)
- .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()));
- assert!(
- !dat.archive_name.is_empty(),
- "{} empty archive",
- path.display()
- );
- assert!(
- !dat.model_key.is_empty(),
- "{} empty model key",
- path.display()
- );
- parsed += 1;
- }
-
- assert!(parsed > 0, "no .dat files parsed");
- }
-}