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