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#[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#[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#[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 #[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#[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#[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#[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)]
480fn 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}