Skip to main content

rakata_extract/
file.rs

1//! Loose-file resource primitives.
2//!
3//! This module provides a minimal typed wrapper around one on-disk resource
4//! file (`*.utc`, `*.tga`, etc.) without any archive/container semantics.
5//!
6//! ## Scope
7//! - Canonical path + identifier extraction.
8//! - Byte loading for downstream format parsing.
9//! - No installation discovery or search-order logic.
10
11use std::fs;
12use std::path::{Path, PathBuf};
13
14use thiserror::Error;
15
16use rakata_core::ResourceIdentifier;
17
18/// One loose resource file on disk.
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct ResourceFile {
21    path: PathBuf,
22    identifier: ResourceIdentifier,
23}
24
25impl ResourceFile {
26    /// Creates a resource-file wrapper from an existing path.
27    pub fn open(path: impl AsRef<Path>) -> Result<Self, FileResourceError> {
28        let path = path.as_ref();
29        let metadata = fs::metadata(path).map_err(|source| FileResourceError::io(path, source))?;
30        if !metadata.is_file() {
31            return Err(FileResourceError::NotAFile {
32                path: path.to_path_buf(),
33            });
34        }
35        Ok(Self {
36            path: path.to_path_buf(),
37            identifier: ResourceIdentifier::from_path(path),
38        })
39    }
40
41    /// Returns the filesystem path.
42    pub fn path(&self) -> &Path {
43        &self.path
44    }
45
46    /// Returns the canonical `(resname, restype)` identifier.
47    pub fn identifier(&self) -> &ResourceIdentifier {
48        &self.identifier
49    }
50
51    /// Returns the file contents.
52    pub fn read_bytes(&self) -> Result<Vec<u8>, FileResourceError> {
53        fs::read(&self.path).map_err(|source| FileResourceError::io(&self.path, source))
54    }
55}
56
57/// Reads one resource file and returns identifier + payload bytes.
58pub fn read_resource_file(path: impl AsRef<Path>) -> Result<ResourceFileData, FileResourceError> {
59    let file = ResourceFile::open(path)?;
60    let data = file.read_bytes()?;
61    Ok(ResourceFileData {
62        path: file.path,
63        identifier: file.identifier,
64        data,
65    })
66}
67
68/// Loaded loose resource-file payload.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ResourceFileData {
71    /// Filesystem path.
72    pub path: PathBuf,
73    /// Canonical `(resname, restype)` identifier.
74    pub identifier: ResourceIdentifier,
75    /// Raw file bytes.
76    pub data: Vec<u8>,
77}
78
79/// Errors produced by loose-file primitives.
80#[derive(Debug, Error)]
81pub enum FileResourceError {
82    /// I/O failure while reading metadata or file bytes.
83    #[error("I/O failure for `{path}`: {source}")]
84    Io {
85        /// Path being accessed.
86        path: PathBuf,
87        /// Underlying OS error.
88        #[source]
89        source: std::io::Error,
90    },
91    /// Path exists but is not a regular file.
92    #[error("path is not a regular file: `{path}`")]
93    NotAFile {
94        /// Non-file path.
95        path: PathBuf,
96    },
97}
98
99impl FileResourceError {
100    fn io(path: &Path, source: std::io::Error) -> Self {
101        Self::Io {
102            path: path.to_path_buf(),
103            source,
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    use tempfile::TempDir;
113
114    #[test]
115    fn reads_loose_resource_file() {
116        let temp = TempDir::new().expect("create tempdir");
117        let path = temp.path().join("p_bastila.utc");
118        fs::write(&path, b"abc").expect("write fixture");
119
120        let file = ResourceFile::open(&path).expect("open file");
121        assert_eq!(file.identifier().resname(), "p_bastila");
122        assert_eq!(file.identifier().to_string(), "p_bastila.utc");
123        assert_eq!(file.read_bytes().expect("read bytes"), b"abc");
124    }
125
126    #[test]
127    fn read_resource_file_returns_data_and_identifier() {
128        let temp = TempDir::new().expect("create tempdir");
129        let path = temp.path().join("dialog.tlk");
130        fs::write(&path, b"tlk").expect("write fixture");
131
132        let loaded = read_resource_file(&path).expect("read resource file");
133        assert_eq!(loaded.identifier.resname(), "dialog");
134        assert_eq!(loaded.data, b"tlk");
135    }
136}