1use std::io::{Cursor, Read};
4
5use ddsfile::{D3DFormat, Dds as DdsFile, NewD3dParams};
6
7use crate::binary;
8
9use super::{
10 validate_canonical_standard_d3d_format, CResDdsHeader, Dds, DdsBinaryError, DdsSourceFlavor,
11};
12
13const DDS_DDPF_FLAGS_OFFSET: usize = 84;
14const DDS_DDPF_FOURCC_OFFSET: usize = 88;
15const DDPF_FOURCC_FLAG: u32 = 0x4;
16const CRESDDS_HEADER_SIZE: usize = 20;
17const CRESDDS_DXT1_CODE: u8 = 3;
18const CRESDDS_DXT5_CODE: u8 = 4;
19
20#[cfg_attr(
22 feature = "tracing",
23 tracing::instrument(level = "debug", skip(reader))
24)]
25pub fn read_dds<R: Read>(reader: &mut R) -> Result<Dds, DdsBinaryError> {
26 let mut bytes = Vec::new();
27 reader.read_to_end(&mut bytes)?;
28 read_dds_from_bytes(&bytes)
29}
30
31#[cfg_attr(
33 feature = "tracing",
34 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
35)]
36pub fn read_dds_from_bytes(bytes: &[u8]) -> Result<Dds, DdsBinaryError> {
37 if !bytes.starts_with(b"DDS ") {
38 return read_cresdds_header(bytes);
39 }
40
41 if has_dx10_extension_header(bytes) {
42 return Err(DdsBinaryError::InvalidHeader(
43 "DX10 extension headers are unsupported for KotOR-focused DDS handling".into(),
44 ));
45 }
46
47 let dds = DdsFile::read(Cursor::new(bytes)).map_err(DdsBinaryError::from)?;
48 if dds.header10.is_some() {
49 return Err(DdsBinaryError::InvalidHeader(
50 "DX10 extension headers are unsupported for KotOR-focused DDS handling".into(),
51 ));
52 }
53 let parsed = Dds::from_ddsfile(dds);
54 if let Some(format) = parsed.d3d_format() {
55 validate_canonical_standard_d3d_format(format)?;
56 }
57 Ok(parsed)
58}
59
60fn has_dx10_extension_header(bytes: &[u8]) -> bool {
61 if bytes.len() < DDS_DDPF_FOURCC_OFFSET + 4 {
62 return false;
63 }
64
65 let flags = u32::from_le_bytes([
66 bytes[DDS_DDPF_FLAGS_OFFSET],
67 bytes[DDS_DDPF_FLAGS_OFFSET + 1],
68 bytes[DDS_DDPF_FLAGS_OFFSET + 2],
69 bytes[DDS_DDPF_FLAGS_OFFSET + 3],
70 ]);
71 let fourcc = &bytes[DDS_DDPF_FOURCC_OFFSET..DDS_DDPF_FOURCC_OFFSET + 4];
72 flags & DDPF_FOURCC_FLAG != 0 && fourcc == b"DX10"
73}
74
75fn read_cresdds_header(bytes: &[u8]) -> Result<Dds, DdsBinaryError> {
76 if bytes.len() < CRESDDS_HEADER_SIZE {
77 return Err(DdsBinaryError::InvalidHeader(
78 "missing standard DDS magic (`DDS `) and too short for CResDDS prefix header"
79 .to_string(),
80 ));
81 }
82
83 let width = binary::read_u32(bytes, 0)?;
84 let height = binary::read_u32(bytes, 4)?;
85 if width == 0 || height == 0 {
86 return Err(DdsBinaryError::InvalidHeader(
87 "CResDDS prefix dimensions must be non-zero".into(),
88 ));
89 }
90 if !width.is_power_of_two() || !height.is_power_of_two() {
91 return Err(DdsBinaryError::InvalidHeader(
92 "CResDDS prefix dimensions must be powers of two".into(),
93 ));
94 }
95
96 let bytes_per_pixel_code = bytes[8];
98 let reserved_gap_bytes = [bytes[9], bytes[10], bytes[11]];
99 let format = match bytes_per_pixel_code {
100 CRESDDS_DXT1_CODE => D3DFormat::DXT1,
101 CRESDDS_DXT5_CODE => D3DFormat::DXT5,
102 _ => {
103 return Err(DdsBinaryError::InvalidHeader(format!(
104 "unsupported CResDDS prefix bytes-per-pixel code: {bytes_per_pixel_code}"
105 )));
106 }
107 };
108
109 let base_level_data_size = binary::read_u32(bytes, 12)?;
110 let expected_base_level_size = compressed_mipmap_size(width, height, format)?;
111 if base_level_data_size != expected_base_level_size {
112 return Err(DdsBinaryError::InvalidHeader(format!(
113 "CResDDS prefix base-level size mismatch: header={base_level_data_size}, expected={expected_base_level_size}"
114 )));
115 }
116
117 let alpha_mean = binary::read_f32(bytes, 16)?;
118 let payload = &bytes[CRESDDS_HEADER_SIZE..];
119 let mipmap_levels = infer_compressed_mipmap_count(payload.len(), width, height, format)?;
120
121 let mut dds = Dds::new_d3d(NewD3dParams {
122 height,
123 width,
124 depth: None,
125 format,
126 mipmap_levels: Some(mipmap_levels),
127 caps2: None,
128 })?;
129 if dds.data.len() != payload.len() {
130 return Err(DdsBinaryError::InvalidHeader(format!(
131 "CResDDS prefix payload size mismatch: expected {} bytes for inferred mip levels, got {}",
132 dds.data.len(),
133 payload.len()
134 )));
135 }
136 dds.data.copy_from_slice(payload);
137 dds.source_flavor = DdsSourceFlavor::CResDds(CResDdsHeader {
138 width,
139 height,
140 bytes_per_pixel_code,
141 reserved_gap_bytes,
142 base_level_data_size,
143 alpha_mean,
144 });
145
146 Ok(dds)
147}
148
149fn infer_compressed_mipmap_count(
150 payload_len: usize,
151 mut width: u32,
152 mut height: u32,
153 format: D3DFormat,
154) -> Result<u32, DdsBinaryError> {
155 let mut consumed = 0_usize;
156 let mut levels = 0_u32;
157 loop {
158 let level_size =
159 usize::try_from(compressed_mipmap_size(width, height, format)?).map_err(|_| {
160 DdsBinaryError::InvalidHeader("prefix mip size exceeds host usize".into())
161 })?;
162 let next = consumed.checked_add(level_size).ok_or_else(|| {
163 DdsBinaryError::InvalidHeader("prefix mip size accumulation overflow".into())
164 })?;
165 if next > payload_len {
166 break;
167 }
168 consumed = next;
169 levels += 1;
170 if width == 1 && height == 1 {
171 break;
172 }
173 width = (width / 2).max(1);
174 height = (height / 2).max(1);
175 }
176
177 if levels == 0 {
178 return Err(DdsBinaryError::InvalidHeader(
179 "CResDDS prefix payload does not contain enough data for base mip".into(),
180 ));
181 }
182 if consumed != payload_len {
183 return Err(DdsBinaryError::InvalidHeader(format!(
184 "CResDDS prefix payload size does not align to inferred mip chain: consumed={consumed}, payload={payload_len}"
185 )));
186 }
187
188 Ok(levels)
189}
190
191fn compressed_mipmap_size(
192 width: u32,
193 height: u32,
194 format: D3DFormat,
195) -> Result<u32, DdsBinaryError> {
196 let block_bytes = match format {
197 D3DFormat::DXT1 => 8_u32,
198 D3DFormat::DXT5 => 16_u32,
199 _ => {
200 return Err(DdsBinaryError::InvalidHeader(
201 "prefix mip sizing only supports DXT1/DXT5".into(),
202 ));
203 }
204 };
205 let blocks_w = width.div_ceil(4).max(1);
206 let blocks_h = height.div_ceil(4).max(1);
207 blocks_w
208 .checked_mul(blocks_h)
209 .and_then(|blocks| blocks.checked_mul(block_bytes))
210 .ok_or_else(|| DdsBinaryError::InvalidHeader("prefix mip size overflow".into()))
211}
212
213#[cfg(test)]
214mod tests {
215 use ddsfile::{Caps2, D3DFormat, Dds as DdsFile, NewD3dParams};
216
217 use super::*;
218 use crate::binary::{DecodeBinary, EncodeBinary};
219 use crate::dds::{write_dds_to_vec, DdsNewD3dParams, DdsSourceFlavor};
220
221 #[test]
222 fn roundtrip_synthetic_d3d_dxt1_dds() {
223 let mut dds = Dds::new_d3d(DdsNewD3dParams {
224 height: 4,
225 width: 4,
226 depth: None,
227 format: D3DFormat::DXT1,
228 mipmap_levels: Some(1),
229 caps2: None,
230 })
231 .expect("create DXT1 DDS");
232
233 dds.data.copy_from_slice(&[0x11; 8]);
234
235 let bytes = write_dds_to_vec(&dds).expect("write should succeed");
236 let parsed = read_dds_from_bytes(&bytes).expect("read should succeed");
237
238 assert_eq!(parsed.width(), 4);
239 assert_eq!(parsed.height(), 4);
240 assert_eq!(parsed.mipmap_levels(), 1);
241 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
242 assert_eq!(parsed.data, dds.data);
243 }
244
245 #[test]
246 fn writer_is_deterministic_for_synthetic_dxt1_dds() {
247 let mut dds = Dds::new_d3d(DdsNewD3dParams {
248 height: 4,
249 width: 4,
250 depth: None,
251 format: D3DFormat::DXT1,
252 mipmap_levels: Some(1),
253 caps2: None,
254 })
255 .expect("create DXT1 DDS");
256 dds.data.copy_from_slice(&[0x11; 8]);
257
258 let first = write_dds_to_vec(&dds).expect("first write should succeed");
259 let second = write_dds_to_vec(&dds).expect("second write should succeed");
260 assert_eq!(first, second, "canonical DDS writer output drifted");
261 }
262
263 #[test]
264 fn supports_canonical_kotor_dxt_formats() {
265 for format in [D3DFormat::DXT1, D3DFormat::DXT5] {
266 let mut dds = Dds::new_d3d(DdsNewD3dParams {
267 height: 4,
268 width: 4,
269 depth: None,
270 format,
271 mipmap_levels: Some(1),
272 caps2: None,
273 })
274 .expect("create D3D DDS");
275
276 dds.data.fill(0x7F);
277 let bytes = write_dds_to_vec(&dds).expect("write should succeed");
278 let parsed = read_dds_from_bytes(&bytes).expect("read should succeed");
279
280 assert_eq!(parsed.d3d_format(), Some(format));
281 assert_eq!(parsed.data, dds.data);
282 }
283 }
284
285 #[test]
286 fn rejects_standard_dxt3_on_read() {
287 let mut dds = DdsFile::new_d3d(NewD3dParams {
288 height: 4,
289 width: 4,
290 depth: None,
291 format: D3DFormat::DXT3,
292 mipmap_levels: Some(1),
293 caps2: None,
294 })
295 .expect("create DXT3 DDS via backend");
296 dds.data.fill(0xAB);
297 let mut cursor = std::io::Cursor::new(Vec::new());
298 dds.write(&mut cursor).expect("serialize backend DDS");
299 let bytes = cursor.into_inner();
300
301 let err = read_dds_from_bytes(&bytes).expect_err("canonical reader must reject DXT3");
302 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
303 }
304
305 #[test]
306 fn rejects_standard_dxt3_on_write() {
307 let mut dds = DdsFile::new_d3d(NewD3dParams {
308 height: 4,
309 width: 4,
310 depth: None,
311 format: D3DFormat::DXT3,
312 mipmap_levels: Some(1),
313 caps2: None,
314 })
315 .expect("create DXT3 DDS via backend");
316 dds.data.fill(0xCD);
317
318 let model = Dds {
319 header: dds.header,
320 data: dds.data,
321 source_flavor: DdsSourceFlavor::Standard,
322 };
323 let err = write_dds_to_vec(&model).expect_err("canonical writer must reject DXT3");
324 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
325 }
326
327 #[test]
328 fn reports_cubemap_from_caps2() {
329 let dds = Dds::new_d3d(DdsNewD3dParams {
330 height: 4,
331 width: 4,
332 depth: None,
333 format: D3DFormat::DXT1,
334 mipmap_levels: Some(1),
335 caps2: Some(Caps2::CUBEMAP | Caps2::CUBEMAP_ALLFACES),
336 })
337 .expect("create cubemap DDS");
338
339 assert!(dds.is_cubemap());
340 assert_eq!(dds.array_layers(), 6);
341 }
342
343 #[test]
344 fn rejects_missing_dds_magic() {
345 let err = read_dds_from_bytes(&[0_u8; 16]).expect_err("must fail");
346 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
347 }
348
349 #[test]
350 fn rejects_truncated_standard_dds_header() {
351 let bytes = b"DDS ".to_vec();
352 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
353 assert!(matches!(
354 err,
355 DdsBinaryError::InvalidHeader(_) | DdsBinaryError::Ddsfile(_) | DdsBinaryError::Io(_)
356 ));
357 }
358
359 fn make_cresdds_prefix_header(
360 width: u32,
361 height: u32,
362 bytes_per_pixel_code: u8,
363 base_level_data_size: u32,
364 alpha_mean: f32,
365 ) -> Vec<u8> {
366 make_cresdds_prefix_header_with_reserved(
367 width,
368 height,
369 bytes_per_pixel_code,
370 [0_u8; 3],
371 base_level_data_size,
372 alpha_mean,
373 )
374 }
375
376 fn make_cresdds_prefix_header_with_reserved(
377 width: u32,
378 height: u32,
379 bytes_per_pixel_code: u8,
380 reserved_gap_bytes: [u8; 3],
381 base_level_data_size: u32,
382 alpha_mean: f32,
383 ) -> Vec<u8> {
384 let mut out = Vec::with_capacity(CRESDDS_HEADER_SIZE);
385 out.extend_from_slice(&width.to_le_bytes());
386 out.extend_from_slice(&height.to_le_bytes());
387 out.push(bytes_per_pixel_code);
388 out.extend_from_slice(&reserved_gap_bytes);
389 out.extend_from_slice(&base_level_data_size.to_le_bytes());
390 out.extend_from_slice(&alpha_mean.to_le_bytes());
391 out
392 }
393
394 #[test]
395 fn parses_cresdds_prefix_dxt1_header_variant() {
396 let width = 8_u32;
397 let height = 8_u32;
398 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
399 let mut bytes = make_cresdds_prefix_header(width, height, 3, base_size, 1.0);
400 bytes.extend_from_slice(&[0xAA; 48]);
402
403 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
404 assert_eq!(parsed.width(), width);
405 assert_eq!(parsed.height(), height);
406 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
407 assert_eq!(parsed.mipmap_levels(), 3);
408 assert_eq!(parsed.data.len(), 48);
409 match parsed.source_flavor {
410 DdsSourceFlavor::CResDds(header) => {
411 assert_eq!(header.bytes_per_pixel_code, 3);
412 assert_eq!(header.reserved_gap_bytes, [0_u8; 3]);
413 assert_eq!(header.base_level_data_size, base_size);
414 assert_eq!(header.alpha_mean, 1.0);
415 }
416 DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
417 }
418 }
419
420 #[test]
421 fn parses_cresdds_prefix_dxt5_header_variant() {
422 let width = 4_u32;
423 let height = 4_u32;
424 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT5).expect("base size");
425 let mut bytes = make_cresdds_prefix_header(width, height, 4, base_size, 0.0);
426 bytes.extend_from_slice(&[0x11; 16]);
428
429 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
430 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT5));
431 assert_eq!(parsed.mipmap_levels(), 1);
432 assert_eq!(parsed.data.len(), 16);
433 }
434
435 #[test]
436 fn rejects_cresdds_prefix_with_unknown_bpp_code() {
437 let mut bytes = make_cresdds_prefix_header(4, 4, 9, 16, 0.0);
438 bytes.extend_from_slice(&[0_u8; 16]);
439
440 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
441 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
442 }
443
444 #[test]
445 fn parses_cresdds_prefix_with_nonzero_reserved_gap_bytes() {
446 let width = 4_u32;
447 let height = 4_u32;
448 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
449 let mut bytes = make_cresdds_prefix_header_with_reserved(
450 width,
451 height,
452 3,
453 [0xAA, 0xBB, 0xCC],
454 base_size,
455 1.0,
456 );
457 bytes.extend_from_slice(&[0x42; 8]);
458
459 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
460 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
461 match parsed.source_flavor {
462 DdsSourceFlavor::CResDds(header) => {
463 assert_eq!(header.bytes_per_pixel_code, 3);
464 assert_eq!(header.reserved_gap_bytes, [0xAA, 0xBB, 0xCC]);
465 }
466 DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
467 }
468 }
469
470 #[test]
471 fn parses_cresdds_prefix_alpha_mean_metadata() {
472 let width = 4_u32;
473 let height = 4_u32;
474 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
475 let mut bytes = make_cresdds_prefix_header(width, height, 3, base_size, 0.625);
476 bytes.extend_from_slice(&[0x42; 8]);
477
478 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
479 match parsed.source_flavor {
480 DdsSourceFlavor::CResDds(header) => {
481 assert_eq!(header.alpha_mean, 0.625);
482 }
483 DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
484 }
485 }
486
487 #[test]
488 fn rejects_cresdds_prefix_non_power_of_two() {
489 let mut bytes = make_cresdds_prefix_header(6, 4, 3, 12, 0.0);
490 bytes.extend_from_slice(&[0_u8; 12]);
491
492 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
493 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
494 }
495
496 #[test]
497 fn rejects_cresdds_prefix_payload_size_mismatch() {
498 let mut bytes = make_cresdds_prefix_header(8, 8, 3, 32, 1.0);
499 bytes.extend_from_slice(&[0x22; 41]);
501
502 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
503 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
504 }
505
506 #[test]
507 fn decode_encode_traits_roundtrip() {
508 let dds = Dds::new_d3d(DdsNewD3dParams {
509 height: 4,
510 width: 4,
511 depth: None,
512 format: D3DFormat::DXT1,
513 mipmap_levels: Some(1),
514 caps2: None,
515 })
516 .expect("create DDS");
517
518 let bytes = dds.encode_binary().expect("encode");
519 let decoded = Dds::decode_binary(&bytes).expect("decode");
520
521 assert_eq!(decoded.width(), 4);
522 assert_eq!(decoded.height(), 4);
523 assert_eq!(decoded.d3d_format(), Some(D3DFormat::DXT1));
524 assert_eq!(decoded.data.len(), dds.data.len());
525 assert_eq!(decoded.source_flavor, DdsSourceFlavor::Standard);
526 }
527
528 #[test]
529 fn rejects_unsupported_extension_header() {
530 let dds = Dds::new_d3d(DdsNewD3dParams {
531 height: 4,
532 width: 4,
533 depth: None,
534 format: D3DFormat::DXT1,
535 mipmap_levels: Some(1),
536 caps2: None,
537 })
538 .expect("create DDS");
539 let mut bytes = write_dds_to_vec(&dds).expect("write should succeed");
540
541 bytes[DDS_DDPF_FLAGS_OFFSET..DDS_DDPF_FLAGS_OFFSET + 4]
542 .copy_from_slice(&DDPF_FOURCC_FLAG.to_le_bytes());
543 bytes[DDS_DDPF_FOURCC_OFFSET..DDS_DDPF_FOURCC_OFFSET + 4].copy_from_slice(b"DX10");
544
545 let err =
546 read_dds_from_bytes(&bytes).expect_err("unsupported DDS extension header must fail");
547 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
548 }
549}