Skip to main content

nwnrs_key/
io.rs

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/// Reads a KEY file from a reader and a caller-supplied BIF resolver.
20///
21/// The resolver is stored for lazy BIF loading and is only invoked when a
22/// referenced resource is actually demanded.
23///
24/// # Errors
25///
26/// Returns [`KeyError`] if the data cannot be read or does not conform to the
27/// KEY format.
28#[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/// Opens a KEY file from disk and resolves BIF paths relative to the KEY
41/// directory.
42///
43/// # Errors
44///
45/// Returns [`KeyError`] if the file cannot be opened or parsed.
46#[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/// Writes a KEY file together with its referenced BIF files.
277///
278/// `bifs` controls both the emitted BIF set and their resource order. For each
279/// resource, `writer` must write the raw payload bytes and return the
280/// uncompressed size together with the payload SHA-1.
281///
282/// # Errors
283///
284/// Returns [`KeyError`] if the write fails.
285#[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
538/// Writes a KEY/BIF resource set using provenance preserved on a loaded
539/// [`KeyTable`].
540///
541/// # Errors
542///
543/// Returns [`KeyError`] if the write fails.
544pub 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}