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> {
18 let mut bytes = Vec::new();
19 reader.read_to_end(&mut bytes)?;
20 read_ssf_from_bytes(&bytes)
21}
22
23pub 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 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}