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]
434 fn parses_cresdds_prefix_dxt5_header_variant() {
435 let width = 4_u32;
436 let height = 4_u32;
437 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT5).expect("base size");
438 let mut bytes = make_cresdds_prefix_header(width, height, 4, base_size, 0.0);
439 bytes.extend_from_slice(&[0x11; 16]);
441
442 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
443 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT5));
444 assert_eq!(parsed.mipmap_levels(), 1);
445 assert_eq!(parsed.data.len(), 16);
446 }
447
448 #[test]
449 fn rejects_cresdds_prefix_with_unknown_bpp_code() {
450 let mut bytes = make_cresdds_prefix_header(4, 4, 9, 16, 0.0);
451 bytes.extend_from_slice(&[0_u8; 16]);
452
453 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
454 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
455 }
456
457 #[test]
458 fn parses_cresdds_prefix_with_nonzero_reserved_gap_bytes() {
459 let width = 4_u32;
460 let height = 4_u32;
461 let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
462 let mut bytes = make_cresdds_prefix_header_with_reserved(
463 width,
464 height,
465 3,
466 [0xAA, 0xBB, 0xCC],
467 base_size,
468 1.0,
469 );
470 bytes.extend_from_slice(&[0x42; 8]);
471
472 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
473 assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
474 match parsed.source_flavor {
475 DdsSourceFlavor::CResDds(header) => {
476 assert_eq!(header.bytes_per_pixel_code, 3);
477 assert_eq!(header.reserved_gap_bytes, [0xAA, 0xBB, 0xCC]);
478 }
479 DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
480 }
481 }
482
483 #[test]
484 fn parses_cresdds_prefix_alpha_mean_metadata() {
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(width, height, 3, base_size, 0.625);
489 bytes.extend_from_slice(&[0x42; 8]);
490
491 let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
492 match parsed.source_flavor {
493 DdsSourceFlavor::CResDds(header) => {
494 assert_eq!(header.alpha_mean, 0.625);
495 }
496 DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
497 }
498 }
499
500 #[test]
501 fn rejects_cresdds_prefix_non_power_of_two() {
502 let mut bytes = make_cresdds_prefix_header(6, 4, 3, 12, 0.0);
503 bytes.extend_from_slice(&[0_u8; 12]);
504
505 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
506 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
507 }
508
509 #[test]
510 fn rejects_cresdds_prefix_payload_size_mismatch() {
511 let mut bytes = make_cresdds_prefix_header(8, 8, 3, 32, 1.0);
512 bytes.extend_from_slice(&[0x22; 41]);
514
515 let err = read_dds_from_bytes(&bytes).expect_err("must fail");
516 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
517 }
518
519 #[test]
520 fn decode_encode_traits_roundtrip() {
521 let dds = Dds::new_d3d(DdsNewD3dParams {
522 height: 4,
523 width: 4,
524 depth: None,
525 format: D3DFormat::DXT1,
526 mipmap_levels: Some(1),
527 caps2: None,
528 })
529 .expect("create DDS");
530
531 let bytes = dds.encode_binary().expect("encode");
532 let decoded = Dds::decode_binary(&bytes).expect("decode");
533
534 assert_eq!(decoded.width(), 4);
535 assert_eq!(decoded.height(), 4);
536 assert_eq!(decoded.d3d_format(), Some(D3DFormat::DXT1));
537 assert_eq!(decoded.data.len(), dds.data.len());
538 assert_eq!(decoded.source_flavor, DdsSourceFlavor::Standard);
539 }
540
541 #[test]
542 fn rejects_unsupported_extension_header() {
543 let dds = Dds::new_d3d(DdsNewD3dParams {
544 height: 4,
545 width: 4,
546 depth: None,
547 format: D3DFormat::DXT1,
548 mipmap_levels: Some(1),
549 caps2: None,
550 })
551 .expect("create DDS");
552 let mut bytes = write_dds_to_vec(&dds).expect("write should succeed");
553
554 bytes[DDS_DDPF_FLAGS_OFFSET..DDS_DDPF_FLAGS_OFFSET + 4]
555 .copy_from_slice(&DDPF_FOURCC_FLAG.to_le_bytes());
556 bytes[DDS_DDPF_FOURCC_OFFSET..DDS_DDPF_FOURCC_OFFSET + 4].copy_from_slice(b"DX10");
557
558 let err =
559 read_dds_from_bytes(&bytes).expect_err("unsupported DDS extension header must fail");
560 assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
561 }
562}