Skip to main content

nwnrs_resfile/
read.rs

1use std::{
2    fs::{self, File},
3    io,
4    path::Path,
5    sync::Arc,
6    time::SystemTime,
7};
8
9use nwnrs_checksums::prelude::*;
10use nwnrs_exo::prelude::*;
11use nwnrs_resman::prelude::*;
12use nwnrs_resref::prelude::*;
13use tracing::{debug, instrument};
14
15use crate::{ResFile, ResFileError, ResFileResult};
16
17/// Reads a resource file using its filename-derived resource reference.
18///
19/// # Errors
20///
21/// Returns [`ResFileError`] if the filename cannot be resolved or the file
22/// cannot be read.
23#[allow(clippy :: redundant_closure_call)]
match (move ||
                {

                    #[allow(unknown_lints, unreachable_code, clippy ::
                    diverging_sub_expression, clippy :: empty_loop, clippy ::
                    let_unit_value, clippy :: let_with_type_underscore, clippy
                    :: needless_return, clippy :: unreachable)]
                    if false {
                        let __tracing_attr_fake_return: ResFileResult<ResFile> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let path = path.as_ref();
                        let file_name =
                            path.file_name().and_then(|value|
                                            value.to_str()).ok_or_else(||
                                        ResFileError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("{0} has no valid filename",
                                                            path.display()))
                                                })))?;
                        let resolved = ResolvedResRef::from_filename(file_name)?;
                        read_resfile_as(path, resolved.base().clone())
                    }
                })()
    {
        #[allow(clippy :: unit_arg)]
        Ok(x) => Ok(x),
    Err(e) => {
        {
            use ::tracing::__macro_support::Callsite as _;
            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                {
                    static META: ::tracing::Metadata<'static> =
                        {
                            ::tracing_core::metadata::Metadata::new("event src/read.rs:23",
                                "nwnrs_resfile::read", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/read.rs"),
                                ::tracing_core::__macro_support::Option::Some(23u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resfile::read"),
                                ::tracing_core::field::FieldSet::new(&[{
                                                    const NAME:
                                                        ::tracing::__macro_support::FieldName<{
                                                            ::tracing::__macro_support::FieldName::len("error")
                                                        }> =
                                                        ::tracing::__macro_support::FieldName::new("error");
                                                    NAME.as_str()
                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                ::tracing::metadata::Kind::EVENT)
                        };
                    ::tracing::callsite::DefaultCallsite::new(&META)
                };
            let enabled =
                ::tracing::Level::ERROR <=
                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                        ::tracing::Level::ERROR <=
                            ::tracing::level_filters::LevelFilter::current() &&
                    {
                        let interest = __CALLSITE.interest();
                        !interest.is_never() &&
                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                interest)
                    };
            if enabled {
                (|value_set: ::tracing::field::ValueSet|
                            {
                                let meta = __CALLSITE.metadata();
                                ::tracing::Event::dispatch(meta, &value_set);
                                ;
                            })({
                        #[allow(unused_imports)]
                        use ::tracing::field::{debug, display, Value};
                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&::tracing::field::display(&e)
                                                    as &dyn ::tracing::field::Value))])
                    });
            } else { ; }
        };
        Err(e)
    }
}#[instrument(level = "debug", skip_all, err, fields(path = %path.as_ref().display()))]
24pub fn read_resfile(path: impl AsRef<Path>) -> ResFileResult<ResFile> {
25    let path = path.as_ref();
26    let file_name = path
27        .file_name()
28        .and_then(|value| value.to_str())
29        .ok_or_else(|| ResFileError::msg(format!("{} has no valid filename", path.display())))?;
30    let resolved = ResolvedResRef::from_filename(file_name)?;
31    read_resfile_as(path, resolved.base().clone())
32}
33
34/// Reads a resource file with an explicit resource reference override.
35///
36/// # Errors
37///
38/// Returns [`ResFileError`] if the path is not a regular file or metadata
39/// cannot be read.
40#[allow(clippy :: redundant_closure_call)]
match (move ||
                {

                    #[allow(unknown_lints, unreachable_code, clippy ::
                    diverging_sub_expression, clippy :: empty_loop, clippy ::
                    let_unit_value, clippy :: let_with_type_underscore, clippy
                    :: needless_return, clippy :: unreachable)]
                    if false {
                        let __tracing_attr_fake_return: ResFileResult<ResFile> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let path = path.as_ref();
                        let metadata = fs::metadata(path)?;
                        if !metadata.is_file() {
                            return Err(ResFileError::msg(::alloc::__export::must_use({
                                                ::alloc::fmt::format(format_args!("{0} is not a regular file",
                                                        path.display()))
                                            })));
                        }
                        let label = path.display().to_string();
                        let mtime =
                            metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
                        let io_size = metadata.len().cast_signed();
                        let path_for_io = path.to_path_buf();
                        let origin_label = label.clone();
                        let spawner =
                            Arc::new(move ||
                                    -> io::Result<Box<dyn nwnrs_resman::ReadSeek + Send>>
                                    { Ok(Box::new(File::open(&path_for_io)?)) });
                        let result =
                            ResFile {
                                path: path.to_path_buf(),
                                label: label.clone(),
                                entry: Res::new_with_spawner(new_res_origin(::alloc::__export::must_use({
                                                ::alloc::fmt::format(format_args!("ResFile:{0}", label))
                                            }), origin_label), resref, mtime, spawner, io_size, 0,
                                    ExoResFileCompressionType::None, None,
                                    usize::try_from(io_size.max(0)).unwrap_or(usize::MAX),
                                    EMPTY_SECURE_HASH),
                            };
                        {
                            use ::tracing::__macro_support::Callsite as _;
                            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                                {
                                    static META: ::tracing::Metadata<'static> =
                                        {
                                            ::tracing_core::metadata::Metadata::new("event src/read.rs:78",
                                                "nwnrs_resfile::read", ::tracing::Level::DEBUG,
                                                ::tracing_core::__macro_support::Option::Some("src/read.rs"),
                                                ::tracing_core::__macro_support::Option::Some(78u32),
                                                ::tracing_core::__macro_support::Option::Some("nwnrs_resfile::read"),
                                                ::tracing_core::field::FieldSet::new(&["message",
                                                                {
                                                                    const NAME:
                                                                        ::tracing::__macro_support::FieldName<{
                                                                            ::tracing::__macro_support::FieldName::len("io_size")
                                                                        }> =
                                                                        ::tracing::__macro_support::FieldName::new("io_size");
                                                                    NAME.as_str()
                                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                                ::tracing::metadata::Kind::EVENT)
                                        };
                                    ::tracing::callsite::DefaultCallsite::new(&META)
                                };
                            let enabled =
                                ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                                        ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::LevelFilter::current() &&
                                    {
                                        let interest = __CALLSITE.interest();
                                        !interest.is_never() &&
                                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                                interest)
                                    };
                            if enabled {
                                (|value_set: ::tracing::field::ValueSet|
                                            {
                                                let meta = __CALLSITE.metadata();
                                                ::tracing::Event::dispatch(meta, &value_set);
                                                ;
                                            })({
                                        #[allow(unused_imports)]
                                        use ::tracing::field::{debug, display, Value};
                                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("read resource file")
                                                                    as &dyn ::tracing::field::Value)),
                                                        (::tracing::__macro_support::Option::Some(&io_size as
                                                                    &dyn ::tracing::field::Value))])
                                    });
                            } else { ; }
                        };
                        Ok(result)
                    }
                })()
    {
        #[allow(clippy :: unit_arg)]
        Ok(x) => Ok(x),
    Err(e) => {
        {
            use ::tracing::__macro_support::Callsite as _;
            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                {
                    static META: ::tracing::Metadata<'static> =
                        {
                            ::tracing_core::metadata::Metadata::new("event src/read.rs:40",
                                "nwnrs_resfile::read", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/read.rs"),
                                ::tracing_core::__macro_support::Option::Some(40u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resfile::read"),
                                ::tracing_core::field::FieldSet::new(&[{
                                                    const NAME:
                                                        ::tracing::__macro_support::FieldName<{
                                                            ::tracing::__macro_support::FieldName::len("error")
                                                        }> =
                                                        ::tracing::__macro_support::FieldName::new("error");
                                                    NAME.as_str()
                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                ::tracing::metadata::Kind::EVENT)
                        };
                    ::tracing::callsite::DefaultCallsite::new(&META)
                };
            let enabled =
                ::tracing::Level::ERROR <=
                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                        ::tracing::Level::ERROR <=
                            ::tracing::level_filters::LevelFilter::current() &&
                    {
                        let interest = __CALLSITE.interest();
                        !interest.is_never() &&
                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                interest)
                    };
            if enabled {
                (|value_set: ::tracing::field::ValueSet|
                            {
                                let meta = __CALLSITE.metadata();
                                ::tracing::Event::dispatch(meta, &value_set);
                                ;
                            })({
                        #[allow(unused_imports)]
                        use ::tracing::field::{debug, display, Value};
                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&::tracing::field::display(&e)
                                                    as &dyn ::tracing::field::Value))])
                    });
            } else { ; }
        };
        Err(e)
    }
}#[instrument(level = "debug", skip_all, err, fields(path = %path.as_ref().display(), resref = %resref))]
41pub fn read_resfile_as(path: impl AsRef<Path>, resref: ResRef) -> ResFileResult<ResFile> {
42    let path = path.as_ref();
43    let metadata = fs::metadata(path)?;
44    if !metadata.is_file() {
45        return Err(ResFileError::msg(format!(
46            "{} is not a regular file",
47            path.display()
48        )));
49    }
50
51    let label = path.display().to_string();
52    let mtime = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
53    let io_size = metadata.len().cast_signed();
54    let path_for_io = path.to_path_buf();
55    let origin_label = label.clone();
56    let spawner = Arc::new(
57        move || -> io::Result<Box<dyn nwnrs_resman::ReadSeek + Send>> {
58            Ok(Box::new(File::open(&path_for_io)?))
59        },
60    );
61
62    let result = ResFile {
63        path:  path.to_path_buf(),
64        label: label.clone(),
65        entry: Res::new_with_spawner(
66            new_res_origin(format!("ResFile:{label}"), origin_label),
67            resref,
68            mtime,
69            spawner,
70            io_size,
71            0,
72            ExoResFileCompressionType::None,
73            None,
74            usize::try_from(io_size.max(0)).unwrap_or(usize::MAX),
75            EMPTY_SECURE_HASH,
76        ),
77    };
78    debug!(io_size, "read resource file");
79    Ok(result)
80}
81
82#[allow(clippy::panic)]
83#[cfg(test)]
84mod tests {
85    use std::{
86        fs,
87        path::PathBuf,
88        time::{SystemTime, UNIX_EPOCH},
89    };
90
91    use nwnrs_resman::{CachePolicy, ResContainer};
92    use nwnrs_resref::{ResRef, ResolvedResRef};
93    use nwnrs_restype::ResType;
94
95    use crate::{read_resfile, read_resfile_as};
96
97    fn unique_test_dir(prefix: &str) -> PathBuf {
98        let nanos = SystemTime::now()
99            .duration_since(UNIX_EPOCH)
100            .unwrap_or_default()
101            .as_nanos();
102        std::env::temp_dir().join(format!("nwnrs-rf-{prefix}-{nanos}"))
103    }
104
105    #[test]
106    fn reads_resref_from_filename() {
107        let root = unique_test_dir("auto");
108        if let Err(error) = fs::create_dir_all(&root) {
109            panic!("create root: {error}");
110        }
111        let path = root.join("alpha.utc");
112        if let Err(error) = fs::write(&path, b"payload") {
113            panic!("write file: {error}");
114        }
115
116        let resfile = match read_resfile(&path) {
117            Ok(value) => value,
118            Err(error) => panic!("read resfile: {error}"),
119        };
120        let filename = match path.file_name().and_then(|value| value.to_str()) {
121            Some(value) => value,
122            None => panic!("filename should be valid utf-8"),
123        };
124        let rr = match ResolvedResRef::from_filename(filename) {
125            Ok(value) => value,
126            Err(error) => panic!("resolve rr: {error}"),
127        };
128        assert!(resfile.contains(rr.base()));
129        let bytes = match resfile.res().read_all(CachePolicy::Bypass) {
130            Ok(value) => value,
131            Err(error) => panic!("read payload: {error}"),
132        };
133        assert_eq!(bytes, b"payload".to_vec());
134    }
135
136    #[test]
137    fn supports_explicit_resref_override() {
138        let root = unique_test_dir("override");
139        if let Err(error) = fs::create_dir_all(&root) {
140            panic!("create root: {error}");
141        }
142        let path = root.join("payload.bin");
143        if let Err(error) = fs::write(&path, b"payload") {
144            panic!("write file: {error}");
145        }
146        let rr = match ResRef::new("custom", ResType(2027)) {
147            Ok(value) => value,
148            Err(error) => panic!("custom rr: {error}"),
149        };
150
151        let resfile = match read_resfile_as(&path, rr.clone()) {
152            Ok(value) => value,
153            Err(error) => panic!("read resfile as: {error}"),
154        };
155        assert!(resfile.contains(&rr));
156    }
157}