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