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