1use std::{
2 collections::hash_map::RandomState,
3 fs::{self, File},
4 io::{self, BufWriter, Read, Seek, SeekFrom, Write},
5 path::Path,
6 sync::Mutex,
7};
8
9use nwnrs_checksums::prelude::*;
10use nwnrs_compressedbuf::prelude::*;
11use nwnrs_exo::prelude::*;
12use nwnrs_resman::prelude::*;
13use nwnrs_resref::prelude::*;
14use nwnrs_restype::prelude::*;
15use tracing::{debug, instrument};
16
17use crate::prelude::*;
18
19#[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: KeyResult<KeyTable> =
loop {};
return __tracing_attr_fake_return;
}
{
read_key_table_from_reader(reader, label.into(), resolver)
}
})()
{
#[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:28",
"nwnrs_key::io", ::tracing::Level::ERROR,
::tracing_core::__macro_support::Option::Some("src/io.rs"),
::tracing_core::__macro_support::Option::Some(28u32),
::tracing_core::__macro_support::Option::Some("nwnrs_key::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)]
29pub fn read_key_table<R>(
30 reader: R,
31 label: impl Into<String>,
32 resolver: BifResolver,
33) -> KeyResult<KeyTable>
34where
35 R: Read + Seek,
36{
37 read_key_table_from_reader(reader, label.into(), resolver)
38}
39
40#[allow(clippy :: redundant_closure_call)]
match (move ||
{
#[allow(unknown_lints, unreachable_code, clippy ::
diverging_sub_expression, clippy :: empty_loop, clippy ::
let_unit_value, clippy :: let_with_type_underscore, clippy
:: needless_return, clippy :: unreachable)]
if false {
let __tracing_attr_fake_return: KeyResult<KeyTable> =
loop {};
return __tracing_attr_fake_return;
}
{
let path = path.as_ref();
let parent =
path.parent().unwrap_or_else(||
Path::new(".")).to_path_buf();
let resolver: BifResolver =
std::sync::Arc::new(move |filename: &str|
{
let normalized = normalize_bif_filename(filename);
let direct = parent.join(&normalized);
if direct.is_file() {
return Ok(Some(shared_stream(File::open(direct)?)));
}
if let Some(basename) = Path::new(&normalized).file_name() {
let basename_candidate = parent.join(basename);
if basename_candidate.is_file() {
return Ok(Some(shared_stream(File::open(basename_candidate)?)));
}
}
Ok(None)
});
let file = File::open(path)?;
read_key_table_from_reader(file, path.display().to_string(),
resolver)
}
})()
{
#[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:46",
"nwnrs_key::io", ::tracing::Level::ERROR,
::tracing_core::__macro_support::Option::Some("src/io.rs"),
::tracing_core::__macro_support::Option::Some(46u32),
::tracing_core::__macro_support::Option::Some("nwnrs_key::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()))]
47pub fn read_key_table_from_file(path: impl AsRef<Path>) -> KeyResult<KeyTable> {
48 let path = path.as_ref();
49 let parent = path
50 .parent()
51 .unwrap_or_else(|| Path::new("."))
52 .to_path_buf();
53 let resolver: BifResolver = std::sync::Arc::new(move |filename: &str| {
54 let normalized = normalize_bif_filename(filename);
55 let direct = parent.join(&normalized);
56 if direct.is_file() {
57 return Ok(Some(shared_stream(File::open(direct)?)));
58 }
59
60 if let Some(basename) = Path::new(&normalized).file_name() {
61 let basename_candidate = parent.join(basename);
62 if basename_candidate.is_file() {
63 return Ok(Some(shared_stream(File::open(basename_candidate)?)));
64 }
65 }
66
67 Ok(None)
68 });
69
70 let file = File::open(path)?;
71 read_key_table_from_reader(file, path.display().to_string(), resolver)
72}
73
74#[allow(clippy::needless_pass_by_value)]
75fn read_key_table_from_reader<R>(
76 mut reader: R,
77 label: String,
78 resolver: BifResolver,
79) -> KeyResult<KeyTable>
80where
81 R: Read + Seek,
82{
83 let io_start = reader.stream_position()?;
84 let file_type = read_fixed_string(&mut reader, 4)?;
85 if file_type != "KEY " {
86 return Err(KeyError::msg("invalid key magic"));
87 }
88
89 let file_version = read_fixed_string(&mut reader, 4)?;
90 let version = match file_version.as_str() {
91 "V1 " => KeyBifVersion::V1,
92 "E1 " => KeyBifVersion::E1,
93 _ => {
94 return Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("unsupported key version {0}",
file_version))
})format!(
95 "unsupported key version {file_version}"
96 )));
97 }
98 };
99
100 let bif_count = read_u32(&mut reader)? as usize;
101 let key_count = read_u32(&mut reader)? as usize;
102 let offset_to_file_table = u64::from(read_u32(&mut reader)?);
103 let offset_to_key_table = u64::from(read_u32(&mut reader)?);
104 let build_year = read_u32(&mut reader)?;
105 let build_day = read_u32(&mut reader)?;
106 let oid = match version {
107 KeyBifVersion::V1 => {
108 reader.seek(SeekFrom::Current(32))?;
109 None
110 }
111 KeyBifVersion::E1 => {
112 let oid = read_fixed_string(&mut reader, 24)?;
113 reader.seek(SeekFrom::Current(8))?;
114 Some(oid)
115 }
116 };
117 let normalized_oid = oid.as_deref().map(normalize_oid).transpose()?;
118
119 reader.seek(SeekFrom::Start(io_start + offset_to_file_table))?;
120 let mut file_table = Vec::with_capacity(bif_count);
121 for _ in 0..bif_count {
122 let file_size = read_u32(&mut reader)?;
123 let filename_offset = read_u32(&mut reader)?;
124 let filename_size = read_u16(&mut reader)?;
125 let drives = read_u16(&mut reader)?;
126 file_table.push((file_size, filename_offset, filename_size, drives));
127 }
128
129 let mut bifs = Vec::with_capacity(bif_count);
130 for (_, filename_offset, filename_size, drives) in &file_table {
131 reader.seek(SeekFrom::Start(io_start + u64::from(*filename_offset)))?;
132 let filename = trim_trailing_nuls(&read_bytes(&mut reader, usize::from(*filename_size))?);
133 let resolver_filename = normalize_bif_filename(&filename);
134 bifs.push(crate::BifHandle {
135 filename,
136 resolver_filename,
137 expected_version: version,
138 expected_oid: normalized_oid.clone(),
139 drives: *drives,
140 resolver: resolver.clone(),
141 loaded: Mutex::new(None),
142 });
143 }
144
145 let mut resref_id_lookup = indexmap::IndexMap::with_hasher(RandomState::new());
146 reader.seek(SeekFrom::Start(io_start + offset_to_key_table))?;
147 for _ in 0..key_count {
148 let res_ref_raw = trim_trailing_nuls(&read_bytes(&mut reader, 16)?);
149 let res_type = read_u16(&mut reader)?;
150 let res_id = read_u32(&mut reader)?;
151 let bif_idx = (res_id >> 20) as usize;
152 if bif_idx >= bifs.len() {
153 return Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("while reading res {0}={1}.{2}, bifidx not indiced by keyfile: {3}",
res_id, res_ref_raw, res_type, bif_idx))
})format!(
154 "while reading res {res_id}={res_ref_raw}.{res_type}, bifidx not indiced by \
155 keyfile: {bif_idx}"
156 )));
157 }
158
159 let sha1 = if version == KeyBifVersion::E1 {
160 read_secure_hash(&mut reader)?
161 } else {
162 EMPTY_SECURE_HASH
163 };
164
165 let rr = ResRef::new(res_ref_raw, ResType(res_type))?;
166 resref_id_lookup.insert(
167 rr,
168 crate::KeyEntry {
169 res_id,
170 sha1,
171 },
172 );
173 }
174
175 Ok(KeyTable {
176 version,
177 label,
178 build_year,
179 build_day,
180 bifs,
181 resref_id_lookup,
182 oid: normalized_oid,
183 raw_oid: oid,
184 })
185}
186
187pub(crate) fn read_bif(
188 stream: nwnrs_resman::SharedReadSeek,
189 filename: &str,
190 expected_version: KeyBifVersion,
191 expected_oid: Option<&str>,
192) -> KeyResult<crate::LoadedBif> {
193 let mut reader = stream
194 .lock()
195 .map_err(|error| KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("bif stream lock poisoned: {0}",
error))
})format!("bif stream lock poisoned: {error}")))?;
196 reader.seek(SeekFrom::Start(0))?;
197
198 let file_type = read_fixed_string(reader.as_mut(), 4)?;
199 if file_type != "BIFF" {
200 return Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("invalid bif magic in {0}",
filename))
})format!("invalid bif magic in {filename}")));
201 }
202
203 let version = match read_fixed_string(reader.as_mut(), 4)?.as_str() {
204 "V1 " => KeyBifVersion::V1,
205 "E1 " => KeyBifVersion::E1,
206 other => return Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("unsupported bif version {0}",
other))
})format!("unsupported bif version {other}"))),
207 };
208
209 if version != expected_version {
210 return Err(KeyError::msg("bif version mismatches key version"));
211 }
212
213 let variable_count = read_u32(reader.as_mut())? as usize;
214 let fixed_count = read_u32(reader.as_mut())?;
215 let variable_table_offset = u64::from(read_u32(reader.as_mut())?);
216 let raw_oid = if version == KeyBifVersion::E1 {
217 let oid = read_fixed_string(reader.as_mut(), 24)?;
218 let normalized = normalize_oid(&oid)?;
219 if let Some(expected_oid) = expected_oid
220 && normalized != expected_oid
221 {
222 return Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("bif oid ({0}) mismatches key oid ({1})",
normalized, expected_oid))
})format!(
223 "bif oid ({normalized}) mismatches key oid ({expected_oid})"
224 )));
225 }
226 Some(oid)
227 } else {
228 None
229 };
230
231 if fixed_count != 0 {
232 return Err(KeyError::msg("fixed resources in bif not supported"));
233 }
234
235 reader.seek(SeekFrom::Start(variable_table_offset))?;
236 let mut variable_resources = indexmap::IndexMap::with_hasher(RandomState::new());
237 for _ in 0..variable_count {
238 let full_id = read_u32(reader.as_mut())?;
239 let offset = u64::from(read_u32(reader.as_mut())?);
240 let file_size = read_u32(reader.as_mut())? as usize;
241 let _res_type = read_u32(reader.as_mut())?;
242 let (compression_type, uncompressed_size) = if version == KeyBifVersion::E1 {
243 let compression = ExoResFileCompressionType::from_u32(read_u32(reader.as_mut())?)
244 .ok_or_else(|| KeyError::msg("invalid bif compression type"))?;
245 let uncompressed_size = read_u32(reader.as_mut())? as usize;
246 (compression, uncompressed_size)
247 } else {
248 (ExoResFileCompressionType::None, file_size)
249 };
250
251 variable_resources.insert(
252 full_id & 0x000f_ffff,
253 VariableResource {
254 id: full_id,
255 io_offset: offset,
256 io_size: file_size,
257 compression_type,
258 uncompressed_size,
259 },
260 );
261 }
262
263 drop(reader);
264
265 Ok(crate::LoadedBif {
266 stream,
267 file_type,
268 file_version: version,
269 variable_resources,
270 oid: raw_oid.as_deref().map(normalize_oid).transpose()?,
271 raw_oid,
272 })
273}
274
275#[allow(clippy::too_many_arguments)]
276#[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: KeyResult<()> = loop {};
return __tracing_attr_fake_return;
}
{
if exocomp != ExoResFileCompressionType::None &&
version != KeyBifVersion::E1 {
return Err(KeyError::msg("Compression requires E1"));
}
let dest_dir = dest_dir.as_ref();
fs::create_dir_all(dest_dir)?;
let key_oid =
normalize_oid(key_oid.unwrap_or("000000000000000000000000"))?;
let mut file_table = std::io::Cursor::new(Vec::new());
let mut filenames = std::io::Cursor::new(Vec::new());
let mut bif_results = Vec::with_capacity(bifs.len());
for (bif_idx, bif) in bifs.iter().enumerate() {
let filename_for_bif = build_bif_filename(bif_prefix, bif);
let normalized_filename =
normalize_bif_filename(&filename_for_bif);
let bif_path = dest_dir.join(&normalized_filename);
if let Some(parent) = bif_path.parent() {
fs::create_dir_all(parent)?;
}
let bif_file = File::create(&bif_path)?;
let mut bif_writer = BufWriter::new(bif_file);
let written =
write_bif(bif_idx, version, exocomp, compalg,
bif.bif_oid.as_deref().unwrap_or(&key_oid), &mut bif_writer,
&bif.entries, &mut writer)?;
bif_writer.flush()?;
let bif_disk_size = written.0;
bif_results.push(written);
write_u32(&mut file_table,
to_u32_usize(bif_disk_size.saturating_sub(20),
"BIF disk size")?)?;
write_u32(&mut file_table,
to_u32_u64(crate::HEADER_SIZE +
(u64::try_from(bifs.len()).map_err(|_error|
KeyError::msg("too many BIF entries"))? * 12) +
filenames.position(), "BIF filename offset")?)?;
write_u16(&mut file_table,
to_u16_len(filename_for_bif.len(),
"BIF filename length")?)?;
write_u16(&mut file_table, bif.drives)?;
filenames.write_all(filename_for_bif.as_bytes())?;
filenames.write_all(&[0_u8])?;
}
let file_table_size = file_table.position();
let filenames_size = filenames.position();
let total_resref_count: usize =
bifs.iter().map(|bif| bif.entries.len()).sum();
let key_path =
dest_dir.join(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.key", key_name))
}));
let key_file = File::create(&key_path)?;
let mut key_writer = BufWriter::new(key_file);
key_writer.write_all(b"KEY ")?;
match version {
KeyBifVersion::V1 => key_writer.write_all(b"V1 ")?,
KeyBifVersion::E1 => key_writer.write_all(b"E1 ")?,
}
write_u32(&mut key_writer,
to_u32_len(bifs.len(), "KEY BIF count")?)?;
write_u32(&mut key_writer,
to_u32_len(total_resref_count, "KEY resource count")?)?;
write_u32(&mut key_writer,
to_u32_u64(crate::HEADER_SIZE, "KEY header size")?)?;
write_u32(&mut key_writer,
to_u32_u64(crate::HEADER_SIZE + file_table_size +
filenames_size, "KEY resource table offset")?)?;
write_u32(&mut key_writer, build_year)?;
write_u32(&mut key_writer, build_day)?;
match version {
KeyBifVersion::V1 => key_writer.write_all(&[0_u8; 32])?,
KeyBifVersion::E1 => {
key_writer.write_all(key_oid.as_bytes())?;
key_writer.write_all(&[0_u8; 8])?;
}
}
file_table.seek(SeekFrom::Start(0))?;
filenames.seek(SeekFrom::Start(0))?;
io::copy(&mut file_table, &mut key_writer)?;
io::copy(&mut filenames, &mut key_writer)?;
for (bif_idx, bif) in bifs.iter().enumerate() {
for (res_idx, resref) in bif.entries.iter().enumerate() {
write_padded_resref(&mut key_writer, resref.res_ref())?;
write_u16(&mut key_writer, resref.res_type().0)?;
let id =
(to_u32_len(bif_idx, "BIF index")? << 20) +
to_u32_len(res_idx, "resource index")?;
write_u32(&mut key_writer, id)?;
if version == KeyBifVersion::E1 {
let sha1 =
bif_results.get(bif_idx).and_then(|result|
result.1.get(res_idx)).map(|entry|
&entry.1).ok_or_else(||
KeyError::msg("missing E1 SHA entry for packed resource"))?;
key_writer.write_all(sha1.as_bytes())?;
}
}
}
key_writer.flush()?;
{
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:428",
"nwnrs_key::io", ::tracing::Level::DEBUG,
::tracing_core::__macro_support::Option::Some("src/io.rs"),
::tracing_core::__macro_support::Option::Some(428u32),
::tracing_core::__macro_support::Option::Some("nwnrs_key::io"),
::tracing_core::field::FieldSet::new(&["message",
{
const NAME:
::tracing::__macro_support::FieldName<{
::tracing::__macro_support::FieldName::len("bif_count")
}> =
::tracing::__macro_support::FieldName::new("bif_count");
NAME.as_str()
},
{
const NAME:
::tracing::__macro_support::FieldName<{
::tracing::__macro_support::FieldName::len("resource_count")
}> =
::tracing::__macro_support::FieldName::new("resource_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 key and bif set")
as &dyn ::tracing::field::Value)),
(::tracing::__macro_support::Option::Some(&bifs.len() as
&dyn ::tracing::field::Value)),
(::tracing::__macro_support::Option::Some(&total_resref_count
as &dyn ::tracing::field::Value))])
});
} else { ; }
};
Ok(())
}
})()
{
#[allow(clippy :: unit_arg)]
Ok(x) => Ok(x),
Err(e) => {
{
use ::tracing::__macro_support::Callsite as _;
static __CALLSITE: ::tracing::callsite::DefaultCallsite =
{
static META: ::tracing::Metadata<'static> =
{
::tracing_core::metadata::Metadata::new("event src/io.rs:285",
"nwnrs_key::io", ::tracing::Level::ERROR,
::tracing_core::__macro_support::Option::Some("src/io.rs"),
::tracing_core::__macro_support::Option::Some(285u32),
::tracing_core::__macro_support::Option::Some("nwnrs_key::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(
286 level = "debug",
287 skip_all,
288 err,
289 fields(key_name, entry_count = bifs.len(), version = ?version)
290)]
291pub fn write_key_and_bif<F>(
292 version: KeyBifVersion,
293 exocomp: ExoResFileCompressionType,
294 compalg: Algorithm,
295 dest_dir: impl AsRef<Path>,
296 key_name: &str,
297 bif_prefix: &str,
298 bifs: &[KeyBifEntry],
299 build_year: u32,
300 build_day: u32,
301 key_oid: Option<&str>,
302 mut writer: F,
303) -> KeyResult<()>
304where
305 F: FnMut(&ResRef, &mut dyn Write) -> KeyResult<(usize, SecureHash)>,
306{
307 if exocomp != ExoResFileCompressionType::None && version != KeyBifVersion::E1 {
308 return Err(KeyError::msg("Compression requires E1"));
309 }
310
311 let dest_dir = dest_dir.as_ref();
312 fs::create_dir_all(dest_dir)?;
313 let key_oid = normalize_oid(key_oid.unwrap_or("000000000000000000000000"))?;
314
315 let mut file_table = std::io::Cursor::new(Vec::new());
316 let mut filenames = std::io::Cursor::new(Vec::new());
317 let mut bif_results = Vec::with_capacity(bifs.len());
318
319 for (bif_idx, bif) in bifs.iter().enumerate() {
320 let filename_for_bif = build_bif_filename(bif_prefix, bif);
321 let normalized_filename = normalize_bif_filename(&filename_for_bif);
322 let bif_path = dest_dir.join(&normalized_filename);
323 if let Some(parent) = bif_path.parent() {
324 fs::create_dir_all(parent)?;
325 }
326 let bif_file = File::create(&bif_path)?;
327 let mut bif_writer = BufWriter::new(bif_file);
328 let written = write_bif(
329 bif_idx,
330 version,
331 exocomp,
332 compalg,
333 bif.bif_oid.as_deref().unwrap_or(&key_oid),
334 &mut bif_writer,
335 &bif.entries,
336 &mut writer,
337 )?;
338 bif_writer.flush()?;
339 let bif_disk_size = written.0;
340 bif_results.push(written);
341
342 write_u32(
343 &mut file_table,
344 to_u32_usize(bif_disk_size.saturating_sub(20), "BIF disk size")?,
345 )?;
346 write_u32(
347 &mut file_table,
348 to_u32_u64(
349 crate::HEADER_SIZE
350 + (u64::try_from(bifs.len())
351 .map_err(|_error| KeyError::msg("too many BIF entries"))?
352 * 12)
353 + filenames.position(),
354 "BIF filename offset",
355 )?,
356 )?;
357 write_u16(
358 &mut file_table,
359 to_u16_len(filename_for_bif.len(), "BIF filename length")?,
360 )?;
361 write_u16(&mut file_table, bif.drives)?;
362 filenames.write_all(filename_for_bif.as_bytes())?;
363 filenames.write_all(&[0_u8])?;
364 }
365
366 let file_table_size = file_table.position();
367 let filenames_size = filenames.position();
368 let total_resref_count: usize = bifs.iter().map(|bif| bif.entries.len()).sum();
369
370 let key_path = dest_dir.join(format!("{key_name}.key"));
371 let key_file = File::create(&key_path)?;
372 let mut key_writer = BufWriter::new(key_file);
373 key_writer.write_all(b"KEY ")?;
374 match version {
375 KeyBifVersion::V1 => key_writer.write_all(b"V1 ")?,
376 KeyBifVersion::E1 => key_writer.write_all(b"E1 ")?,
377 }
378 write_u32(&mut key_writer, to_u32_len(bifs.len(), "KEY BIF count")?)?;
379 write_u32(
380 &mut key_writer,
381 to_u32_len(total_resref_count, "KEY resource count")?,
382 )?;
383 write_u32(
384 &mut key_writer,
385 to_u32_u64(crate::HEADER_SIZE, "KEY header size")?,
386 )?;
387 write_u32(
388 &mut key_writer,
389 to_u32_u64(
390 crate::HEADER_SIZE + file_table_size + filenames_size,
391 "KEY resource table offset",
392 )?,
393 )?;
394 write_u32(&mut key_writer, build_year)?;
395 write_u32(&mut key_writer, build_day)?;
396 match version {
397 KeyBifVersion::V1 => key_writer.write_all(&[0_u8; 32])?,
398 KeyBifVersion::E1 => {
399 key_writer.write_all(key_oid.as_bytes())?;
400 key_writer.write_all(&[0_u8; 8])?;
401 }
402 }
403
404 file_table.seek(SeekFrom::Start(0))?;
405 filenames.seek(SeekFrom::Start(0))?;
406 io::copy(&mut file_table, &mut key_writer)?;
407 io::copy(&mut filenames, &mut key_writer)?;
408
409 for (bif_idx, bif) in bifs.iter().enumerate() {
410 for (res_idx, resref) in bif.entries.iter().enumerate() {
411 write_padded_resref(&mut key_writer, resref.res_ref())?;
412 write_u16(&mut key_writer, resref.res_type().0)?;
413 let id =
414 (to_u32_len(bif_idx, "BIF index")? << 20) + to_u32_len(res_idx, "resource index")?;
415 write_u32(&mut key_writer, id)?;
416 if version == KeyBifVersion::E1 {
417 let sha1 = bif_results
418 .get(bif_idx)
419 .and_then(|result| result.1.get(res_idx))
420 .map(|entry| &entry.1)
421 .ok_or_else(|| KeyError::msg("missing E1 SHA entry for packed resource"))?;
422 key_writer.write_all(sha1.as_bytes())?;
423 }
424 }
425 }
426
427 key_writer.flush()?;
428 debug!(
429 bif_count = bifs.len(),
430 resource_count = total_resref_count,
431 "wrote key and bif set"
432 );
433 Ok(())
434}
435
436fn write_bif<F>(
437 bif_idx: usize,
438 version: KeyBifVersion,
439 exocomp: ExoResFileCompressionType,
440 compalg: Algorithm,
441 oid: &str,
442 writer: &mut dyn WriteSeek,
443 entries: &[ResRef],
444 entry_writer: &mut F,
445) -> KeyResult<crate::WriteBifResult>
446where
447 F: FnMut(&ResRef, &mut dyn Write) -> KeyResult<(usize, SecureHash)>,
448{
449 writer.seek(SeekFrom::Start(0))?;
450 writer.write_all(b"BIFF")?;
451 match version {
452 KeyBifVersion::V1 => writer.write_all(b"V1 ")?,
453 KeyBifVersion::E1 => writer.write_all(b"E1 ")?,
454 }
455
456 let variable_table_offset = match version {
457 KeyBifVersion::V1 => 20_u32,
458 KeyBifVersion::E1 => 44_u32,
459 };
460 write_u32(writer, to_u32_len(entries.len(), "BIF entry count")?)?;
461 write_u32(writer, 0)?;
462 write_u32(writer, variable_table_offset)?;
463 if version == KeyBifVersion::E1 {
464 writer.write_all(oid.as_bytes())?;
465 }
466
467 let entry_size = match version {
468 KeyBifVersion::V1 => 16_usize,
469 KeyBifVersion::E1 => 24_usize,
470 };
471 writer.write_all(&::alloc::vec::from_elem(0_u8, entry_size * entries.len())vec![0_u8; entry_size * entries.len()])?;
472 let var_table_start = u64::from(variable_table_offset);
473
474 let mut entry_meta =
475 indexmap::IndexMap::<ResRef, (usize, usize, SecureHash), RandomState>::with_hasher(
476 RandomState::new(),
477 );
478 let mut sha_entries = Vec::with_capacity(entries.len());
479 for resref in entries {
480 let pos = writer.stream_position()?;
481 let (compressed_size, uncompressed_size, sha1) = match exocomp {
482 ExoResFileCompressionType::None => {
483 let (bytes, sha1) = entry_writer(resref, writer)?;
484 (bytes, bytes, sha1)
485 }
486 ExoResFileCompressionType::CompressedBuf => {
487 let mut buffer = Vec::new();
488 let (uncompressed_size, sha1) = entry_writer(resref, &mut buffer)?;
489 nwnrs_compressedbuf::compress_writer(
490 writer,
491 &buffer,
492 compalg,
493 EXO_RES_FILE_COMPRESSED_BUF_MAGIC,
494 )?;
495 let compressed_size = usize::try_from(writer.stream_position()? - pos)
496 .map_err(|_error| KeyError::msg("compressed BIF entry size exceeds usize"))?;
497 (compressed_size, uncompressed_size, sha1)
498 }
499 };
500 entry_meta.insert(resref.clone(), (uncompressed_size, compressed_size, sha1));
501 sha_entries.push((resref.clone(), sha1));
502 }
503
504 let end = writer.stream_position()?;
505 writer.seek(SeekFrom::Start(var_table_start))?;
506 let mut offset = var_table_start
507 + u64::try_from(entries.len().saturating_mul(entry_size))
508 .map_err(|_error| KeyError::msg("BIF variable table size exceeds 64-bit range"))?;
509 for (idx, resref) in entries.iter().enumerate() {
510 let id = (to_u32_len(bif_idx, "BIF index")? << 20) + to_u32_len(idx, "BIF resource id")?;
511 let (uncompressed_size, compressed_size, _) = entry_meta
512 .get(resref)
513 .ok_or_else(|| KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("missing written entry metadata for {0}",
resref))
})format!("missing written entry metadata for {resref}")))?;
514 write_u32(writer, id)?;
515 write_u32(writer, to_u32_u64(offset, "BIF entry offset")?)?;
516 write_u32(
517 writer,
518 to_u32_usize(*compressed_size, "BIF compressed entry size")?,
519 )?;
520 offset += *compressed_size as u64;
521 write_u32(writer, u32::from(resref.res_type().0))?;
522 if version == KeyBifVersion::E1 {
523 write_u32(writer, exocomp as u32)?;
524 write_u32(
525 writer,
526 to_u32_usize(*uncompressed_size, "BIF uncompressed entry size")?,
527 )?;
528 }
529 }
530 writer.seek(SeekFrom::Start(end))?;
531
532 Ok((
533 usize::try_from(end).map_err(|_error| KeyError::msg("BIF size exceeds usize"))?,
534 sha_entries,
535 ))
536}
537
538pub fn write_key_table_archive(
545 value: &KeyTable,
546 dest_dir: impl AsRef<Path>,
547 key_name: &str,
548) -> KeyResult<()> {
549 let mut bifs = Vec::with_capacity(value.bifs.len());
550 let bif_contents = value.bif_contents()?;
551 let mut payloads =
552 indexmap::IndexMap::<ResRef, Vec<u8>, RandomState>::with_hasher(RandomState::new());
553
554 for (bif_idx, contents) in bif_contents.into_iter().enumerate() {
555 let handle = value
556 .bifs
557 .get(bif_idx)
558 .ok_or_else(|| KeyError::msg("missing bif handle"))?;
559 let loaded = handle.load()?;
560 for rr in &contents.resources {
561 payloads.insert(rr.clone(), value.demand(rr)?.read_all(CachePolicy::Bypass)?);
562 }
563
564 let path = Path::new(&contents.filename);
565 let name = path
566 .file_stem()
567 .and_then(|value| value.to_str())
568 .filter(|value| !value.is_empty())
569 .ok_or_else(|| KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("invalid bif filename {0}",
contents.filename))
})format!("invalid bif filename {}", contents.filename)))?
570 .to_string();
571 let directory = path
572 .parent()
573 .filter(|parent| !parent.as_os_str().is_empty())
574 .map(|parent| parent.to_string_lossy().replace('\\', "/"))
575 .unwrap_or_default();
576
577 bifs.push(crate::KeyBifEntry {
578 directory,
579 name,
580 recorded_filename: Some(contents.filename.clone()),
581 drives: handle.drives,
582 bif_oid: loaded.raw_oid.clone(),
583 entries: contents.resources,
584 });
585 }
586
587 write_key_and_bif(
588 value.version,
589 infer_key_exocomp(value)?,
590 infer_key_algorithm(value)?,
591 dest_dir,
592 key_name,
593 "",
594 &bifs,
595 value.build_year,
596 value.build_day,
597 value.raw_oid.as_deref(),
598 |rr, io| {
599 let bytes = payloads
600 .get(rr)
601 .ok_or_else(|| io::Error::other(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("missing payload for {0}", rr))
})format!("missing payload for {rr}")))?;
602 io.write_all(bytes)?;
603 Ok((bytes.len(), secure_hash(bytes)))
604 },
605 )
606}
607
608trait WriteSeek: Write + Seek {}
609impl<T: Write + Seek> WriteSeek for T {}
610
611fn normalize_oid(input: &str) -> KeyResult<String> {
612 let normalized = input.trim().to_ascii_lowercase();
613 if normalized.len() == 24 && normalized.chars().all(|ch| ch.is_ascii_hexdigit()) {
614 Ok(normalized)
615 } else {
616 Err(KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("invalid oid: {0}", input))
})format!("invalid oid: {input}")))
617 }
618}
619
620fn normalize_bif_filename(filename: &str) -> String {
621 filename.replace('\\', "/")
622}
623
624fn build_bif_filename(bif_prefix: &str, bif: &crate::KeyBifEntry) -> String {
625 if let Some(filename) = &bif.recorded_filename {
626 return filename.clone();
627 }
628
629 let prefix = bif_prefix.trim_matches(|ch| ch == '/' || ch == '\\');
630 if prefix.is_empty() {
631 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}.bif", bif.name))
})format!("{}.bif", bif.name)
632 } else {
633 ::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0}\\{1}.bif", prefix, bif.name))
})format!("{}\\{}.bif", prefix, bif.name)
634 }
635}
636
637fn infer_key_exocomp(value: &KeyTable) -> KeyResult<ExoResFileCompressionType> {
638 for bif in &value.bifs {
639 let loaded = bif.load()?;
640 if loaded
641 .variable_resources
642 .values()
643 .any(|resource| resource.compression_type != ExoResFileCompressionType::None)
644 {
645 return Ok(ExoResFileCompressionType::CompressedBuf);
646 }
647 }
648 Ok(ExoResFileCompressionType::None)
649}
650
651fn infer_key_algorithm(value: &KeyTable) -> KeyResult<Algorithm> {
652 for rr in value.contents() {
653 let res = value.demand(&rr)?;
654 if let Some(algorithm) = res.compressed_buf_algorithm()
655 && algorithm != Algorithm::None
656 {
657 return Ok(algorithm);
658 }
659 }
660 Ok(Algorithm::None)
661}
662
663fn trim_trailing_nuls(bytes: &[u8]) -> String {
664 let end = bytes
665 .iter()
666 .position(|byte| *byte == 0)
667 .unwrap_or(bytes.len());
668 String::from_utf8_lossy(bytes.get(..end).unwrap_or(bytes)).to_string()
669}
670
671fn read_bytes<R: Read + ?Sized>(reader: &mut R, size: usize) -> io::Result<Vec<u8>> {
672 let mut bytes = ::alloc::vec::from_elem(0_u8, size)vec![0_u8; size];
673 reader.read_exact(&mut bytes)?;
674 Ok(bytes)
675}
676
677fn read_fixed_string<R: Read + ?Sized>(reader: &mut R, size: usize) -> io::Result<String> {
678 let bytes = read_bytes(reader, size)?;
679 String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
680}
681
682fn read_secure_hash<R: Read + ?Sized>(reader: &mut R) -> io::Result<SecureHash> {
683 let mut bytes = [0_u8; 20];
684 reader.read_exact(&mut bytes)?;
685 Ok(SecureHash::new(bytes))
686}
687
688fn read_u16<R: Read + ?Sized>(reader: &mut R) -> io::Result<u16> {
689 let mut bytes = [0_u8; 2];
690 reader.read_exact(&mut bytes)?;
691 Ok(u16::from_le_bytes(bytes))
692}
693
694fn read_u32<R: Read + ?Sized>(reader: &mut R) -> io::Result<u32> {
695 let mut bytes = [0_u8; 4];
696 reader.read_exact(&mut bytes)?;
697 Ok(u32::from_le_bytes(bytes))
698}
699
700fn write_u16<W: Write + ?Sized>(writer: &mut W, value: u16) -> io::Result<()> {
701 writer.write_all(&value.to_le_bytes())
702}
703
704fn write_u32<W: Write + ?Sized>(writer: &mut W, value: u32) -> io::Result<()> {
705 writer.write_all(&value.to_le_bytes())
706}
707
708fn write_padded_resref<W: Write + ?Sized>(writer: &mut W, resref: &str) -> io::Result<()> {
709 writer.write_all(resref.as_bytes())?;
710 writer.write_all(&::alloc::vec::from_elem(0_u8, 16 - resref.len())vec![0_u8; 16 - resref.len()])
711}
712
713fn to_u32_len(value: usize, what: &str) -> KeyResult<u32> {
714 u32::try_from(value).map_err(|_error| KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
})format!("{what} exceeds 32-bit range")))
715}
716
717fn to_u32_usize(value: usize, what: &str) -> KeyResult<u32> {
718 u32::try_from(value).map_err(|_error| KeyError::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_u32_u64(value: u64, what: &str) -> KeyResult<u32> {
722 u32::try_from(value).map_err(|_error| KeyError::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_u16_len(value: usize, what: &str) -> KeyResult<u16> {
726 u16::try_from(value).map_err(|_error| KeyError::msg(::alloc::__export::must_use({
::alloc::fmt::format(format_args!("{0} exceeds 16-bit range", what))
})format!("{what} exceeds 16-bit range")))
727}
728
729#[cfg(test)]
730mod tests {
731 use std::{
732 collections::HashMap,
733 fs,
734 path::PathBuf,
735 time::{SystemTime, UNIX_EPOCH},
736 };
737
738 use nwnrs_compressedbuf::Algorithm;
739 use nwnrs_exo::ExoResFileCompressionType;
740 use nwnrs_resman::{CachePolicy, ResContainer};
741 use nwnrs_resref::{ResRef, ResolvedResRef};
742
743 use super::{read_key_table_from_file, write_key_and_bif, write_key_table_archive};
744 use crate::{KeyBifEntry, KeyBifVersion};
745
746 fn unique_test_dir(prefix: &str) -> PathBuf {
747 let nanos = SystemTime::now()
748 .duration_since(UNIX_EPOCH)
749 .expect("clock drift before unix epoch")
750 .as_nanos();
751 std::env::temp_dir().join(format!("nwnrs-key-{prefix}-{nanos}"))
752 }
753
754 #[test]
755 fn key_archive_roundtrip_preserves_recorded_bif_names_and_multi_bif_ids() {
756 let source_dir = unique_test_dir("source");
757 let output_dir = unique_test_dir("output");
758 fs::create_dir_all(&source_dir).expect("create source dir");
759 fs::create_dir_all(&output_dir).expect("create output dir");
760
761 let alpha: ResRef = ResolvedResRef::from_filename("alpha.uti")
762 .expect("alpha resref")
763 .into();
764 let beta: ResRef = ResolvedResRef::from_filename("beta.utc")
765 .expect("beta resref")
766 .into();
767
768 let payloads: HashMap<ResRef, Vec<u8>> = HashMap::from([
769 (alpha.clone(), b"alpha-bytes".to_vec()),
770 (beta.clone(), b"beta-bytes".to_vec()),
771 ]);
772
773 write_key_and_bif(
774 KeyBifVersion::E1,
775 ExoResFileCompressionType::None,
776 Algorithm::None,
777 &source_dir,
778 "chitin",
779 "",
780 &[
781 KeyBifEntry {
782 directory: String::new(),
783 name: "data_a".to_string(),
784 recorded_filename: Some("Data\\First.BIF".to_string()),
785 drives: 7,
786 bif_oid: Some("fedcba987654321001234567".to_string()),
787 entries: vec![alpha.clone()],
788 },
789 KeyBifEntry {
790 directory: String::new(),
791 name: "data_b".to_string(),
792 recorded_filename: Some("Data\\Second.BIF".to_string()),
793 drives: 9,
794 bif_oid: Some("fedcba987654321001234567".to_string()),
795 entries: vec![beta.clone()],
796 },
797 ],
798 2025,
799 97,
800 Some("fedcba987654321001234567"),
801 |rr, io| {
802 let bytes = payloads.get(rr).expect("payload for resref");
803 io.write_all(bytes)?;
804 Ok((bytes.len(), nwnrs_checksums::secure_hash(bytes)))
805 },
806 )
807 .expect("write key+bif");
808
809 let key_path = source_dir.join("chitin.key");
810 let key = read_key_table_from_file(&key_path).expect("read key");
811 assert_eq!(
812 key.bifs(),
813 vec![
814 "Data\\First.BIF".to_string(),
815 "Data\\Second.BIF".to_string()
816 ]
817 );
818 assert_eq!(key.raw_oid(), Some("fedcba987654321001234567"));
819 assert_eq!(
820 key.demand(&beta)
821 .expect("demand second bif resource")
822 .read_all(CachePolicy::Bypass)
823 .expect("read second bif resource"),
824 b"beta-bytes"
825 );
826
827 write_key_table_archive(&key, &output_dir, "chitin").expect("rewrite key archive");
828
829 assert_eq!(
830 fs::read(source_dir.join("chitin.key")).expect("read source key"),
831 fs::read(output_dir.join("chitin.key")).expect("read output key")
832 );
833 assert_eq!(
834 fs::read(source_dir.join("Data/First.BIF")).expect("read source first bif"),
835 fs::read(output_dir.join("Data/First.BIF")).expect("read output first bif")
836 );
837 assert_eq!(
838 fs::read(source_dir.join("Data/Second.BIF")).expect("read source second bif"),
839 fs::read(output_dir.join("Data/Second.BIF")).expect("read output second bif")
840 );
841 }
842}