rakata_formats/tpc/
reader.rs

1//! TPC binary reader.
2
3use std::io::Read;
4
5use super::{expected_payload_size, read_header, Tpc, TpcBinaryError, FILE_HEADER_SIZE};
6
7/// Reads TPC data from a reader.
8#[cfg_attr(
9    feature = "tracing",
10    tracing::instrument(level = "debug", skip(reader))
11)]
12pub fn read_tpc<R: Read>(reader: &mut R) -> Result<Tpc, TpcBinaryError> {
13    let mut bytes = Vec::new();
14    reader.read_to_end(&mut bytes)?;
15    crate::trace_debug!(bytes_len = bytes.len(), "read tpc bytes from reader");
16    read_tpc_from_bytes(&bytes)
17}
18
19/// Reads TPC data from bytes.
20#[cfg_attr(
21    feature = "tracing",
22    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
23)]
24pub fn read_tpc_from_bytes(bytes: &[u8]) -> Result<Tpc, TpcBinaryError> {
25    if bytes.len() < FILE_HEADER_SIZE {
26        return Err(TpcBinaryError::InvalidHeader(
27            "file smaller than TPC header".into(),
28        ));
29    }
30
31    let header = read_header(bytes)?;
32    let remaining = &bytes[FILE_HEADER_SIZE..];
33    let payload_size = expected_payload_size(&header)?;
34
35    if payload_size > remaining.len() {
36        return Err(TpcBinaryError::InvalidHeader(format!(
37            "payload exceeds file bounds: expected {payload_size} bytes, available {}",
38            remaining.len()
39        )));
40    }
41
42    let payload = remaining[..payload_size].to_vec();
43    let txi_footer = remaining[payload_size..].to_vec();
44
45    let tpc = Tpc::new(header, payload, txi_footer);
46    crate::trace_debug!(
47        pixel_type = tpc.header.pixel_type,
48        mipmap_count = tpc.header.mipmap_count,
49        payload_len = tpc.payload.len(),
50        txi_footer_len = tpc.txi_footer.len(),
51        "parsed tpc from bytes"
52    );
53    Ok(tpc)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use crate::tpc::{
60        write_header, write_tpc_to_vec, TpcHeader, TpcHeaderPixelFormat, TpcPixelFormatCode,
61        RESERVED_SIZE,
62    };
63
64    fn make_header(
65        data_size: u32,
66        width: u16,
67        height: u16,
68        pixel_type: u8,
69        mipmap_count: u8,
70    ) -> TpcHeader {
71        TpcHeader {
72            data_size,
73            alpha_test: 0.5,
74            width,
75            height,
76            pixel_type,
77            mipmap_count,
78            reserved: [0_u8; RESERVED_SIZE],
79        }
80    }
81
82    #[test]
83    fn roundtrip_synthetic_uncompressed_tpc_with_txi_footer() {
84        let header = make_header(0, 4, 2, 4, 1);
85        let payload: Vec<u8> = (0_u8..32).collect();
86        let txi_footer = b"mipmap 0\ncube 1".to_vec();
87        let tpc = Tpc::new(header, payload, txi_footer);
88
89        let bytes = write_tpc_to_vec(&tpc).expect("write should succeed");
90        let parsed = read_tpc_from_bytes(&bytes).expect("read should succeed");
91
92        assert_eq!(parsed, tpc);
93        assert_eq!(parsed.txi_text(), "mipmap 0\ncube 1");
94        assert_eq!(
95            parsed.known_pixel_format(),
96            Some(TpcHeaderPixelFormat::Rgba)
97        );
98    }
99
100    #[test]
101    fn writer_is_deterministic_for_synthetic_tpc() {
102        let header = make_header(0, 4, 2, 4, 1);
103        let payload: Vec<u8> = (0_u8..32).collect();
104        let txi_footer = b"mipmap 0\ncube 1".to_vec();
105        let tpc = Tpc::new(header, payload, txi_footer);
106
107        let first = write_tpc_to_vec(&tpc).expect("first write should succeed");
108        let second = write_tpc_to_vec(&tpc).expect("second write should succeed");
109        assert_eq!(first, second, "canonical TPC writer output drifted");
110    }
111
112    #[test]
113    fn parses_compressed_cubemap_payload() {
114        let header = make_header(8, 4, 24, 2, 1);
115        let payload = vec![0xAA; 48];
116        let tpc = Tpc::new(header, payload, Vec::new());
117
118        let bytes = write_tpc_to_vec(&tpc).expect("write should succeed");
119        let parsed = read_tpc_from_bytes(&bytes).expect("read should succeed");
120
121        assert!(parsed.is_cube_map());
122        assert_eq!(parsed.payload.len(), 48);
123        assert_eq!(
124            parsed.known_pixel_format(),
125            Some(TpcHeaderPixelFormat::Dxt1)
126        );
127    }
128
129    #[test]
130    fn rejects_unknown_pixel_type() {
131        let header = make_header(0, 4, 4, 9, 1);
132        let mut bytes = Vec::new();
133        write_header(&mut bytes, &header).expect("header write");
134        bytes.extend_from_slice(&[0_u8; 16]);
135
136        let err = read_tpc_from_bytes(&bytes).expect_err("must fail");
137        assert!(matches!(err, TpcBinaryError::UnsupportedPixelType(_)));
138    }
139
140    #[test]
141    fn rejects_uncompressed_pixel_type_twelve() {
142        let header = make_header(0, 4, 4, 12, 1);
143        let mut bytes = Vec::new();
144        write_header(&mut bytes, &header).expect("header write");
145        bytes.extend_from_slice(&[0_u8; 64]);
146
147        let err = read_tpc_from_bytes(&bytes).expect_err("must fail");
148        assert!(matches!(err, TpcBinaryError::UnsupportedPixelType(_)));
149    }
150
151    #[test]
152    fn rejects_truncated_header_or_payload() {
153        let err = read_tpc_from_bytes(&[]).expect_err("must fail");
154        assert!(matches!(err, TpcBinaryError::InvalidHeader(_)));
155        let err = read_tpc_from_bytes(&[0_u8; FILE_HEADER_SIZE - 1]).expect_err("must fail");
156        assert!(matches!(err, TpcBinaryError::InvalidHeader(_)));
157
158        let header = make_header(0, 4, 4, 4, 1);
159        let tpc = Tpc::new(header, vec![0_u8; 63], Vec::new());
160        let err = write_tpc_to_vec(&tpc).expect_err("must fail");
161        assert!(matches!(err, TpcBinaryError::InvalidData(_)));
162    }
163
164    #[test]
165    fn writer_validates_payload_size() {
166        let header = make_header(8, 4, 4, 2, 2);
167        let tpc = Tpc::new(header, vec![0_u8; 8], Vec::new());
168        let err = write_tpc_to_vec(&tpc).expect_err("must fail");
169        assert!(matches!(err, TpcBinaryError::InvalidData(_)));
170    }
171
172    #[test]
173    fn pixel_format_code_mapping_matches_supported_matrix() {
174        let supported = [
175            ((false, 1_u8), TpcHeaderPixelFormat::Greyscale),
176            ((false, 2_u8), TpcHeaderPixelFormat::Rgb),
177            ((false, 4_u8), TpcHeaderPixelFormat::Rgba),
178            ((true, 2_u8), TpcHeaderPixelFormat::Dxt1),
179            ((true, 4_u8), TpcHeaderPixelFormat::Dxt5),
180        ];
181
182        for ((compressed, pixel_type), expected) in supported {
183            let code = TpcPixelFormatCode {
184                compressed,
185                pixel_type,
186            };
187            assert_eq!(code.known_format(), Some(expected));
188        }
189
190        let unsupported = [
191            (false, 12_u8),
192            (true, 1_u8),
193            (true, 12_u8),
194            (false, 9_u8),
195            (true, 9_u8),
196        ];
197        for (compressed, pixel_type) in unsupported {
198            let code = TpcPixelFormatCode {
199                compressed,
200                pixel_type,
201            };
202            assert_eq!(code.known_format(), None);
203        }
204    }
205
206    #[test]
207    fn compressed_pixel_type_four_maps_to_dxt5() {
208        let header = make_header(16, 4, 4, 4, 1);
209        let tpc = Tpc::new(header, vec![0_u8; 16], Vec::new());
210        let bytes = write_tpc_to_vec(&tpc).expect("write should succeed");
211        let parsed = read_tpc_from_bytes(&bytes).expect("read should succeed");
212        assert_eq!(
213            parsed.known_pixel_format(),
214            Some(TpcHeaderPixelFormat::Dxt5)
215        );
216    }
217
218    #[test]
219    fn uncompressed_shifted_zero_mips_contribute_no_extra_bytes() {
220        let header = make_header(0, 1, 1, 1, 4);
221        let tpc = Tpc::new(header, vec![0_u8; 1], Vec::new());
222        let bytes = write_tpc_to_vec(&tpc).expect("write should succeed");
223        let parsed = read_tpc_from_bytes(&bytes).expect("read should succeed");
224        assert_eq!(parsed.payload.len(), 1);
225    }
226
227    #[test]
228    fn compressed_shifted_zero_mips_contribute_no_extra_blocks() {
229        let header = make_header(8, 4, 4, 2, 4);
230        let tpc = Tpc::new(header, vec![0_u8; 24], Vec::new());
231        let bytes = write_tpc_to_vec(&tpc).expect("write should succeed");
232        let parsed = read_tpc_from_bytes(&bytes).expect("read should succeed");
233        assert_eq!(parsed.payload.len(), 24);
234    }
235
236    #[test]
237    fn rejects_additional_unknown_pixel_type_values() {
238        for (compressed, pixel_type) in [
239            (false, 0_u8),
240            (false, 3_u8),
241            (false, 5_u8),
242            (false, 8_u8),
243            (true, 0_u8),
244            (true, 1_u8),
245            (true, 3_u8),
246            (true, 5_u8),
247            (true, 12_u8),
248        ] {
249            let code = TpcPixelFormatCode {
250                compressed,
251                pixel_type,
252            };
253            assert_eq!(code.known_format(), None);
254        }
255    }
256}