Skip to main content

rakata_formats/lip/
reader.rs

1//! LIP binary reader.
2
3use std::io::Read;
4
5use crate::binary;
6
7use super::{
8    Lip, LipBinaryError, LipKeyframe, LipShapeCode, FILE_HEADER_SIZE, KEYFRAME_ENTRY_SIZE,
9    LIP_MAGIC, LIP_VERSION_V10,
10};
11
12/// Reads a LIP file from a reader.
13///
14/// The stream is consumed from its current position.
15///
16/// # Errors
17///
18/// [`LipBinaryError::Io`] when the stream will not read to end, and whatever
19/// [`read_lip_from_bytes`] reports for the bytes it collected.
20#[cfg_attr(
21    feature = "tracing",
22    tracing::instrument(level = "debug", skip(reader))
23)]
24pub fn read_lip<R: Read>(reader: &mut R) -> Result<Lip, LipBinaryError> {
25    let mut bytes = Vec::new();
26    reader.read_to_end(&mut bytes)?;
27    read_lip_from_bytes(&bytes)
28}
29
30/// Reads a LIP file directly from bytes.
31///
32/// # Errors
33///
34/// [`LipBinaryError::InvalidHeader`] when `bytes` are shorter than the header
35/// or its keyframe count runs past the end, [`LipBinaryError::InvalidMagic`]
36/// when the signature is not `LIP `, [`LipBinaryError::InvalidVersion`] for a
37/// version other than v1.0, and [`LipBinaryError::InvalidData`] when a
38/// keyframe's shape byte falls off the end. A shape code the engine does not
39/// define is kept rather than rejected, so an unknown one round-trips.
40#[cfg_attr(
41    feature = "tracing",
42    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
43)]
44pub fn read_lip_from_bytes(bytes: &[u8]) -> Result<Lip, LipBinaryError> {
45    if bytes.len() < FILE_HEADER_SIZE {
46        return Err(LipBinaryError::InvalidHeader(
47            "file smaller than LIP header".into(),
48        ));
49    }
50
51    let magic = binary::read_fourcc(bytes, 0)?;
52    if magic != LIP_MAGIC {
53        return Err(LipBinaryError::InvalidMagic(magic));
54    }
55
56    let version = binary::read_fourcc(bytes, 4)?;
57    if version != LIP_VERSION_V10 {
58        return Err(LipBinaryError::InvalidVersion(version));
59    }
60
61    let length = binary::read_f32(bytes, 8)?;
62    let entry_count = binary::checked_to_usize(binary::read_u32(bytes, 12)?, "entry_count")?;
63    let keyframes_table_size = entry_count
64        .checked_mul(KEYFRAME_ENTRY_SIZE)
65        .ok_or_else(|| LipBinaryError::InvalidHeader("keyframe table size overflow".into()))?;
66    binary::check_slice_in_bounds(
67        bytes,
68        FILE_HEADER_SIZE,
69        keyframes_table_size,
70        "keyframe table",
71    )?;
72
73    let mut keyframes = Vec::with_capacity(entry_count);
74    for entry_index in 0..entry_count {
75        let base = FILE_HEADER_SIZE + entry_index * KEYFRAME_ENTRY_SIZE;
76        let time = binary::read_f32(bytes, base)?;
77        let shape = *bytes
78            .get(base + 4)
79            .ok_or_else(|| LipBinaryError::InvalidData("shape byte missing".into()))?;
80
81        keyframes.push(LipKeyframe {
82            time,
83            shape: LipShapeCode::from_raw_id(shape),
84        });
85    }
86
87    Ok(Lip { length, keyframes })
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::lip::{write_lip_to_vec, LipShape};
94
95    const TEST_LIP: &[u8] = include_bytes!(concat!(
96        env!("CARGO_MANIFEST_DIR"),
97        "/../../fixtures/test.lip"
98    ));
99    const CORRUPTED_LIP: &[u8] = include_bytes!(concat!(
100        env!("CARGO_MANIFEST_DIR"),
101        "/../../fixtures/test_corrupted.lip"
102    ));
103
104    #[test]
105    fn parses_lip_fixture() {
106        let lip = read_lip_from_bytes(TEST_LIP).expect("fixture should parse");
107        assert!((lip.length - 1.5).abs() < f32::EPSILON);
108        assert_eq!(lip.keyframes.len(), 3);
109
110        assert_eq!(lip.keyframes[0].shape.raw_id(), 0);
111        assert_eq!(lip.keyframes[1].shape.raw_id(), 5);
112        assert_eq!(lip.keyframes[2].shape.raw_id(), 10);
113
114        assert!((lip.keyframes[0].time - 0.0).abs() < 0.0001);
115        assert!((lip.keyframes[1].time - 0.7777).abs() < 0.0001);
116        assert!((lip.keyframes[2].time - 1.25).abs() < 0.0001);
117    }
118
119    #[test]
120    fn roundtrip_synthetic_lip() {
121        let mut lip = Lip::new();
122        lip.length = 2.75;
123        lip.push_keyframe_with_shape(0.0, LipShapeCode::from(LipShape::Neutral));
124        lip.push_keyframe_with_shape(1.0, LipShapeCode::from(LipShape::Th));
125        lip.push_keyframe(2.5, 200);
126
127        let bytes = write_lip_to_vec(&lip).expect("write should succeed");
128        let parsed = read_lip_from_bytes(&bytes).expect("read should succeed");
129        assert_eq!(parsed, lip);
130        assert!(!parsed.keyframes[2].shape.is_known());
131    }
132
133    #[test]
134    fn writer_is_deterministic_for_synthetic_lip() {
135        let mut lip = Lip::new();
136        lip.length = 1.25;
137        lip.push_keyframe(0.0, 0);
138        lip.push_keyframe(0.5, 7);
139        lip.push_keyframe(1.0, 15);
140
141        let first = write_lip_to_vec(&lip).expect("first write should succeed");
142        let second = write_lip_to_vec(&lip).expect("second write should succeed");
143        assert_eq!(first, second);
144    }
145
146    #[test]
147    fn read_write_roundtrip_preserves_fixture_semantics() {
148        let parsed = read_lip_from_bytes(TEST_LIP).expect("fixture should parse");
149        let bytes = write_lip_to_vec(&parsed).expect("write should succeed");
150        let reparsed = read_lip_from_bytes(&bytes).expect("re-read should succeed");
151        assert_eq!(reparsed, parsed);
152    }
153
154    #[test]
155    fn rejects_invalid_magic() {
156        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
157        bytes[0..4].copy_from_slice(b"NOPE");
158        bytes[4..8].copy_from_slice(&LIP_VERSION_V10);
159
160        let err = read_lip_from_bytes(&bytes).expect_err("must fail");
161        assert!(matches!(err, LipBinaryError::InvalidMagic(_)));
162    }
163
164    #[test]
165    fn rejects_invalid_version() {
166        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
167        bytes[0..4].copy_from_slice(&LIP_MAGIC);
168        bytes[4..8].copy_from_slice(b"V9.9");
169
170        let err = read_lip_from_bytes(&bytes).expect_err("must fail");
171        assert!(matches!(err, LipBinaryError::InvalidVersion(_)));
172    }
173
174    #[test]
175    fn rejects_truncated_header() {
176        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
177        let err = read_lip_from_bytes(&bytes).expect_err("must fail");
178        assert!(matches!(err, LipBinaryError::InvalidHeader(_)));
179    }
180
181    #[test]
182    fn rejects_truncated_or_corrupted_fixture() {
183        let err = read_lip_from_bytes(CORRUPTED_LIP).expect_err("must fail");
184        assert!(matches!(
185            err,
186            LipBinaryError::InvalidHeader(_) | LipBinaryError::InvalidData(_)
187        ));
188    }
189
190    #[test]
191    fn shape_wrapper_maps_known_and_unknown_values() {
192        assert_eq!(
193            LipShapeCode::from_raw_id(0).known_shape(),
194            Some(LipShape::Neutral)
195        );
196        assert_eq!(
197            LipShapeCode::from_raw_id(15).known_shape(),
198            Some(LipShape::Kg)
199        );
200        assert_eq!(LipShapeCode::from_raw_id(16).known_shape(), None);
201    }
202}