Skip to main content

rakata_formats/ssf/
reader.rs

1//! SSF binary reader.
2
3use std::io::Read;
4
5use rakata_core::StrRef;
6
7use crate::binary;
8
9use super::{
10    Ssf, SsfBinaryError, FILE_HEADER_SIZE, SOUND_ENTRY_SIZE, SOUND_SLOT_COUNT, SSF_MAGIC,
11    SSF_VERSION_V11,
12};
13
14/// Reads an SSF file from a reader.
15///
16/// The stream is consumed from its current position.
17///
18/// # Errors
19///
20/// [`SsfBinaryError::Io`] when the stream will not read to end, and whatever
21/// [`read_ssf_from_bytes`] reports for the bytes it collected.
22pub fn read_ssf<R: Read>(reader: &mut R) -> Result<Ssf, SsfBinaryError> {
23    let mut bytes = Vec::new();
24    reader.read_to_end(&mut bytes)?;
25    read_ssf_from_bytes(&bytes)
26}
27
28/// Reads an SSF file directly from bytes.
29///
30/// # Errors
31///
32/// [`SsfBinaryError::InvalidMagic`] and [`SsfBinaryError::InvalidVersion`] for
33/// a header that is not an SSF v1.1, [`SsfBinaryError::InvalidHeader`] when
34/// the sound-table offset overlaps the header or its entries run past the end
35/// of `bytes`, and [`SsfBinaryError::InvalidData`] when the bytes trailing the
36/// core table are not a whole number of entries. Those trailing entries are
37/// kept, which is what lets a file carrying more than the documented slots
38/// round-trip.
39pub fn read_ssf_from_bytes(bytes: &[u8]) -> Result<Ssf, SsfBinaryError> {
40    if bytes.len() < FILE_HEADER_SIZE {
41        return Err(SsfBinaryError::InvalidHeader(
42            "file smaller than SSF header".into(),
43        ));
44    }
45
46    let magic = binary::read_fourcc(bytes, 0)?;
47    if magic != SSF_MAGIC {
48        return Err(SsfBinaryError::InvalidMagic(magic));
49    }
50
51    let version = binary::read_fourcc(bytes, 4)?;
52    if version != SSF_VERSION_V11 {
53        return Err(SsfBinaryError::InvalidVersion(version));
54    }
55
56    let sound_table_offset = binary::read_u32(bytes, 8)?;
57    let sound_table_offset_usize =
58        binary::checked_to_usize(sound_table_offset, "sound_table_offset")?;
59    if sound_table_offset_usize < FILE_HEADER_SIZE {
60        return Err(SsfBinaryError::InvalidHeader(
61            "sound table offset overlaps SSF header".into(),
62        ));
63    }
64
65    let core_table_size = SOUND_SLOT_COUNT
66        .checked_mul(SOUND_ENTRY_SIZE)
67        .ok_or_else(|| SsfBinaryError::InvalidHeader("core table size overflow".into()))?;
68    binary::check_slice_in_bounds(
69        bytes,
70        sound_table_offset_usize,
71        core_table_size,
72        "sound table",
73    )?;
74
75    // Read core sound table: each entry is a 4-byte StrRef stored as i32 LE.
76    // Reinterpret u32 -> i32 via LE byte pattern to preserve sign (-1 = unset).
77    let mut sounds = [StrRef::invalid(); SOUND_SLOT_COUNT];
78    for (index, slot) in sounds.iter_mut().enumerate() {
79        let base = sound_table_offset_usize + index * SOUND_ENTRY_SIZE;
80        let raw = binary::read_u32(bytes, base)?;
81        *slot = StrRef::from_raw(i32::from_le_bytes(raw.to_le_bytes()));
82    }
83
84    let tail_offset = sound_table_offset_usize
85        .checked_add(core_table_size)
86        .ok_or_else(|| SsfBinaryError::InvalidData("tail offset overflow".into()))?;
87    let remaining = bytes.len().saturating_sub(tail_offset);
88    if remaining % SOUND_ENTRY_SIZE != 0 {
89        return Err(SsfBinaryError::InvalidData(
90            "trailing table bytes are not aligned to 4-byte entries".into(),
91        ));
92    }
93
94    let mut reserved_entries = Vec::with_capacity(remaining / SOUND_ENTRY_SIZE);
95    for entry_index in 0..(remaining / SOUND_ENTRY_SIZE) {
96        let base = tail_offset + entry_index * SOUND_ENTRY_SIZE;
97        let raw = binary::read_u32(bytes, base)?;
98        reserved_entries.push(StrRef::from_raw(i32::from_le_bytes(raw.to_le_bytes())));
99    }
100
101    Ok(Ssf {
102        sound_table_offset,
103        sounds,
104        reserved_entries,
105    })
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::ssf::{
112        write_ssf_to_vec, SsfSoundSlot, CANONICAL_RESERVED_ENTRY_COUNT, SOUND_SLOT_COUNT,
113    };
114
115    const TEST_SSF: &[u8] = include_bytes!(concat!(
116        env!("CARGO_MANIFEST_DIR"),
117        "/../../fixtures/test.ssf"
118    ));
119    const ITHORIAN_SSF: &[u8] = include_bytes!(concat!(
120        env!("CARGO_MANIFEST_DIR"),
121        "/../../fixtures/n_ithorian.ssf"
122    ));
123    const CORRUPTED_SSF: &[u8] = include_bytes!(concat!(
124        env!("CARGO_MANIFEST_DIR"),
125        "/../../fixtures/test_corrupted.ssf"
126    ));
127
128    #[test]
129    fn parses_ssf_fixture() {
130        let ssf = read_ssf_from_bytes(TEST_SSF).expect("fixture should parse");
131
132        assert_eq!(ssf.sound_table_offset, 12);
133        assert_eq!(ssf.sounds.len(), SOUND_SLOT_COUNT);
134        assert_eq!(ssf.reserved_entries.len(), CANONICAL_RESERVED_ENTRY_COUNT);
135
136        assert_eq!(ssf.get(SsfSoundSlot::BattleCry1), StrRef::from_raw(123075));
137        assert_eq!(ssf.get(SsfSoundSlot::BattleCry6), StrRef::from_raw(123070));
138        assert_eq!(ssf.get(SsfSoundSlot::Select1), StrRef::from_raw(123069));
139        assert_eq!(ssf.get(SsfSoundSlot::Poisoned), StrRef::from_raw(123048));
140        assert!(ssf.reserved_entries.iter().all(|entry| entry.is_invalid()));
141    }
142
143    #[test]
144    fn parses_ithorian_ssf_fixture() {
145        let ssf = read_ssf_from_bytes(ITHORIAN_SSF).expect("fixture should parse");
146        assert_eq!(ssf.sounds.len(), SOUND_SLOT_COUNT);
147        assert_eq!(ssf.reserved_entries.len(), CANONICAL_RESERVED_ENTRY_COUNT);
148    }
149
150    #[test]
151    fn roundtrip_synthetic_ssf() {
152        let mut ssf = Ssf::new();
153        ssf.set(SsfSoundSlot::BattleCry1, StrRef::from_raw(111));
154        ssf.set(SsfSoundSlot::UnlockFailed, StrRef::from_raw(222));
155        ssf.set(SsfSoundSlot::Poisoned, StrRef::invalid());
156        ssf.reserved_entries = vec![
157            StrRef::invalid(),
158            StrRef::invalid(),
159            StrRef::from_raw(999),
160            StrRef::invalid(),
161        ];
162
163        let bytes = write_ssf_to_vec(&ssf).expect("write should succeed");
164        let parsed = read_ssf_from_bytes(&bytes).expect("read should succeed");
165        assert_eq!(parsed, ssf);
166    }
167
168    #[test]
169    fn writer_is_deterministic_for_synthetic_ssf() {
170        let mut ssf = Ssf::new();
171        ssf.set(SsfSoundSlot::Select1, StrRef::from_raw(42));
172        ssf.set(SsfSoundSlot::CriticalHit, StrRef::from_raw(77));
173        ssf.reserved_entries = vec![StrRef::invalid(); CANONICAL_RESERVED_ENTRY_COUNT];
174
175        let first = write_ssf_to_vec(&ssf).expect("first write should succeed");
176        let second = write_ssf_to_vec(&ssf).expect("second write should succeed");
177        assert_eq!(first, second);
178    }
179
180    #[test]
181    fn read_write_roundtrip_preserves_fixture_semantics() {
182        let parsed = read_ssf_from_bytes(TEST_SSF).expect("fixture should parse");
183        let bytes = write_ssf_to_vec(&parsed).expect("write should succeed");
184        let reparsed = read_ssf_from_bytes(&bytes).expect("re-read should succeed");
185        assert_eq!(reparsed, parsed);
186    }
187
188    #[test]
189    fn rejects_invalid_magic() {
190        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
191        bytes[0..4].copy_from_slice(b"NOPE");
192        bytes[4..8].copy_from_slice(&SSF_VERSION_V11);
193
194        let err = read_ssf_from_bytes(&bytes).expect_err("must fail");
195        assert!(matches!(err, SsfBinaryError::InvalidMagic(_)));
196    }
197
198    #[test]
199    fn rejects_invalid_version() {
200        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
201        bytes[0..4].copy_from_slice(&SSF_MAGIC);
202        bytes[4..8].copy_from_slice(b"V9.9");
203
204        let err = read_ssf_from_bytes(&bytes).expect_err("must fail");
205        assert!(matches!(err, SsfBinaryError::InvalidVersion(_)));
206    }
207
208    #[test]
209    fn rejects_truncated_header() {
210        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
211        let err = read_ssf_from_bytes(&bytes).expect_err("must fail");
212        assert!(matches!(err, SsfBinaryError::InvalidHeader(_)));
213    }
214
215    #[test]
216    fn rejects_corrupted_fixture() {
217        let err = read_ssf_from_bytes(CORRUPTED_SSF).expect_err("must fail");
218        assert!(matches!(
219            err,
220            SsfBinaryError::InvalidHeader(_) | SsfBinaryError::InvalidData(_)
221        ));
222    }
223
224    #[test]
225    fn rejects_header_overlapping_sound_table_offset() {
226        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
227        bytes[0..4].copy_from_slice(&SSF_MAGIC);
228        bytes[4..8].copy_from_slice(&SSF_VERSION_V11);
229        bytes[8..12].copy_from_slice(&8_u32.to_le_bytes());
230
231        let err = read_ssf_from_bytes(&bytes).expect_err("must fail");
232        assert!(matches!(err, SsfBinaryError::InvalidHeader(_)));
233    }
234
235    #[test]
236    fn slot_order_is_stable() {
237        assert_eq!(SsfSoundSlot::ALL.len(), SOUND_SLOT_COUNT);
238        assert_eq!(SsfSoundSlot::ALL[0], SsfSoundSlot::BattleCry1);
239        assert_eq!(SsfSoundSlot::ALL[27], SsfSoundSlot::Poisoned);
240    }
241}