1use std::io::Read;
4
5use super::{expected_payload_size, read_header, Tpc, TpcBinaryError, FILE_HEADER_SIZE};
6
7#[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#[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}