Skip to main content

nwnrs_ssf/
io.rs

1use std::io::{self, Read, Seek, SeekFrom, Write};
2
3use nwnrs_io::prelude::*;
4use tracing::{debug, instrument};
5
6use crate::{
7    ENTRY_DATA_SIZE, HEADER_MAGIC, HEADER_VERSION, TABLE_OFFSET, decode_resref, prelude::*,
8};
9
10/// Reads an `SSF` document from `reader`.
11///
12/// # Errors
13///
14/// Returns an [`io::Error`] if the data cannot be read or does not conform to
15/// the SSF format.
16#[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: SsfResult<SsfRoot> =
                            loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        let file_type = read_str_or_err(reader, 4)?;
                        expect(file_type == HEADER_MAGIC,
                                    ::alloc::__export::must_use({
                                            ::alloc::fmt::format(format_args!("expected {0:?}, got {1:?}",
                                                    HEADER_MAGIC, file_type))
                                        })).map_err(invalid_data)?;
                        let file_version = read_str_or_err(reader, 4)?;
                        expect(file_version == HEADER_VERSION,
                                    ::alloc::__export::must_use({
                                            ::alloc::fmt::format(format_args!("expected {0:?}, got {1:?}",
                                                    HEADER_VERSION, file_version))
                                        })).map_err(invalid_data)?;
                        let entry_count = read_u32(reader)? as usize;
                        let table_offset = read_u32(reader)?;
                        expect(table_offset == TABLE_OFFSET,
                                    ::alloc::__export::must_use({
                                            ::alloc::fmt::format(format_args!("expected table offset {0}, got {1}",
                                                    TABLE_OFFSET, table_offset))
                                        })).map_err(invalid_data)?;
                        let padding = read_bytes_or_err(reader, 24)?;
                        expect(padding.iter().all(|byte| *byte == 0),
                                    "expected 24 bytes of zero padding").map_err(invalid_data)?;
                        let entry_offsets =
                            read_fixed_count_seq(reader, entry_count,
                                    |_, reader| read_u32(reader))?;
                        let entries =
                            read_fixed_count_seq(reader, entry_count,
                                    |idx, reader|
                                        {
                                            let offset =
                                                entry_offsets.get(idx).copied().ok_or_else(||
                                                            invalid_message("SSF entry offset index out of range"))?;
                                            reader.seek(SeekFrom::Start(u64::from(offset)))?;
                                            let raw_resref = read_bytes_or_err(reader, 16)?;
                                            let strref = read_u32(reader)?;
                                            let mut raw_resref_bytes = [0_u8; 16];
                                            raw_resref_bytes.copy_from_slice(&raw_resref);
                                            Ok(SsfEntry {
                                                    raw_resref: raw_resref_bytes,
                                                    resref: decode_resref(&raw_resref),
                                                    strref,
                                                })
                                        })?;
                        let root = SsfRoot { entries };
                        {
                            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:69",
                                                "nwnrs_ssf::io", ::tracing::Level::DEBUG,
                                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                                ::tracing_core::__macro_support::Option::Some(69u32),
                                                ::tracing_core::__macro_support::Option::Some("nwnrs_ssf::io"),
                                                ::tracing_core::field::FieldSet::new(&["message",
                                                                {
                                                                    const NAME:
                                                                        ::tracing::__macro_support::FieldName<{
                                                                            ::tracing::__macro_support::FieldName::len("entry_count")
                                                                        }> =
                                                                        ::tracing::__macro_support::FieldName::new("entry_count");
                                                                    NAME.as_str()
                                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                                ::tracing::metadata::Kind::EVENT)
                                        };
                                    ::tracing::callsite::DefaultCallsite::new(&META)
                                };
                            let enabled =
                                ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                                        ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::LevelFilter::current() &&
                                    {
                                        let interest = __CALLSITE.interest();
                                        !interest.is_never() &&
                                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                                interest)
                                    };
                            if enabled {
                                (|value_set: ::tracing::field::ValueSet|
                                            {
                                                let meta = __CALLSITE.metadata();
                                                ::tracing::Event::dispatch(meta, &value_set);
                                                ;
                                            })({
                                        #[allow(unused_imports)]
                                        use ::tracing::field::{debug, display, Value};
                                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("read ssf")
                                                                    as &dyn ::tracing::field::Value)),
                                                        (::tracing::__macro_support::Option::Some(&root.entries.len()
                                                                    as &dyn ::tracing::field::Value))])
                                    });
                            } else { ; }
                        };
                        Ok(root)
                    }
                })()
    {
        #[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:16",
                                "nwnrs_ssf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(16u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_ssf::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)]
17pub fn read_ssf<R: Read + Seek>(reader: &mut R) -> SsfResult<SsfRoot> {
18    let file_type = read_str_or_err(reader, 4)?;
19    expect(
20        file_type == HEADER_MAGIC,
21        format!("expected {HEADER_MAGIC:?}, got {file_type:?}"),
22    )
23    .map_err(invalid_data)?;
24
25    let file_version = read_str_or_err(reader, 4)?;
26    expect(
27        file_version == HEADER_VERSION,
28        format!("expected {HEADER_VERSION:?}, got {file_version:?}"),
29    )
30    .map_err(invalid_data)?;
31
32    let entry_count = read_u32(reader)? as usize;
33    let table_offset = read_u32(reader)?;
34    expect(
35        table_offset == TABLE_OFFSET,
36        format!("expected table offset {TABLE_OFFSET}, got {table_offset}"),
37    )
38    .map_err(invalid_data)?;
39
40    let padding = read_bytes_or_err(reader, 24)?;
41    expect(
42        padding.iter().all(|byte| *byte == 0),
43        "expected 24 bytes of zero padding",
44    )
45    .map_err(invalid_data)?;
46
47    let entry_offsets = read_fixed_count_seq(reader, entry_count, |_, reader| read_u32(reader))?;
48    let entries = read_fixed_count_seq(reader, entry_count, |idx, reader| {
49        let offset = entry_offsets
50            .get(idx)
51            .copied()
52            .ok_or_else(|| invalid_message("SSF entry offset index out of range"))?;
53        reader.seek(SeekFrom::Start(u64::from(offset)))?;
54        let raw_resref = read_bytes_or_err(reader, 16)?;
55        let strref = read_u32(reader)?;
56        let mut raw_resref_bytes = [0_u8; 16];
57        raw_resref_bytes.copy_from_slice(&raw_resref);
58
59        Ok(SsfEntry {
60            raw_resref: raw_resref_bytes,
61            resref: decode_resref(&raw_resref),
62            strref,
63        })
64    })?;
65
66    let root = SsfRoot {
67        entries,
68    };
69    debug!(entry_count = root.entries.len(), "read ssf");
70    Ok(root)
71}
72
73/// Writes an `SSF` document to `writer`.
74///
75/// # Errors
76///
77/// Returns an [`io::Error`] if the SSF data cannot be serialized or the write
78/// fails.
79#[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: SsfResult<()> = loop {};
                        return __tracing_attr_fake_return;
                    }
                    {
                        writer.write_all(HEADER_MAGIC.as_bytes())?;
                        writer.write_all(HEADER_VERSION.as_bytes())?;
                        writer.write_all(&to_u32(ssf.entries.len(),
                                                "SSF entry count")?.to_le_bytes())?;
                        writer.write_all(&TABLE_OFFSET.to_le_bytes())?;
                        writer.write_all(&[0_u8; 24])?;
                        for (idx, _) in ssf.entries.iter().enumerate() {
                            let table_offset =
                                usize::try_from(TABLE_OFFSET).map_err(|_error|
                                            invalid_message("SSF table offset exceeds usize"))?;
                            let offset =
                                ssf.entries.len().checked_mul(4).and_then(|value|
                                                    value.checked_add(table_offset)).and_then(|value|
                                                value.checked_add(idx.saturating_mul(ENTRY_DATA_SIZE))).ok_or_else(||
                                            invalid_message("SSF entry offset overflow"))?;
                            let offset = to_u32(offset, "SSF entry offset")?;
                            writer.write_all(&offset.to_le_bytes())?;
                        }
                        for entry in &ssf.entries {
                            writer.write_all(&entry.stored_resref_bytes()?)?;
                            writer.write_all(&entry.strref.to_le_bytes())?;
                        }
                        {
                            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:106",
                                                "nwnrs_ssf::io", ::tracing::Level::DEBUG,
                                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                                ::tracing_core::__macro_support::Option::Some(106u32),
                                                ::tracing_core::__macro_support::Option::Some("nwnrs_ssf::io"),
                                                ::tracing_core::field::FieldSet::new(&["message",
                                                                {
                                                                    const NAME:
                                                                        ::tracing::__macro_support::FieldName<{
                                                                            ::tracing::__macro_support::FieldName::len("entry_count")
                                                                        }> =
                                                                        ::tracing::__macro_support::FieldName::new("entry_count");
                                                                    NAME.as_str()
                                                                }], ::tracing_core::callsite::Identifier(&__CALLSITE)),
                                                ::tracing::metadata::Kind::EVENT)
                                        };
                                    ::tracing::callsite::DefaultCallsite::new(&META)
                                };
                            let enabled =
                                ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::STATIC_MAX_LEVEL &&
                                        ::tracing::Level::DEBUG <=
                                            ::tracing::level_filters::LevelFilter::current() &&
                                    {
                                        let interest = __CALLSITE.interest();
                                        !interest.is_never() &&
                                            ::tracing::__macro_support::__is_enabled(__CALLSITE.metadata(),
                                                interest)
                                    };
                            if enabled {
                                (|value_set: ::tracing::field::ValueSet|
                                            {
                                                let meta = __CALLSITE.metadata();
                                                ::tracing::Event::dispatch(meta, &value_set);
                                                ;
                                            })({
                                        #[allow(unused_imports)]
                                        use ::tracing::field::{debug, display, Value};
                                        __CALLSITE.metadata().fields().value_set_all(&[(::tracing::__macro_support::Option::Some(&format_args!("wrote ssf")
                                                                    as &dyn ::tracing::field::Value)),
                                                        (::tracing::__macro_support::Option::Some(&ssf.entries.len()
                                                                    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:79",
                                "nwnrs_ssf::io", ::tracing::Level::ERROR,
                                ::tracing_core::__macro_support::Option::Some("src/io.rs"),
                                ::tracing_core::__macro_support::Option::Some(79u32),
                                ::tracing_core::__macro_support::Option::Some("nwnrs_ssf::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(entry_count = ssf.entries.len()))]
80pub fn write_ssf<W: Write>(writer: &mut W, ssf: &SsfRoot) -> SsfResult<()> {
81    writer.write_all(HEADER_MAGIC.as_bytes())?;
82    writer.write_all(HEADER_VERSION.as_bytes())?;
83    writer.write_all(&to_u32(ssf.entries.len(), "SSF entry count")?.to_le_bytes())?;
84    writer.write_all(&TABLE_OFFSET.to_le_bytes())?;
85    writer.write_all(&[0_u8; 24])?;
86
87    for (idx, _) in ssf.entries.iter().enumerate() {
88        let table_offset = usize::try_from(TABLE_OFFSET)
89            .map_err(|_error| invalid_message("SSF table offset exceeds usize"))?;
90        let offset = ssf
91            .entries
92            .len()
93            .checked_mul(4)
94            .and_then(|value| value.checked_add(table_offset))
95            .and_then(|value| value.checked_add(idx.saturating_mul(ENTRY_DATA_SIZE)))
96            .ok_or_else(|| invalid_message("SSF entry offset overflow"))?;
97        let offset = to_u32(offset, "SSF entry offset")?;
98        writer.write_all(&offset.to_le_bytes())?;
99    }
100
101    for entry in &ssf.entries {
102        writer.write_all(&entry.stored_resref_bytes()?)?;
103        writer.write_all(&entry.strref.to_le_bytes())?;
104    }
105
106    debug!(entry_count = ssf.entries.len(), "wrote ssf");
107    Ok(())
108}
109
110fn read_u32<R: Read>(reader: &mut R) -> io::Result<u32> {
111    let mut bytes = [0_u8; 4];
112    reader.read_exact(&mut bytes)?;
113    Ok(u32::from_le_bytes(bytes))
114}
115
116fn invalid_data(error: impl std::error::Error + Send + Sync + 'static) -> io::Error {
117    io::Error::new(io::ErrorKind::InvalidData, error)
118}
119
120fn invalid_message(message: impl Into<String>) -> io::Error {
121    io::Error::new(io::ErrorKind::InvalidData, message.into())
122}
123
124fn to_u32(value: usize, what: &str) -> io::Result<u32> {
125    u32::try_from(value).map_err(|_error| invalid_message(::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0} exceeds 32-bit range", what))
    })format!("{what} exceeds 32-bit range")))
126}
127
128#[allow(clippy::panic)]
129#[cfg(test)]
130mod tests {
131    use std::io::Cursor;
132
133    use crate::{SsfEntry, SsfRoot, read_ssf, write_ssf};
134
135    #[test]
136    fn ssf_preserves_raw_resref_bytes_when_only_strref_changes() {
137        let mut original = Vec::new();
138        original.extend_from_slice(b"SSF ");
139        original.extend_from_slice(b"V1.0");
140        original.extend_from_slice(&1_u32.to_le_bytes());
141        original.extend_from_slice(&40_u32.to_le_bytes());
142        original.extend_from_slice(&[0_u8; 24]);
143        original.extend_from_slice(&44_u32.to_le_bytes());
144        let mut raw = [0_u8; 16];
145        if let Some(prefix) = raw.get_mut(..5) {
146            prefix.copy_from_slice(b"HELLO");
147        } else {
148            panic!("fixture resref slice should be in bounds");
149        }
150        original.extend_from_slice(&raw);
151        original.extend_from_slice(&7_u32.to_le_bytes());
152
153        let mut cursor = Cursor::new(original.clone());
154        let mut ssf = match read_ssf(&mut cursor) {
155            Ok(ssf) => ssf,
156            Err(error) => panic!("read ssf: {error}"),
157        };
158        if let Some(entry) = ssf.entries.get_mut(0) {
159            entry.strref = 9;
160        } else {
161            panic!("fixture should contain one SSF entry");
162        }
163
164        let mut encoded = Vec::new();
165        if let Err(error) = write_ssf(&mut encoded, &ssf) {
166            panic!("write ssf: {error}");
167        }
168
169        assert_eq!(
170            encoded.get(..44),
171            original.get(..44),
172            "header prefix should exist"
173        );
174        assert_eq!(
175            encoded.get(44..60),
176            original.get(44..60),
177            "resref should exist"
178        );
179        assert_eq!(encoded.get(60..64), Some(&9_u32.to_le_bytes()[..]));
180    }
181
182    #[test]
183    fn ssf_new_entry_uses_canonical_padding() {
184        let mut ssf = SsfRoot::new();
185        ssf.entries.push(SsfEntry::new("hello", 7));
186
187        let mut encoded = Vec::new();
188        if let Err(error) = write_ssf(&mut encoded, &ssf) {
189            panic!("write ssf: {error}");
190        }
191
192        assert_eq!(encoded.get(44..49), Some(&b"hello"[..]));
193        assert!(
194            encoded
195                .get(49..60)
196                .unwrap_or(&[])
197                .iter()
198                .all(|byte| *byte == 0)
199        );
200    }
201}