Skip to main content

nwnrs_resman/
types.rs

1use std::{
2    fmt,
3    io::{self, Read, Seek, SeekFrom},
4    sync::{Arc, Mutex, MutexGuard},
5    time::SystemTime,
6};
7
8use nwnrs_checksums::prelude::*;
9use nwnrs_compressedbuf::prelude::*;
10use nwnrs_exo::prelude::*;
11use nwnrs_resref::prelude::*;
12use tracing::instrument;
13
14/// Maximum payload size that [`Res::read_all`] will retain in the per-resource
15/// cache.
16pub const MEMORY_CACHE_THRESHOLD: usize = 1024 * 1024;
17
18/// Convenience trait alias for readable, seekable streams.
19pub trait ReadSeek: Read + Seek {}
20impl<T: Read + Seek> ReadSeek for T {}
21
22/// Shared stream handle used by stream-backed [`Res`] values.
23pub type SharedReadSeek = Arc<Mutex<Box<dyn ReadSeek + Send>>>;
24/// Factory for creating a fresh readable, seekable stream on demand.
25pub type ResIoSpawner =
26    Arc<dyn Fn() -> io::Result<Box<dyn ReadSeek + Send>> + Send + Sync + 'static>;
27
28#[derive(#[automatically_derived]
impl ::core::fmt::Debug for CachePolicy {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f,
            match self {
                CachePolicy::Use => "Use",
                CachePolicy::Bypass => "Bypass",
            })
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for CachePolicy {
    #[inline]
    fn clone(&self) -> CachePolicy { *self }
}Clone, #[automatically_derived]
impl ::core::marker::Copy for CachePolicy { }Copy, #[automatically_derived]
impl ::core::cmp::PartialEq for CachePolicy {
    #[inline]
    fn eq(&self, other: &CachePolicy) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for CachePolicy {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {}
}Eq, #[automatically_derived]
impl ::core::hash::Hash for CachePolicy {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        ::core::hash::Hash::hash(&__self_discr, state)
    }
}Hash)]
29/// Cache behavior for resource reads and cache-aware loaders.
30pub enum CachePolicy {
31    /// Use and populate any cache involved in the operation.
32    Use,
33    /// Bypass and do not populate any cache involved in the operation.
34    Bypass,
35}
36
37impl CachePolicy {
38    /// Returns `true` when caches should be consulted and populated.
39    #[must_use]
40    pub const fn uses_cache(self) -> bool {
41        #[allow(non_exhaustive_omitted_patterns)] match self {
    Self::Use => true,
    _ => false,
}matches!(self, Self::Use)
42    }
43}
44
45#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ResManError {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            ResManError::Io(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Io",
                    &__self_0),
            ResManError::CompressedBuf(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "CompressedBuf", &__self_0),
            ResManError::Message(__self_0) =>
                ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                    "Message", &__self_0),
        }
    }
}Debug)]
46/// Errors returned by resource-container and resource-manager operations.
47pub enum ResManError {
48    /// An underlying IO operation failed.
49    Io(io::Error),
50    /// A compressed EXO payload could not be decoded.
51    CompressedBuf(CompressedBufError),
52    /// The requested resource could not be resolved or interpreted.
53    Message(String),
54}
55
56impl ResManError {
57    pub(crate) fn msg(message: impl Into<String>) -> Self {
58        Self::Message(message.into())
59    }
60}
61
62impl fmt::Display for ResManError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::Io(error) => error.fmt(f),
66            Self::CompressedBuf(error) => error.fmt(f),
67            Self::Message(message) => f.write_str(message),
68        }
69    }
70}
71
72impl std::error::Error for ResManError {}
73
74impl From<io::Error> for ResManError {
75    fn from(value: io::Error) -> Self {
76        Self::Io(value)
77    }
78}
79
80impl From<CompressedBufError> for ResManError {
81    fn from(value: CompressedBufError) -> Self {
82        Self::CompressedBuf(value)
83    }
84}
85
86/// Result type for resource-manager operations.
87pub type ResManResult<T> = Result<T, ResManError>;
88
89#[derive(#[automatically_derived]
impl ::core::fmt::Debug for ResOrigin {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_struct_field2_finish(f, "ResOrigin",
            "container", &self.container, "label", &&self.label)
    }
}Debug, #[automatically_derived]
impl ::core::clone::Clone for ResOrigin {
    #[inline]
    fn clone(&self) -> ResOrigin {
        ResOrigin {
            container: ::core::clone::Clone::clone(&self.container),
            label: ::core::clone::Clone::clone(&self.label),
        }
    }
}Clone, #[automatically_derived]
impl ::core::cmp::PartialEq for ResOrigin {
    #[inline]
    fn eq(&self, other: &ResOrigin) -> bool {
        self.container == other.container && self.label == other.label
    }
}PartialEq, #[automatically_derived]
impl ::core::cmp::Eq for ResOrigin {
    #[inline]
    #[doc(hidden)]
    #[coverage(off)]
    fn assert_fields_are_eq(&self) {
        let _: ::core::cmp::AssertParamIsEq<String>;
    }
}Eq)]
90/// Human-readable origin information for a [`Res`].
91///
92/// The origin is used for error messages and debug output rather than for
93/// identity.
94pub struct ResOrigin {
95    container: String,
96    label:     String,
97}
98
99impl ResOrigin {
100    /// Creates a new origin description.
101    pub fn new(container: impl Into<String>, label: impl Into<String>) -> Self {
102        Self {
103            container: container.into(),
104            label:     label.into(),
105        }
106    }
107
108    /// Returns the high-level container name.
109    #[must_use]
110    pub fn container(&self) -> &str {
111        &self.container
112    }
113
114    /// Returns the container-local label for the resource.
115    #[must_use]
116    pub fn label(&self) -> &str {
117        &self.label
118    }
119}
120
121impl fmt::Display for ResOrigin {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        if self.label.is_empty() {
124            f.write_str(&self.container)
125        } else {
126            f.write_fmt(format_args!("{0}({1})", self.container, self.label))write!(f, "{}({})", self.container, self.label)
127        }
128    }
129}
130
131pub(crate) enum ResBacking {
132    Shared(SharedReadSeek),
133    Spawned(ResIoSpawner),
134}
135
136pub(crate) struct ResMutableState {
137    pub cached: bool,
138    pub cache:  Vec<u8>,
139    pub sha1:   SecureHash,
140}
141
142pub(crate) struct ResInner {
143    pub mtime:                    SystemTime,
144    pub io_offset:                u64,
145    pub io_size:                  i64,
146    pub resref:                   ResRef,
147    pub compression:              ExoResFileCompressionType,
148    pub compressed_buf_algorithm: Option<Algorithm>,
149    pub uncompressed_size:        usize,
150    pub origin:                   ResOrigin,
151    pub backing:                  ResBacking,
152    pub state:                    Mutex<ResMutableState>,
153}
154
155#[derive(#[automatically_derived]
impl ::core::clone::Clone for Res {
    #[inline]
    fn clone(&self) -> Res {
        Res { inner: ::core::clone::Clone::clone(&self.inner) }
    }
}Clone)]
156/// A lazily readable NWN resource payload.
157///
158/// A `Res` remembers where a payload lives, how large it is, whether it must be
159/// decompressed, and how to reopen or share the underlying stream. Cloning a
160/// `Res` is cheap.
161pub struct Res {
162    pub(crate) inner: Arc<ResInner>,
163}
164
165impl fmt::Debug for Res {
166    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
167        f.debug_struct("Res")
168            .field("resref", &self.resref())
169            .field("origin", &self.origin())
170            .field("io_offset", &self.io_offset())
171            .field("io_size", &self.io_size())
172            .field("compression", &self.compression_algorithm())
173            .finish()
174    }
175}
176
177impl fmt::Display for Res {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.write_fmt(format_args!("{0}@{1}", self.resref(), self.origin()))write!(f, "{}@{}", self.resref(), self.origin())
180    }
181}
182
183impl Res {
184    #[allow(clippy::too_many_arguments)]
185    /// Creates a resource backed by a shared stream handle.
186    pub fn new_with_stream(
187        origin: ResOrigin,
188        resref: ResRef,
189        mtime: SystemTime,
190        io: SharedReadSeek,
191        io_size: i64,
192        io_offset: u64,
193        compression: ExoResFileCompressionType,
194        compressed_buf_algorithm: Option<Algorithm>,
195        uncompressed_size: usize,
196        sha1: SecureHash,
197    ) -> Self {
198        Self::new(
199            origin,
200            resref,
201            mtime,
202            ResBacking::Shared(io),
203            io_size,
204            io_offset,
205            compression,
206            compressed_buf_algorithm,
207            uncompressed_size,
208            sha1,
209        )
210    }
211
212    #[allow(clippy::too_many_arguments)]
213    /// Creates a resource backed by a stream factory.
214    ///
215    /// This is useful when a caller wants each read to operate on a fresh
216    /// stream instead of a shared locked handle.
217    pub fn new_with_spawner(
218        origin: ResOrigin,
219        resref: ResRef,
220        mtime: SystemTime,
221        io_spawner: ResIoSpawner,
222        io_size: i64,
223        io_offset: u64,
224        compression: ExoResFileCompressionType,
225        compressed_buf_algorithm: Option<Algorithm>,
226        uncompressed_size: usize,
227        sha1: SecureHash,
228    ) -> Self {
229        Self::new(
230            origin,
231            resref,
232            mtime,
233            ResBacking::Spawned(io_spawner),
234            io_size,
235            io_offset,
236            compression,
237            compressed_buf_algorithm,
238            uncompressed_size,
239            sha1,
240        )
241    }
242
243    #[allow(clippy::too_many_arguments)]
244    fn new(
245        origin: ResOrigin,
246        resref: ResRef,
247        mtime: SystemTime,
248        backing: ResBacking,
249        io_size: i64,
250        io_offset: u64,
251        compression: ExoResFileCompressionType,
252        compressed_buf_algorithm: Option<Algorithm>,
253        uncompressed_size: usize,
254        sha1: SecureHash,
255    ) -> Self {
256        let effective_uncompressed =
257            if compression == ExoResFileCompressionType::None && uncompressed_size == 0 {
258                usize::try_from(io_size.max(0)).unwrap_or(usize::MAX)
259            } else {
260                uncompressed_size
261            };
262
263        Self {
264            inner: Arc::new(ResInner {
265                mtime,
266                io_offset,
267                io_size,
268                resref,
269                compression,
270                compressed_buf_algorithm,
271                uncompressed_size: effective_uncompressed,
272                origin,
273                backing,
274                state: Mutex::new(ResMutableState {
275                    cached: false,
276                    cache: Vec::new(),
277                    sha1,
278                }),
279            }),
280        }
281    }
282
283    /// Returns the resource reference identifying this payload.
284    #[must_use]
285    pub fn resref(&self) -> ResRef {
286        self.inner.resref.clone()
287    }
288
289    /// Returns the modification time recorded for this resource.
290    #[must_use]
291    pub fn mtime(&self) -> SystemTime {
292        self.inner.mtime
293    }
294
295    /// Returns the byte offset of the stored payload inside the backing stream.
296    #[must_use]
297    pub fn io_offset(&self) -> u64 {
298        self.inner.io_offset
299    }
300
301    /// Returns the stored payload size in bytes.
302    ///
303    /// A negative value indicates that the payload should be read until
304    /// end-of-stream.
305    #[must_use]
306    pub fn io_size(&self) -> i64 {
307        self.inner.io_size
308    }
309
310    /// Returns whether the decoded payload is currently cached in memory.
311    #[must_use]
312    pub fn cached(&self) -> bool {
313        self.lock_state().is_ok_and(|state| state.cached)
314    }
315
316    /// Returns the expected size after decompression.
317    #[must_use]
318    pub fn uncompressed_size(&self) -> usize {
319        self.inner.uncompressed_size
320    }
321
322    /// Returns the EXO compression marker for this payload.
323    #[must_use]
324    pub fn compression_algorithm(&self) -> ExoResFileCompressionType {
325        self.inner.compression
326    }
327
328    /// Returns the compressed-buffer algorithm stored in an ERF payload, when
329    /// known.
330    #[must_use]
331    pub fn compressed_buf_algorithm(&self) -> Option<Algorithm> {
332        self.inner.compressed_buf_algorithm
333    }
334
335    /// Returns the descriptive origin for this payload.
336    #[must_use]
337    pub fn origin(&self) -> ResOrigin {
338        self.inner.origin.clone()
339    }
340
341    /// Returns `true` when this resource is backed by a shared stream handle.
342    #[must_use]
343    pub fn io_owned(&self) -> bool {
344        #[allow(non_exhaustive_omitted_patterns)] match self.inner.backing {
    ResBacking::Shared(_) => true,
    _ => false,
}matches!(self.inner.backing, ResBacking::Shared(_))
345    }
346
347    /// Seeks the underlying stream to the start of this payload.
348    ///
349    /// This is mainly useful for callers performing manual reads with
350    /// [`with_stream`](Self::with_stream).
351    ///
352    /// # Errors
353    ///
354    /// Returns [`ResManError`] if the stream cannot be locked or the seek
355    /// fails.
356    #[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: ResManResult<()> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        self.with_stream(|stream|
                                {
                                    stream.seek(SeekFrom::Start(self.inner.io_offset))?;
                                    Ok(())
                                })
                    }
                })()
    {
        #[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/types.rs:356",
                                "nwnrs_resman::types", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/types.rs"),
                                ::tracing_core::__macro_support::Option::Some(356u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resman::types"),
                                ::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(resref = %self.inner.resref))]
357    pub fn seek(&self) -> ResManResult<()> {
358        self.with_stream(|stream| {
359            stream.seek(SeekFrom::Start(self.inner.io_offset))?;
360            Ok(())
361        })
362    }
363
364    /// Reads the full payload, decompressing it when required.
365    ///
366    /// When [`CachePolicy::Use`] is selected, small decoded payloads are
367    /// retained in memory.
368    ///
369    /// # Errors
370    ///
371    /// Returns [`ResManError`] if the stream cannot be read or decompression
372    /// fails.
373    #[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: ResManResult<Vec<u8>> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        if cache_policy.uses_cache() {
                            let state = self.lock_state()?;
                            if state.cached { return Ok(state.cache.clone()); }
                        }
                        let raw = self.read_raw()?;
                        let data =
                            match self.inner.compression {
                                ExoResFileCompressionType::None => raw,
                                ExoResFileCompressionType::CompressedBuf => {
                                    decompress_bytes(&raw, EXO_RES_FILE_COMPRESSED_BUF_MAGIC)?
                                }
                            };
                        if cache_policy.uses_cache() &&
                                data.len() < MEMORY_CACHE_THRESHOLD {
                            let mut state = self.lock_state()?;
                            state.cached = true;
                            state.cache.clone_from(&data);
                        }
                        Ok(data)
                    }
                })()
    {
        #[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/types.rs:373",
                                "nwnrs_resman::types", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/types.rs"),
                                ::tracing_core::__macro_support::Option::Some(373u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resman::types"),
                                ::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(resref = %self.inner.resref, cache_policy = ?cache_policy))]
374    pub fn read_all(&self, cache_policy: CachePolicy) -> ResManResult<Vec<u8>> {
375        if cache_policy.uses_cache() {
376            let state = self.lock_state()?;
377            if state.cached {
378                return Ok(state.cache.clone());
379            }
380        }
381
382        let raw = self.read_raw()?;
383        let data = match self.inner.compression {
384            ExoResFileCompressionType::None => raw,
385            ExoResFileCompressionType::CompressedBuf => {
386                decompress_bytes(&raw, EXO_RES_FILE_COMPRESSED_BUF_MAGIC)?
387            }
388        };
389
390        if cache_policy.uses_cache() && data.len() < MEMORY_CACHE_THRESHOLD {
391            let mut state = self.lock_state()?;
392            state.cached = true;
393            state.cache.clone_from(&data);
394        }
395
396        Ok(data)
397    }
398
399    /// Returns the SHA-1 digest for the decoded payload.
400    ///
401    /// If the digest was not provided by the container, it is computed lazily
402    /// and cached.
403    ///
404    /// # Errors
405    ///
406    /// Returns [`ResManError`] if the payload cannot be read or the state lock
407    /// is poisoned.
408    #[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: ResManResult<SecureHash> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        {
                            let state = self.lock_state()?;
                            if state.sha1 != EMPTY_SECURE_HASH {
                                return Ok(state.sha1);
                            }
                        }
                        let digest = secure_hash(self.read_all(CachePolicy::Use)?);
                        let mut state = self.lock_state()?;
                        state.sha1 = digest;
                        Ok(digest)
                    }
                })()
    {
        #[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/types.rs:408",
                                "nwnrs_resman::types", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/types.rs"),
                                ::tracing_core::__macro_support::Option::Some(408u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resman::types"),
                                ::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(resref = %self.inner.resref))]
409    pub fn sha1(&self) -> ResManResult<SecureHash> {
410        {
411            let state = self.lock_state()?;
412            if state.sha1 != EMPTY_SECURE_HASH {
413                return Ok(state.sha1);
414            }
415        }
416
417        let digest = secure_hash(self.read_all(CachePolicy::Use)?);
418        let mut state = self.lock_state()?;
419        state.sha1 = digest;
420        Ok(digest)
421    }
422
423    /// Runs `op` against the underlying stream.
424    ///
425    /// Shared-stream resources lock the stream for the duration of the
426    /// callback. Spawned resources create a fresh stream for the call.
427    ///
428    /// # Errors
429    ///
430    /// Returns [`ResManError`] if the stream cannot be locked, spawned, or if
431    /// `op` fails.
432    #[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: ResManResult<T> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        match &self.inner.backing {
                            ResBacking::Shared(stream) => {
                                let mut stream =
                                    stream.lock().map_err(|error|
                                                {
                                                    ResManError::msg(::alloc::__export::must_use({
                                                                ::alloc::fmt::format(format_args!("shared res stream lock poisoned: {0}",
                                                                        error))
                                                            }))
                                                })?;
                                op(stream.as_mut())
                            }
                            ResBacking::Spawned(spawner) => {
                                let mut stream = spawner()?;
                                op(stream.as_mut())
                            }
                        }
                    }
                })()
    {
        #[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/types.rs:432",
                                "nwnrs_resman::types", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/types.rs"),
                                ::tracing_core::__macro_support::Option::Some(432u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_resman::types"),
                                ::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(resref = %self.inner.resref))]
433    pub fn with_stream<T, F>(&self, op: F) -> ResManResult<T>
434    where
435        F: FnOnce(&mut dyn ReadSeek) -> ResManResult<T>,
436    {
437        match &self.inner.backing {
438            ResBacking::Shared(stream) => {
439                let mut stream = stream.lock().map_err(|error| {
440                    ResManError::msg(format!("shared res stream lock poisoned: {error}"))
441                })?;
442                op(stream.as_mut())
443            }
444            ResBacking::Spawned(spawner) => {
445                let mut stream = spawner()?;
446                op(stream.as_mut())
447            }
448        }
449    }
450
451    fn read_raw(&self) -> ResManResult<Vec<u8>> {
452        self.with_stream(|stream| {
453            stream.seek(SeekFrom::Start(self.inner.io_offset))?;
454            if self.inner.io_size < 0 {
455                let mut data = Vec::new();
456                stream.read_to_end(&mut data)?;
457                Ok(data)
458            } else {
459                let data_len = usize::try_from(self.inner.io_size).map_err(|error| {
460                    ResManError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("resource size out of range: {0}",
                error))
    })format!("resource size out of range: {error}"))
461                })?;
462                let mut data = ::alloc::vec::from_elem(0_u8, data_len)vec![0_u8; data_len];
463                stream.read_exact(&mut data)?;
464                Ok(data)
465            }
466        })
467    }
468
469    fn lock_state(&self) -> ResManResult<MutexGuard<'_, ResMutableState>> {
470        self.inner
471            .state
472            .lock()
473            .map_err(|error| ResManError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("res state lock poisoned: {0}",
                error))
    })format!("res state lock poisoned: {error}")))
474    }
475}
476
477/// Trait implemented by all resource containers in the workspace.
478pub trait ResContainer: fmt::Display + Send + Sync {
479    /// Returns whether the container can resolve `rr`.
480    fn contains(&self, rr: &ResRef) -> bool;
481    /// Returns the resource identified by `rr` or an error when it is absent.
482    ///
483    /// # Errors
484    ///
485    /// Returns [`ResManError`] if the resource is not present or cannot be
486    /// loaded.
487    fn demand(&self, rr: &ResRef) -> ResManResult<Res>;
488    /// Returns the number of resources exposed by the container.
489    fn count(&self) -> usize;
490    /// Returns every resource reference exposed by the container.
491    fn contents(&self) -> Vec<ResRef>;
492}
493
494/// Convenience constructor for [`ResOrigin`].
495pub fn new_res_origin(container: impl Into<String>, label: impl Into<String>) -> ResOrigin {
496    ResOrigin::new(container, label)
497}
498
499/// Wraps a stream in the shared-handle type used by stream-backed resources.
500pub fn shared_stream<T>(stream: T) -> SharedReadSeek
501where
502    T: ReadSeek + Send + 'static,
503{
504    Arc::new(Mutex::new(Box::new(stream)))
505}