Skip to main content

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