rakata_formats/ssf/
reader.rs1use 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
14pub 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
28pub 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 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}