Skip to main content

nwnrs_erf/
io.rs

1use std::{
2    collections::{BTreeMap, hash_map::RandomState},
3    fs::File,
4    io::{self, Read, Seek, SeekFrom, Write},
5    path::Path,
6    time::SystemTime,
7};
8
9use nwnrs_checksums::prelude::*;
10use nwnrs_compressedbuf::prelude::*;
11use nwnrs_encoding::prelude::*;
12use nwnrs_exo::prelude::*;
13use nwnrs_io::prelude::*;
14use nwnrs_resman::prelude::*;
15use nwnrs_resref::prelude::*;
16use tracing::{debug, instrument};
17
18use crate::{
19    Erf, ErfError, ErfResMeta, ErfResult, ErfVersion, ErfWriteOptions, HEADER_SIZE, VALID_ERF_TYPES,
20};
21
22/// Reads an ERF-family archive from a seekable reader.
23///
24/// The returned [`Erf`] contains lazily readable [`nwnrs_resman::Res`] entries
25/// backed by the supplied stream.
26///
27/// # Errors
28///
29/// Returns [`ErfError`] if the data cannot be read or does not conform to an
30/// ERF-family format.
31#[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: ErfResult<Erf> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    { read_erf_shared(shared_stream(reader), filename.into()) }
                })()
    {
        #[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/io.rs:31",
                                "nwnrs_erf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(31u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                                ::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)]
32pub fn read_erf<R>(reader: R, filename: impl Into<String>) -> ErfResult<Erf>
33where
34    R: Read + Seek + Send + 'static,
35{
36    read_erf_shared(shared_stream(reader), filename.into())
37}
38
39/// Opens a file from disk and reads it as an ERF-family archive.
40///
41/// # Errors
42///
43/// Returns [`ErfError`] if the file cannot be opened or parsed.
44#[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: ErfResult<Erf> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let path = path.as_ref();
                        let file = File::open(path)?;
                        read_erf(file, path.display().to_string())
                    }
                })()
    {
        #[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/io.rs:44",
                                "nwnrs_erf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(44u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                                ::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()))]
45pub fn read_erf_from_file(path: impl AsRef<Path>) -> ErfResult<Erf> {
46    let path = path.as_ref();
47    let file = File::open(path)?;
48    read_erf(file, path.display().to_string())
49}
50
51/// Reads an ERF-family archive from a shared stream handle.
52///
53/// This is the most direct constructor when the caller already manages stream
54/// sharing.
55///
56/// # Errors
57///
58/// Returns [`ErfError`] if the data cannot be read or does not conform to an
59/// ERF-family format.
60#[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: ErfResult<Erf> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let mut io =
                            stream.lock().map_err(|error|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("erf stream lock poisoned: {0}",
                                                            error))
                                                })))?;
                        io.seek(SeekFrom::Start(0))?;
                        let file_type = read_fixed_string(io.as_mut(), 4)?;
                        let file_version =
                            match read_fixed_string(io.as_mut(), 4)?.as_str() {
                                "V1.0" => ErfVersion::V1,
                                "E1.0" => ErfVersion::E1,
                                other =>
                                    return Err(ErfError::msg(::alloc::__export::must_use({
                                                        ::alloc::fmt::format(format_args!("unsupported erf version: {0}",
                                                                other))
                                                    }))),
                            };
                        let loc_str_count =
                            usize::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF loc string count is negative: {0}",
                                                            e))
                                                })))?;
                        let loc_string_size =
                            u64::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF loc string size is negative: {0}",
                                                            e))
                                                })))?;
                        let entry_count =
                            usize::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF entry count is negative: {0}",
                                                            e))
                                                })))?;
                        let offset_to_loc_str =
                            u64::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF loc string offset is negative: {0}",
                                                            e))
                                                })))?;
                        let offset_to_key_list =
                            u64::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF key list offset is negative: {0}",
                                                            e))
                                                })))?;
                        let offset_to_resource_list =
                            u64::try_from(read_i32(io.as_mut())?).map_err(|e|
                                        ErfError::msg(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("ERF resource list offset is negative: {0}",
                                                            e))
                                                })))?;
                        let build_year = read_i32(io.as_mut())?;
                        let build_day = read_i32(io.as_mut())?;
                        let str_ref = read_i32(io.as_mut())?;
                        let oid =
                            match file_version {
                                ErfVersion::V1 => { io.seek(SeekFrom::Current(116))?; None }
                                ErfVersion::E1 => {
                                    let oid = read_fixed_string(io.as_mut(), 24)?;
                                    io.seek(SeekFrom::Current(92))?;
                                    Some(normalize_oid(&oid)?)
                                }
                            };
                        let mut loc_strings = BTreeMap::new();
                        io.seek(SeekFrom::Start(offset_to_loc_str))?;
                        for _ in 0..loc_str_count {
                            let id = read_i32(io.as_mut())?;
                            let len =
                                usize::try_from(read_i32(io.as_mut())?).map_err(|e|
                                            ErfError::msg(::alloc::__export::must_use({
                                                        ::alloc::fmt::format(format_args!("ERF loc string length is negative: {0}",
                                                                e))
                                                    })))?;
                            let bytes = read_bytes_or_err(io.as_mut(), len)?;
                            loc_strings.insert(id, from_nwnrs_encoding(&bytes)?);
                        }
                        let _is_known_erf_type =
                            VALID_ERF_TYPES.contains(&file_type.as_str());
                        let key_entry_size =
                            match file_version {
                                ErfVersion::V1 => 24_u64,
                                ErfVersion::E1 => 44_u64,
                            };
                        let resource_entry_size =
                            match file_version {
                                ErfVersion::V1 => 8_u64,
                                ErfVersion::E1 => 16_u64,
                            };
                        let expected_resource_list_offset =
                            offset_to_key_list +
                                key_entry_size *
                                    u64::try_from(entry_count).map_err(|_error|
                                                ErfError::msg("ERF entry count exceeds 64-bit range"))?;
                        let file_len = io.seek(SeekFrom::End(0))?;
                        let resource_list_size =
                            resource_entry_size.checked_mul(u64::try_from(entry_count).map_err(|_error|
                                                    ErfError::msg("ERF entry count exceeds 64-bit range"))?).ok_or_else(||
                                        ErfError::msg("ERF resource list size overflow"))?;
                        expect(offset_to_resource_list.checked_add(resource_list_size).is_some_and(|end|
                                        end <= file_len), "ERF resource list offset out of range")?;
                        io.seek(SeekFrom::Start(offset_to_resource_list))?;
                        let mut resources = Vec::with_capacity(entry_count);
                        for _ in 0..entry_count {
                            let offset = u64::from(read_u32(io.as_mut())?);
                            let disk_size = read_u32(io.as_mut())? as usize;
                            let (compression, uncompressed_size) =
                                match file_version {
                                    ErfVersion::V1 =>
                                        (ExoResFileCompressionType::None, disk_size),
                                    ErfVersion::E1 => {
                                        let compression =
                                            ExoResFileCompressionType::from_u32(read_u32(io.as_mut())?).ok_or_else(||
                                                        ErfError::msg("invalid erf compression type"))?;
                                        let uncompressed_size = read_u32(io.as_mut())? as usize;
                                        (compression, uncompressed_size)
                                    }
                                };
                            let disk_size_u64 =
                                u64::try_from(disk_size).map_err(|_error|
                                            ErfError::msg("ERF resource size exceeds 64-bit range"))?;
                            expect(offset != 0,
                                    "ERF resource offset must be non-zero")?;
                            expect(offset.checked_add(disk_size_u64).is_some_and(|end|
                                            end <= file_len), "ERF resource payload out of range")?;
                            resources.push(ErfResMeta {
                                    offset,
                                    disk_size,
                                    uncompressed_size,
                                    compression,
                                });
                        }
                        let origin_container =
                            ::alloc::__export::must_use({
                                    ::alloc::fmt::format(format_args!("Erf:{0}", filename))
                                });
                        let mut entries:
                                indexmap::IndexMap<ResRef, Res, RandomState> =
                            indexmap::IndexMap::with_hasher(RandomState::new());
                        io.seek(SeekFrom::Start(offset_to_key_list))?;
                        for (index, meta) in
                            resources.iter().enumerate().take(entry_count) {
                            let res_ref_raw =
                                trim_trailing_nuls(&read_bytes_or_err(io.as_mut(), 16)?);
                            let _id = read_i32(io.as_mut())?;
                            let res_type = read_u16(io.as_mut())?;
                            io.seek(SeekFrom::Current(2))?;
                            if res_type == u16::MAX { continue; }
                            let sha1 =
                                if file_version == ErfVersion::E1 {
                                    read_secure_hash(io.as_mut())?
                                } else { EMPTY_SECURE_HASH };
                            let mut rr =
                                match ResRef::new(res_ref_raw,
                                        nwnrs_restype::ResType(res_type)) {
                                    Ok(rr) => rr,
                                    Err(_) =>
                                        ResRef::new(::alloc::__export::must_use({
                                                        ::alloc::fmt::format(format_args!("invalid_{0}", index))
                                                    }), nwnrs_restype::ResType(res_type))?,
                                };
                            if let Some(existing) = entries.get(&rr) {
                                if existing.io_offset() == meta.offset &&
                                        existing.io_size() ==
                                            i64::try_from(meta.disk_size).map_err(|e|
                                                        {
                                                            ErfError::msg(::alloc::__export::must_use({
                                                                        ::alloc::fmt::format(format_args!("ERF resource size exceeds i64 range: {0}",
                                                                                e))
                                                                    }))
                                                        })? {
                                    continue;
                                }
                                rr =
                                    ResRef::new(::alloc::__export::must_use({
                                                    ::alloc::fmt::format(format_args!("__erfdup__{0}", index))
                                                }), nwnrs_restype::ResType(res_type))?;
                            }
                            let res =
                                Res::new_with_stream(new_res_origin(origin_container.clone(),
                                        ::alloc::__export::must_use({
                                                ::alloc::fmt::format(format_args!("{0}: {1}", filename, rr))
                                            })), rr.clone(), SystemTime::UNIX_EPOCH, stream.clone(),
                                    i64::try_from(meta.disk_size).map_err(|e|
                                                ErfError::msg(::alloc::__export::must_use({
                                                            ::alloc::fmt::format(format_args!("ERF resource size exceeds i64 range: {0}",
                                                                    e))
                                                        })))?, meta.offset, meta.compression,
                                    read_compressed_buf_algorithm(io.as_mut(), meta)?,
                                    meta.uncompressed_size, sha1);
                            entries.insert(rr, res);
                        }
                        drop(io);
                        #[allow(clippy::no_effect_underscore_binding)]
                        let _has_oversized_loc_table =
                            offset_to_loc_str + loc_string_size > HEADER_SIZE &&
                                entry_count == 0;
                        let erf =
                            Erf {
                                mtime: SystemTime::UNIX_EPOCH,
                                file_type,
                                file_version,
                                filename,
                                build_year,
                                build_day,
                                str_ref,
                                loc_strings,
                                entries,
                                oid,
                                resource_list_padding: offset_to_resource_list.saturating_sub(expected_resource_list_offset),
                            };
                        {
                            use ::tracing::__macro_support::Callsite as _;
                            static __CALLSITE: ::tracing::callsite::DefaultCallsite =
                                {
                                    static META: ::tracing::Metadata<'static> =
                                        {
                                            ::tracing_core::metadata::Metadata::new("event src/io.rs:246",
                                                "nwnrs_erf::io", ::tracing::Level::DEBUG,
                                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                                ::tracing_core::__macro_support::Option::Some(246u32),
                                                ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                                                ::tracing_core::field::FieldSet::new(&["message",
                                                                {
                                                                    const NAME:
                                                                        ::tracing::__macro_support::FieldName<{
                                                                            ::tracing::__macro_support::FieldName::len("entry_count")
                                                                        }> =
                                                                        ::tracing::__macro_support::FieldName::new("entry_count");
                                                                    NAME.as_str()
                                                                },
                                                                {
                                                                    const NAME:
                                                                        ::tracing::__macro_support::FieldName<{
                                                                            ::tracing::__macro_support::FieldName::len("file_type")
                                                                        }> =
                                                                        ::tracing::__macro_support::FieldName::new("file_type");
                                                                    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 erf archive")
                                                                    as &dyn ::tracing::field::Value)),
                                                        (::tracing::__macro_support::Option::Some(&erf.entries.len()
                                                                    as &dyn ::tracing::field::Value)),
                                                        (::tracing::__macro_support::Option::Some(&::tracing::field::display(&erf.file_type)
                                                                    as &dyn ::tracing::field::Value))])
                                    });
                            } else { ; }
                        };
                        Ok(erf)
                    }
                })()
    {
        #[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/io.rs:60",
                                "nwnrs_erf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(60u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                                ::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 = %filename))]
61pub fn read_erf_shared(stream: SharedReadSeek, filename: String) -> ErfResult<Erf> {
62    let mut io = stream
63        .lock()
64        .map_err(|error| ErfError::msg(format!("erf stream lock poisoned: {error}")))?;
65    io.seek(SeekFrom::Start(0))?;
66
67    let file_type = read_fixed_string(io.as_mut(), 4)?;
68    let file_version = match read_fixed_string(io.as_mut(), 4)?.as_str() {
69        "V1.0" => ErfVersion::V1,
70        "E1.0" => ErfVersion::E1,
71        other => return Err(ErfError::msg(format!("unsupported erf version: {other}"))),
72    };
73
74    let loc_str_count = usize::try_from(read_i32(io.as_mut())?)
75        .map_err(|e| ErfError::msg(format!("ERF loc string count is negative: {e}")))?;
76    let loc_string_size = u64::try_from(read_i32(io.as_mut())?)
77        .map_err(|e| ErfError::msg(format!("ERF loc string size is negative: {e}")))?;
78    let entry_count = usize::try_from(read_i32(io.as_mut())?)
79        .map_err(|e| ErfError::msg(format!("ERF entry count is negative: {e}")))?;
80    let offset_to_loc_str = u64::try_from(read_i32(io.as_mut())?)
81        .map_err(|e| ErfError::msg(format!("ERF loc string offset is negative: {e}")))?;
82    let offset_to_key_list = u64::try_from(read_i32(io.as_mut())?)
83        .map_err(|e| ErfError::msg(format!("ERF key list offset is negative: {e}")))?;
84    let offset_to_resource_list = u64::try_from(read_i32(io.as_mut())?)
85        .map_err(|e| ErfError::msg(format!("ERF resource list offset is negative: {e}")))?;
86    let build_year = read_i32(io.as_mut())?;
87    let build_day = read_i32(io.as_mut())?;
88    let str_ref = read_i32(io.as_mut())?;
89    let oid = match file_version {
90        ErfVersion::V1 => {
91            io.seek(SeekFrom::Current(116))?;
92            None
93        }
94        ErfVersion::E1 => {
95            let oid = read_fixed_string(io.as_mut(), 24)?;
96            io.seek(SeekFrom::Current(92))?;
97            Some(normalize_oid(&oid)?)
98        }
99    };
100
101    let mut loc_strings = BTreeMap::new();
102    io.seek(SeekFrom::Start(offset_to_loc_str))?;
103    for _ in 0..loc_str_count {
104        let id = read_i32(io.as_mut())?;
105        let len = usize::try_from(read_i32(io.as_mut())?)
106            .map_err(|e| ErfError::msg(format!("ERF loc string length is negative: {e}")))?;
107        let bytes = read_bytes_or_err(io.as_mut(), len)?;
108        loc_strings.insert(id, from_nwnrs_encoding(&bytes)?);
109    }
110
111    let _is_known_erf_type = VALID_ERF_TYPES.contains(&file_type.as_str());
112
113    let key_entry_size = match file_version {
114        ErfVersion::V1 => 24_u64,
115        ErfVersion::E1 => 44_u64,
116    };
117    let resource_entry_size = match file_version {
118        ErfVersion::V1 => 8_u64,
119        ErfVersion::E1 => 16_u64,
120    };
121    let expected_resource_list_offset = offset_to_key_list
122        + key_entry_size
123            * u64::try_from(entry_count)
124                .map_err(|_error| ErfError::msg("ERF entry count exceeds 64-bit range"))?;
125    let file_len = io.seek(SeekFrom::End(0))?;
126    let resource_list_size = resource_entry_size
127        .checked_mul(
128            u64::try_from(entry_count)
129                .map_err(|_error| ErfError::msg("ERF entry count exceeds 64-bit range"))?,
130        )
131        .ok_or_else(|| ErfError::msg("ERF resource list size overflow"))?;
132    expect(
133        offset_to_resource_list
134            .checked_add(resource_list_size)
135            .is_some_and(|end| end <= file_len),
136        "ERF resource list offset out of range",
137    )?;
138    io.seek(SeekFrom::Start(offset_to_resource_list))?;
139    let mut resources = Vec::with_capacity(entry_count);
140    for _ in 0..entry_count {
141        let offset = u64::from(read_u32(io.as_mut())?);
142        let disk_size = read_u32(io.as_mut())? as usize;
143        let (compression, uncompressed_size) = match file_version {
144            ErfVersion::V1 => (ExoResFileCompressionType::None, disk_size),
145            ErfVersion::E1 => {
146                let compression = ExoResFileCompressionType::from_u32(read_u32(io.as_mut())?)
147                    .ok_or_else(|| ErfError::msg("invalid erf compression type"))?;
148                let uncompressed_size = read_u32(io.as_mut())? as usize;
149                (compression, uncompressed_size)
150            }
151        };
152        let disk_size_u64 = u64::try_from(disk_size)
153            .map_err(|_error| ErfError::msg("ERF resource size exceeds 64-bit range"))?;
154        expect(offset != 0, "ERF resource offset must be non-zero")?;
155        expect(
156            offset
157                .checked_add(disk_size_u64)
158                .is_some_and(|end| end <= file_len),
159            "ERF resource payload out of range",
160        )?;
161
162        resources.push(ErfResMeta {
163            offset,
164            disk_size,
165            uncompressed_size,
166            compression,
167        });
168    }
169
170    let origin_container = format!("Erf:{filename}");
171    let mut entries: indexmap::IndexMap<ResRef, Res, RandomState> =
172        indexmap::IndexMap::with_hasher(RandomState::new());
173    io.seek(SeekFrom::Start(offset_to_key_list))?;
174    for (index, meta) in resources.iter().enumerate().take(entry_count) {
175        let res_ref_raw = trim_trailing_nuls(&read_bytes_or_err(io.as_mut(), 16)?);
176        let _id = read_i32(io.as_mut())?;
177        let res_type = read_u16(io.as_mut())?;
178        io.seek(SeekFrom::Current(2))?;
179        if res_type == u16::MAX {
180            continue;
181        }
182
183        let sha1 = if file_version == ErfVersion::E1 {
184            read_secure_hash(io.as_mut())?
185        } else {
186            EMPTY_SECURE_HASH
187        };
188
189        let mut rr = match ResRef::new(res_ref_raw, nwnrs_restype::ResType(res_type)) {
190            Ok(rr) => rr,
191            Err(_) => ResRef::new(format!("invalid_{index}"), nwnrs_restype::ResType(res_type))?,
192        };
193
194        if let Some(existing) = entries.get(&rr) {
195            if existing.io_offset() == meta.offset
196                && existing.io_size()
197                    == i64::try_from(meta.disk_size).map_err(|e| {
198                        ErfError::msg(format!("ERF resource size exceeds i64 range: {e}"))
199                    })?
200            {
201                continue;
202            }
203            rr = ResRef::new(
204                format!("__erfdup__{index}"),
205                nwnrs_restype::ResType(res_type),
206            )?;
207        }
208
209        let res = Res::new_with_stream(
210            new_res_origin(origin_container.clone(), format!("{filename}: {rr}")),
211            rr.clone(),
212            SystemTime::UNIX_EPOCH,
213            stream.clone(),
214            i64::try_from(meta.disk_size)
215                .map_err(|e| ErfError::msg(format!("ERF resource size exceeds i64 range: {e}")))?,
216            meta.offset,
217            meta.compression,
218            read_compressed_buf_algorithm(io.as_mut(), meta)?,
219            meta.uncompressed_size,
220            sha1,
221        );
222        entries.insert(rr, res);
223    }
224
225    drop(io);
226
227    // TODO: possibly dead code - value is never read
228    #[allow(clippy::no_effect_underscore_binding)]
229    let _has_oversized_loc_table =
230        offset_to_loc_str + loc_string_size > HEADER_SIZE && entry_count == 0;
231
232    let erf = Erf {
233        mtime: SystemTime::UNIX_EPOCH,
234        file_type,
235        file_version,
236        filename,
237        build_year,
238        build_day,
239        str_ref,
240        loc_strings,
241        entries,
242        oid,
243        resource_list_padding: offset_to_resource_list
244            .saturating_sub(expected_resource_list_offset),
245    };
246    debug!(entry_count = erf.entries.len(), file_type = %erf.file_type, "read erf archive");
247    Ok(erf)
248}
249
250fn read_compressed_buf_algorithm<R: Read + Seek + ?Sized>(
251    io: &mut R,
252    meta: &ErfResMeta,
253) -> ErfResult<Option<Algorithm>> {
254    if meta.compression != ExoResFileCompressionType::CompressedBuf {
255        return Ok(None);
256    }
257
258    let current = io.stream_position()?;
259    io.seek(SeekFrom::Start(meta.offset))?;
260    let _magic = read_u32(io)?;
261    let _version = read_u32(io)?;
262    let algorithm = Algorithm::from_u32(read_u32(io)?)
263        .map_err(|_error| ErfError::msg("invalid compressed buffer algorithm"))?;
264    io.seek(SeekFrom::Start(current))?;
265    Ok(Some(algorithm))
266}
267
268#[allow(clippy::too_many_arguments)]
269/// Writes an ERF-family archive.
270///
271/// `entries` defines the archive order. For each entry, `entry_writer` must
272/// write the raw payload bytes and return the uncompressed byte length together
273/// with the payload SHA-1.
274///
275/// # Errors
276///
277/// Returns [`ErfError`] if the write fails.
278#[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: ErfResult<()> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        write_erf_with_options(writer, file_type, file_version,
                            build_year, build_day, exocomp, compalg, loc_strings,
                            str_ref, entries, erf_oid, ErfWriteOptions::default(),
                            entry_writer, entry_algorithm)
                    }
                })()
    {
        #[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/io.rs:278",
                                "nwnrs_erf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(278u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                                ::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(
279    level = "debug",
280    skip_all,
281    err,
282    fields(file_type, version = ?file_version, entry_count = entries.len())
283)]
284pub fn write_erf<W, F>(
285    writer: &mut W,
286    file_type: &str,
287    file_version: ErfVersion,
288    build_year: u32,
289    build_day: u32,
290    exocomp: ExoResFileCompressionType,
291    compalg: Algorithm,
292    loc_strings: &BTreeMap<i32, String>,
293    str_ref: i32,
294    entries: &[ResRef],
295    erf_oid: Option<&str>,
296    entry_writer: F,
297    entry_algorithm: impl FnMut(&ResRef) -> Algorithm,
298) -> ErfResult<()>
299where
300    W: Write + Seek,
301    F: FnMut(&ResRef, &mut dyn Write) -> ErfResult<(usize, SecureHash)>,
302{
303    write_erf_with_options(
304        writer,
305        file_type,
306        file_version,
307        build_year,
308        build_day,
309        exocomp,
310        compalg,
311        loc_strings,
312        str_ref,
313        entries,
314        erf_oid,
315        ErfWriteOptions::default(),
316        entry_writer,
317        entry_algorithm,
318    )
319}
320
321#[allow(
322    clippy::expect_used,
323    clippy::indexing_slicing,
324    clippy::items_after_test_module,
325    clippy::panic
326)]
327#[cfg(test)]
328mod tests {
329    use std::{collections::BTreeMap, io::Cursor};
330
331    use nwnrs_checksums::prelude::secure_hash;
332    use nwnrs_compressedbuf::prelude::Algorithm;
333    use nwnrs_exo::prelude::ExoResFileCompressionType;
334    use nwnrs_resref::ResolvedResRef;
335
336    use super::{ErfVersion, read_erf, write_erf_with_options};
337    use crate::ErfWriteOptions;
338
339    #[test]
340    fn malformed_erf_resource_list_offset_is_rejected() {
341        let entry = ResolvedResRef::from_filename("test.utc")
342            .expect("resref")
343            .into();
344        let mut encoded = Cursor::new(Vec::new());
345        write_erf_with_options(
346            &mut encoded,
347            "ERF ",
348            ErfVersion::V1,
349            2026,
350            98,
351            ExoResFileCompressionType::None,
352            Algorithm::None,
353            &BTreeMap::new(),
354            -1,
355            &[entry],
356            None,
357            ErfWriteOptions::default(),
358            |_rr, out| {
359                out.write_all(b"abc")?;
360                Ok((3, secure_hash(b"abc")))
361            },
362            |_rr| Algorithm::None,
363        )
364        .expect("encode erf");
365        let mut bytes = encoded.into_inner();
366
367        let resource_list_offset =
368            u32::from_le_bytes(bytes[28..32].try_into().expect("resource list offset"));
369        bytes[28..32].copy_from_slice(&(resource_list_offset + 1).to_le_bytes());
370
371        let error =
372            read_erf(Cursor::new(bytes), "broken.erf".to_string()).expect_err("malformed erf");
373        assert!(
374            error.to_string().contains("offset out of range")
375                || error.to_string().contains("payload out of range")
376                || error.to_string().contains("invalid")
377                || error.to_string().contains("not found"),
378            "unexpected error: {error}"
379        );
380    }
381}
382
383/// Writes an ERF-family archive with explicit preserved-layout options.
384///
385/// # Errors
386///
387/// Returns [`ErfError`] if the write fails.
388#[allow(clippy::too_many_arguments)]
389pub fn write_erf_with_options<W, F>(
390    writer: &mut W,
391    file_type: &str,
392    file_version: ErfVersion,
393    build_year: u32,
394    build_day: u32,
395    exocomp: ExoResFileCompressionType,
396    compalg: Algorithm,
397    loc_strings: &BTreeMap<i32, String>,
398    str_ref: i32,
399    entries: &[ResRef],
400    erf_oid: Option<&str>,
401    options: ErfWriteOptions,
402    entry_writer: F,
403    entry_algorithm: impl FnMut(&ResRef) -> Algorithm,
404) -> ErfResult<()>
405where
406    W: Write + Seek,
407    F: FnMut(&ResRef, &mut dyn Write) -> ErfResult<(usize, SecureHash)>,
408{
409    write_erf_inner(
410        writer,
411        file_type,
412        file_version,
413        build_year,
414        build_day,
415        exocomp,
416        compalg,
417        loc_strings,
418        str_ref,
419        entries,
420        erf_oid,
421        options.resource_list_padding,
422        entry_writer,
423        entry_algorithm,
424    )
425}
426
427/// Writes a decoded ERF-family archive back out using its preserved layout.
428///
429/// # Errors
430///
431/// Returns [`ErfError`] if the write fails.
432#[allow(clippy::too_many_arguments)]
433pub fn write_erf_archive<W>(writer: &mut W, value: &Erf) -> ErfResult<()>
434where
435    W: Write + Seek,
436{
437    let entries = value.entries().keys().cloned().collect::<Vec<_>>();
438    let mut payloads = BTreeMap::new();
439    let mut algorithms = BTreeMap::new();
440    let mut exocomp = ExoResFileCompressionType::None;
441
442    for (rr, res) in value.entries() {
443        payloads.insert(rr.clone(), res.read_all(CachePolicy::Bypass)?);
444        let algorithm = res.compressed_buf_algorithm().unwrap_or(Algorithm::None);
445        if algorithm != Algorithm::None {
446            exocomp = ExoResFileCompressionType::CompressedBuf;
447        }
448        algorithms.insert(rr.clone(), algorithm);
449    }
450
451    write_erf_with_options(
452        writer,
453        &value.file_type,
454        value.file_version,
455        u32::try_from(value.build_year)
456            .map_err(|_error| ErfError::msg("ERF build year exceeds 32-bit range"))?,
457        u32::try_from(value.build_day)
458            .map_err(|_error| ErfError::msg("ERF build day exceeds 32-bit range"))?,
459        exocomp,
460        Algorithm::None,
461        value.loc_strings(),
462        value.str_ref,
463        &entries,
464        value.oid(),
465        ErfWriteOptions {
466            resource_list_padding: value.resource_list_padding(),
467        },
468        |rr, out| {
469            let bytes = payloads
470                .get(rr)
471                .ok_or_else(|| io::Error::other(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("missing ERF payload for {0}", rr))
    })format!("missing ERF payload for {rr}")))?;
472            out.write_all(bytes)?;
473            Ok((bytes.len(), secure_hash(bytes)))
474        },
475        |rr| algorithms.get(rr).copied().unwrap_or(Algorithm::None),
476    )
477}
478
479#[allow(clippy::too_many_arguments)]
480/// Internal ERF-family archive writer with explicit padding control.
481fn write_erf_inner<W, F>(
482    writer: &mut W,
483    file_type: &str,
484    file_version: ErfVersion,
485    build_year: u32,
486    build_day: u32,
487    exocomp: ExoResFileCompressionType,
488    _compalg: Algorithm,
489    loc_strings: &BTreeMap<i32, String>,
490    str_ref: i32,
491    entries: &[ResRef],
492    erf_oid: Option<&str>,
493    resource_list_padding: u64,
494    mut entry_writer: F,
495    mut entry_algorithm: impl FnMut(&ResRef) -> Algorithm,
496) -> ErfResult<()>
497where
498    W: Write + Seek,
499    F: FnMut(&ResRef, &mut dyn Write) -> ErfResult<(usize, SecureHash)>,
500{
501    if exocomp != ExoResFileCompressionType::None && file_version != ErfVersion::E1 {
502        return Err(ErfError::msg("Compression requires E1"));
503    }
504
505    let mut encoded_loc_strings = Vec::with_capacity(loc_strings.len());
506    let mut loc_string_size = 0_u64;
507    for (id, text) in loc_strings {
508        let encoded = to_nwnrs_encoding(text)?;
509        loc_string_size += 8 + u64::try_from(encoded.len())
510            .map_err(|_error| ErfError::msg("localized string length exceeds 64-bit range"))?;
511        encoded_loc_strings.push((*id, encoded));
512    }
513
514    let offset_to_loc_str = HEADER_SIZE;
515    let key_entry_size = match file_version {
516        ErfVersion::V1 => 24_u64,
517        ErfVersion::E1 => 44_u64,
518    };
519    let offset_to_key_list = offset_to_loc_str + loc_string_size;
520    let key_list_size = key_entry_size
521        * u64::try_from(entries.len())
522            .map_err(|_error| ErfError::msg("ERF entry count exceeds 64-bit range"))?;
523    let offset_to_resource_list = offset_to_key_list + key_list_size + resource_list_padding;
524    let resource_entry_size = match file_version {
525        ErfVersion::V1 => 8_u64,
526        ErfVersion::E1 => 16_u64,
527    };
528    let resource_list_size = resource_entry_size
529        * u64::try_from(entries.len())
530            .map_err(|_error| ErfError::msg("ERF entry count exceeds 64-bit range"))?;
531
532    writer.seek(SeekFrom::Start(0))?;
533    write_padded_file_type(writer, file_type)?;
534    match file_version {
535        ErfVersion::V1 => writer.write_all(b"V1.0")?,
536        ErfVersion::E1 => writer.write_all(b"E1.0")?,
537    }
538    write_i32(
539        writer,
540        to_i32_len(loc_strings.len(), "ERF localized string count")?,
541    )?;
542    write_i32(
543        writer,
544        to_i32_u64(loc_string_size, "ERF localized string block size")?,
545    )?;
546    write_i32(writer, to_i32_len(entries.len(), "ERF entry count")?)?;
547    write_i32(
548        writer,
549        to_i32_u64(offset_to_loc_str, "ERF locstring offset")?,
550    )?;
551    write_i32(
552        writer,
553        to_i32_u64(offset_to_key_list, "ERF key list offset")?,
554    )?;
555    write_i32(
556        writer,
557        to_i32_u64(offset_to_resource_list, "ERF resource list offset")?,
558    )?;
559    write_i32(writer, to_i32_u32(build_year, "ERF build year")?)?;
560    write_i32(writer, to_i32_u32(build_day, "ERF build day")?)?;
561    write_i32(writer, str_ref)?;
562    match file_version {
563        ErfVersion::V1 => writer.write_all(&[0_u8; 116])?,
564        ErfVersion::E1 => {
565            writer.write_all(
566                normalize_oid(erf_oid.unwrap_or("000000000000000000000000"))?.as_bytes(),
567            )?;
568            writer.write_all(&[0_u8; 92])?;
569        }
570    }
571
572    for (id, encoded) in &encoded_loc_strings {
573        write_i32(writer, *id)?;
574        write_i32(
575            writer,
576            to_i32_len(encoded.len(), "ERF localized string length")?,
577        )?;
578        writer.write_all(encoded)?;
579    }
580
581    writer.write_all(&::alloc::vec::from_elem(0_u8,
    usize::try_from(key_list_size).map_err(|_error|
                { ErfError::msg("ERF key list size exceeds usize") })?)vec![
582        0_u8;
583        usize::try_from(key_list_size).map_err(|_error| {
584            ErfError::msg("ERF key list size exceeds usize")
585        })?
586    ])?;
587    writer.write_all(&::alloc::vec::from_elem(0_u8,
    usize::try_from(resource_list_padding).map_err(|_error|
                ErfError::msg("ERF resource list padding exceeds usize"))?)vec![
588        0_u8;
589        usize::try_from(resource_list_padding).map_err(
590            |_error| ErfError::msg("ERF resource list padding exceeds usize")
591        )?
592    ])?;
593    writer.write_all(&::alloc::vec::from_elem(0_u8,
    usize::try_from(resource_list_size).map_err(|_error|
                ErfError::msg("ERF resource list size exceeds usize"))?)vec![
594        0_u8;
595        usize::try_from(resource_list_size).map_err(
596            |_error| ErfError::msg("ERF resource list size exceeds usize")
597        )?
598    ])?;
599
600    let offset_to_resource_data = writer.stream_position()?;
601    let mut written = Vec::<(ResRef, usize, usize, SecureHash)>::with_capacity(entries.len());
602    for rr in entries {
603        let pos = writer.stream_position()?;
604        let (disk_size, uncompressed_size, sha1) = match exocomp {
605            ExoResFileCompressionType::None => {
606                let (bytes, sha1) = entry_writer(rr, writer)?;
607                (bytes, bytes, sha1)
608            }
609            ExoResFileCompressionType::CompressedBuf => {
610                let mut buffer = Vec::new();
611                let (uncompressed_size, sha1) = entry_writer(rr, &mut buffer)?;
612                let algorithm = entry_algorithm(rr);
613                compress_writer(
614                    writer,
615                    &buffer,
616                    algorithm,
617                    EXO_RES_FILE_COMPRESSED_BUF_MAGIC,
618                )?;
619                let disk_size = usize::try_from(writer.stream_position()? - pos)
620                    .map_err(|_error| ErfError::msg("ERF compressed entry size exceeds usize"))?;
621                (disk_size, uncompressed_size, sha1)
622            }
623        };
624        written.push((rr.clone(), disk_size, uncompressed_size, sha1));
625    }
626
627    let end_of_file = writer.stream_position()?;
628
629    writer.seek(SeekFrom::Start(offset_to_key_list))?;
630    for (index, (rr, _, _, sha1)) in written.iter().enumerate() {
631        write_padded_resref(writer, rr)?;
632        write_i32(writer, to_i32_len(index, "ERF resource index")?)?;
633        write_u16(writer, rr.res_type().0)?;
634        writer.write_all(&[0_u8; 2])?;
635        if file_version == ErfVersion::E1 {
636            writer.write_all(sha1.as_bytes())?;
637        }
638    }
639
640    writer.seek(SeekFrom::Start(offset_to_resource_list))?;
641    let mut current_offset = offset_to_resource_data;
642    for (_, disk_size, uncompressed_size, _) in &written {
643        write_u32(
644            writer,
645            to_u32_u64(current_offset, "ERF resource data offset")?,
646        )?;
647        write_u32(writer, to_u32_len(*disk_size, "ERF disk size")?)?;
648        if file_version == ErfVersion::E1 {
649            write_u32(writer, exocomp as u32)?;
650            write_u32(
651                writer,
652                to_u32_len(*uncompressed_size, "ERF uncompressed size")?,
653            )?;
654        }
655        current_offset += *disk_size as u64;
656    }
657
658    writer.seek(SeekFrom::Start(end_of_file))?;
659    {
    use ::tracing::__macro_support::Callsite as _;
    static __CALLSITE: ::tracing::callsite::DefaultCallsite =
        {
            static META: ::tracing::Metadata<'static> =
                {
                    ::tracing_core::metadata::Metadata::new("event src/io.rs:659",
                        "nwnrs_erf::io", ::tracing::Level::DEBUG,
                        ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                        ::tracing_core::__macro_support::Option::Some(659u32),
                        ::tracing_core::__macro_support::Option::Some("nwnrs_erf::io"),
                        ::tracing_core::field::FieldSet::new(&["message",
                                        {
                                            const NAME:
                                                ::tracing::__macro_support::FieldName<{
                                                    ::tracing::__macro_support::FieldName::len("entry_count")
                                                }> =
                                                ::tracing::__macro_support::FieldName::new("entry_count");
                                            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!("wrote erf archive")
                                            as &dyn ::tracing::field::Value)),
                                (::tracing::__macro_support::Option::Some(&written.len() as
                                            &dyn ::tracing::field::Value))])
            });
    } else { ; }
};debug!(entry_count = written.len(), "wrote erf archive");
660    Ok(())
661}
662
663fn normalize_oid(input: &str) -> ErfResult<String> {
664    let normalized = input.trim().to_ascii_lowercase();
665    if normalized.len() == 24 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
666        Ok(normalized)
667    } else {
668        Err(ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("invalid oid: {0}", input))
    })format!("invalid oid: {input}")))
669    }
670}
671
672fn trim_trailing_nuls(bytes: &[u8]) -> String {
673    let end = bytes
674        .iter()
675        .position(|byte| *byte == 0)
676        .unwrap_or(bytes.len());
677    String::from_utf8_lossy(bytes.get(..end).unwrap_or(bytes)).to_string()
678}
679
680fn read_fixed_string<R: Read + ?Sized>(reader: &mut R, len: usize) -> io::Result<String> {
681    let bytes = read_bytes_or_err(reader, len)?;
682    String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
683}
684
685fn read_secure_hash<R: Read + ?Sized>(reader: &mut R) -> io::Result<SecureHash> {
686    let mut bytes = [0_u8; 20];
687    reader.read_exact(&mut bytes)?;
688    Ok(SecureHash::new(bytes))
689}
690
691fn read_i32<R: Read + ?Sized>(reader: &mut R) -> io::Result<i32> {
692    let mut bytes = [0_u8; 4];
693    reader.read_exact(&mut bytes)?;
694    Ok(i32::from_le_bytes(bytes))
695}
696
697fn read_u16<R: Read + ?Sized>(reader: &mut R) -> io::Result<u16> {
698    let mut bytes = [0_u8; 2];
699    reader.read_exact(&mut bytes)?;
700    Ok(u16::from_le_bytes(bytes))
701}
702
703fn read_u32<R: Read + ?Sized>(reader: &mut R) -> io::Result<u32> {
704    let mut bytes = [0_u8; 4];
705    reader.read_exact(&mut bytes)?;
706    Ok(u32::from_le_bytes(bytes))
707}
708
709fn write_i32<W: Write + ?Sized>(writer: &mut W, value: i32) -> io::Result<()> {
710    writer.write_all(&value.to_le_bytes())
711}
712
713fn write_u16<W: Write + ?Sized>(writer: &mut W, value: u16) -> io::Result<()> {
714    writer.write_all(&value.to_le_bytes())
715}
716
717fn to_i32_len(value: usize, what: &str) -> ErfResult<i32> {
718    i32::try_from(value).map_err(|_error| ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
719}
720
721fn to_i32_u64(value: u64, what: &str) -> ErfResult<i32> {
722    i32::try_from(value).map_err(|_error| ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
723}
724
725fn to_i32_u32(value: u32, what: &str) -> ErfResult<i32> {
726    i32::try_from(value).map_err(|_error| ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
727}
728
729fn to_u32_len(value: usize, what: &str) -> ErfResult<u32> {
730    u32::try_from(value).map_err(|_error| ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
731}
732
733fn to_u32_u64(value: u64, what: &str) -> ErfResult<u32> {
734    u32::try_from(value).map_err(|_error| ErfError::msg(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
735}
736
737fn write_u32<W: Write + ?Sized>(writer: &mut W, value: u32) -> io::Result<()> {
738    writer.write_all(&value.to_le_bytes())
739}
740
741fn write_padded_resref<W: Write + ?Sized>(writer: &mut W, rr: &ResRef) -> io::Result<()> {
742    let value = rr.res_ref();
743    writer.write_all(value.as_bytes())?;
744    writer.write_all(&::alloc::vec::from_elem(0_u8, 16 - value.len())vec![0_u8; 16 - value.len()])
745}
746
747fn write_padded_file_type<W: Write + ?Sized>(writer: &mut W, file_type: &str) -> io::Result<()> {
748    let mut padded = file_type
749        .chars()
750        .take(4)
751        .collect::<String>()
752        .to_ascii_uppercase();
753    while padded.len() < 4 {
754        padded.push(' ');
755    }
756    writer.write_all(padded.as_bytes())
757}