rusty_dds/lib.rs
1// The MIT License (MIT)
2//
3// Copyright (c) 2018 Michael Dilger
4//
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11//
12// The above copyright notice and this permission notice shall be included in
13// all copies or substantial portions of the Software.
14//
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21// THE SOFTWARE.
22
23//! The main entry point for this library is the [`Dds`] type.
24//!
25//! # Features
26//!
27//! | Feature | Default | Provides |
28//! |---------|---------|----------|
29//! | `decode` | yes | [`Dds::decode_rgba8`], `bcdec_rs` BCn kernels |
30//! | `encode` | yes | [`Dds::encode_from_rgba8`], [`EncodeLayout`] |
31//!
32//! Container parse/compose, surfaces, content classification, and GPU upload
33//! plans are always available. Use `default-features = false, features = ["decode"]`
34//! for loaders / WASM that never encode.
35
36#![cfg_attr(docsrs, feature(doc_cfg))]
37// The memory-safety claim is compiler-enforced, not asserted: without the
38// `simd` feature the crate cannot contain a single `unsafe` block. With it,
39// `unsafe` is confined to the `#[target_feature]` AVX2 kernels in
40// `encode::blocks::simd`, each proven bit-exact against its scalar twin and
41// reachable only behind a runtime CPU check.
42#![cfg_attr(not(feature = "simd"), forbid(unsafe_code))]
43
44#[macro_use]
45extern crate bitflags;
46
47mod error;
48pub use error::*;
49
50mod format;
51pub use format::{D3DFormat, DataFormat, DxgiFormat, FourCC, PixelFormat, PixelFormatFlags};
52
53mod header;
54pub use header::{Caps, Caps2, Header, HeaderFlags};
55
56mod header10;
57pub use header10::{AlphaMode, D3D10ResourceDimension, Header10, MiscFlag};
58
59mod surface;
60pub use surface::{CubemapFace, SubresourceId, SurfaceView, SurfaceViewMut};
61
62mod content;
63pub use content::{DecodeContent, HdrDecodeContent, ImageRgba8, ImageRgbaF32};
64
65#[cfg(feature = "decode")]
66mod decode;
67#[cfg(feature = "decode")]
68pub use decode::reference;
69
70#[cfg(feature = "encode")]
71mod encode;
72#[cfg(feature = "encode")]
73pub use encode::{max_abs_diff, psnr_rgba8, EncodeLayout, EncodeQuality, Rdo};
74
75mod upload;
76pub use upload::{GpuFormat, UploadPath, UploadPlan};
77
78use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
79use std::fmt;
80use std::io::{Read, Write};
81
82/// This is the main DirectDraw Surface file structure, generic over how the
83/// payload is stored.
84///
85/// Use the aliases, not this type directly: [`Dds`] owns its payload and
86/// [`DdsView`] borrows one. Every query, surface, decode and upload-plan method
87/// is implemented once, here, for both.
88#[derive(Clone)]
89pub struct DdsBase<D = Vec<u8>> {
90 // magic is implicit
91 pub header: Header,
92 pub header10: Option<Header10>,
93 pub data: D,
94}
95
96/// A DDS that owns its payload. This is what [`Dds::read`] produces, and it is
97/// what `Dds` has always meant.
98pub type Dds = DdsBase<Vec<u8>>;
99
100/// A DDS that **borrows** its payload — no copy, no allocation.
101///
102/// A streaming engine already holds the file bytes: from `fs::read`, a memory
103/// map, or an archive decompressor. [`Dds::read`] would copy them a second time,
104/// and the copy is dominated by the operating system faulting in and zeroing
105/// pages that are about to be overwritten — measured at ~87% of the call.
106/// [`DdsView::parse`] reads the header and points at the bytes you already have.
107///
108/// ```
109/// use rusty_dds::{DdsView, SubresourceId};
110///
111/// # let mut bytes = Vec::new();
112/// # rusty_dds::Dds::new_dxgi(rusty_dds::NewDxgiParams {
113/// # height: 64, width: 64, depth: None,
114/// # format: rusty_dds::DxgiFormat::BC1_UNorm,
115/// # mipmap_levels: None, array_layers: None, caps2: None, is_cubemap: false,
116/// # resource_dimension: rusty_dds::D3D10ResourceDimension::Texture2D,
117/// # alpha_mode: rusty_dds::AlphaMode::Straight,
118/// # })?.write(&mut bytes)?;
119/// let dds = DdsView::parse(&bytes)?;
120/// let plan = dds.upload_plan_compressed(SubresourceId::mip_layer(0, 0))?;
121/// assert_eq!(plan.width, 64);
122/// # Ok::<(), rusty_dds::Error>(())
123/// ```
124pub type DdsView<'a> = DdsBase<&'a [u8]>;
125
126/// Parameters for Dds::new_d3d()
127#[derive(Debug, Clone)]
128pub struct NewD3dParams {
129 pub height: u32,
130 pub width: u32,
131 pub depth: Option<u32>,
132 pub format: D3DFormat,
133 pub mipmap_levels: Option<u32>,
134 pub caps2: Option<Caps2>,
135}
136
137/// Parameters for Dds::new_dxgi()
138#[derive(Debug, Clone)]
139pub struct NewDxgiParams {
140 pub height: u32,
141 pub width: u32,
142 pub depth: Option<u32>,
143 pub format: DxgiFormat,
144 pub mipmap_levels: Option<u32>,
145 pub array_layers: Option<u32>,
146 pub caps2: Option<Caps2>,
147 pub is_cubemap: bool,
148 pub resource_dimension: D3D10ResourceDimension,
149 pub alpha_mode: AlphaMode,
150}
151
152impl Dds {
153 const MAGIC: u32 = 0x20534444; // b"DDS " in little endian
154
155 /// Create a new DirectDraw Surface with a D3DFormat
156 pub fn new_d3d(params: NewD3dParams) -> Result<Dds, Error> {
157 let size = match get_texture_size(
158 params.format.get_pitch(params.width),
159 None,
160 params.format.get_pitch_height(),
161 params.height,
162 params.depth,
163 ) {
164 Some(s) => s,
165 None => return Err(Error::UnsupportedFormat),
166 };
167
168 let mml = params.mipmap_levels.unwrap_or(1);
169 let min_mipmap_size = match params.format.get_minimum_mipmap_size_in_bytes() {
170 Some(mms) => mms,
171 None => return Err(Error::UnsupportedFormat),
172 };
173 let array_stride = get_array_stride(size, min_mipmap_size, mml);
174
175 let data_size = array_stride;
176
177 Ok(Dds {
178 header: Header::new_d3d(
179 params.height,
180 params.width,
181 params.depth,
182 params.format,
183 params.mipmap_levels,
184 params.caps2,
185 )?,
186 header10: None,
187 data: vec![0; data_size as usize],
188 })
189 }
190
191 /// Create a new DirectDraw Surface with a DxgiFormat
192 pub fn new_dxgi(params: NewDxgiParams) -> Result<Dds, Error> {
193 let arraysize = params.array_layers.unwrap_or(1);
194
195 let size = match get_texture_size(
196 params.format.get_pitch(params.width),
197 None,
198 params.format.get_pitch_height(),
199 params.height,
200 params.depth,
201 ) {
202 Some(s) => s,
203 None => return Err(Error::UnsupportedFormat),
204 };
205
206 let mml = params.mipmap_levels.unwrap_or(1);
207 let min_mipmap_size = match params.format.get_minimum_mipmap_size_in_bytes() {
208 Some(mms) => mms,
209 None => return Err(Error::UnsupportedFormat),
210 };
211 let array_stride = get_array_stride(size, min_mipmap_size, mml);
212
213 let data_size = arraysize
214 .checked_mul(array_stride)
215 .ok_or(Error::OutOfBounds)?;
216
217 let arraysize = if params.is_cubemap {
218 arraysize / 6
219 } else {
220 arraysize
221 };
222 let header10 = Header10::new(
223 params.format,
224 params.is_cubemap,
225 params.resource_dimension,
226 arraysize,
227 params.alpha_mode,
228 );
229
230 Ok(Dds {
231 header: Header::new_dxgi(
232 params.height,
233 params.width,
234 params.depth,
235 params.format,
236 params.mipmap_levels,
237 params.array_layers,
238 params.caps2,
239 )?,
240 header10: Some(header10),
241 data: vec![0; data_size as usize],
242 })
243 }
244
245 /// Read a DDS file, accepting a payload of any length.
246 ///
247 /// The payload is read to end-of-stream with no cap, so the peak allocation
248 /// is whatever the reader yields. That is the right behaviour for a trusted
249 /// file on disk and the wrong one for bytes arriving from a network, a
250 /// user upload, or a mod archive — for those, use [`Dds::read_limited`],
251 /// which fails closed at a byte budget you choose.
252 pub fn read<R: Read>(r: R) -> Result<Dds, Error> {
253 Self::read_inner(r, None)
254 }
255
256 /// Read a DDS file, refusing a payload larger than `max_data_len` bytes.
257 ///
258 /// The limit covers the **payload only** — the 128-byte header (148 with a
259 /// DX10 header) is read first and is not counted. Exceeding it returns
260 /// [`Error::SizeLimitExceeded`] without buffering the overrun, so a hostile
261 /// or corrupt stream cannot force an unbounded allocation.
262 ///
263 /// ```
264 /// use rusty_dds::{Dds, Error};
265 ///
266 /// let mut bytes = Vec::new();
267 /// Dds::new_dxgi(rusty_dds::NewDxgiParams {
268 /// height: 64, width: 64, depth: None,
269 /// format: rusty_dds::DxgiFormat::BC1_UNorm,
270 /// mipmap_levels: None, array_layers: None, caps2: None, is_cubemap: false,
271 /// resource_dimension: rusty_dds::D3D10ResourceDimension::Texture2D,
272 /// alpha_mode: rusty_dds::AlphaMode::Straight,
273 /// })?.write(&mut bytes)?;
274 ///
275 /// assert!(Dds::read_limited(&bytes[..], 8 * 1024).is_ok());
276 /// assert!(matches!(
277 /// Dds::read_limited(&bytes[..], 16),
278 /// Err(Error::SizeLimitExceeded { .. })
279 /// ));
280 /// # Ok::<(), Error>(())
281 /// ```
282 pub fn read_limited<R: Read>(r: R, max_data_len: usize) -> Result<Dds, Error> {
283 Self::read_inner(r, Some(max_data_len))
284 }
285
286 fn read_inner<R: Read>(mut r: R, max_data_len: Option<usize>) -> Result<Dds, Error> {
287 let (header, header10) = read_headers(&mut r)?;
288 let mut data: Vec<u8> = Vec::new();
289 read_payload(r, &mut data, max_data_len)?;
290 Ok(Dds {
291 header,
292 header10,
293 data,
294 })
295 }
296
297}
298
299impl<'a> DdsView<'a> {
300 /// Parse a DDS **without copying the payload**.
301 ///
302 /// The returned view borrows `bytes` for its lifetime. Everything a
303 /// streaming engine needs — [`DdsBase::surface`],
304 /// [`DdsBase::subresource_range`], [`DdsBase::upload_plan_compressed`],
305 /// decode — works on it exactly as it does on an owned [`Dds`].
306 pub fn parse(bytes: &'a [u8]) -> Result<DdsView<'a>, Error> {
307 let mut cursor = bytes;
308 let magic = cursor.read_u32::<LittleEndian>()?;
309 if magic != Dds::MAGIC {
310 return Err(Error::BadMagicNumber);
311 }
312 let header = Header::read(&mut cursor)?;
313 let header10 = if header.spf.fourcc == Some(FourCC(<FourCC>::DX10)) {
314 Some(Header10::read(&mut cursor)?)
315 } else {
316 None
317 };
318 // `cursor` has been advanced past the headers by the reads above, so
319 // what remains is exactly the payload — borrowed, never copied.
320 Ok(DdsBase {
321 header,
322 header10,
323 data: cursor,
324 })
325 }
326
327 /// Read a DDS from any reader **into a buffer you own and recycle**.
328 ///
329 /// [`DdsView::parse`] is the right call when you already hold the bytes.
330 /// This is for the case where you do not — an archive decompressor, a
331 /// network stream — and would otherwise be forced back onto [`Dds::read`],
332 /// which allocates a fresh payload buffer every time. A fresh buffer is
333 /// faulted in and zeroed by the operating system before it is overwritten,
334 /// which measured at ~87% of that call; reusing one buffer keeps the pages
335 /// resident and the cost is the copy alone.
336 ///
337 /// `buf` is cleared, so its capacity survives and the second call onwards
338 /// touches no new pages. Reuse one buffer per streaming worker.
339 ///
340 /// ```
341 /// use rusty_dds::{DdsView, SubresourceId};
342 ///
343 /// # let mut bytes = Vec::new();
344 /// # rusty_dds::Dds::new_dxgi(rusty_dds::NewDxgiParams {
345 /// # height: 64, width: 64, depth: None,
346 /// # format: rusty_dds::DxgiFormat::BC1_UNorm,
347 /// # mipmap_levels: None, array_layers: None, caps2: None, is_cubemap: false,
348 /// # resource_dimension: rusty_dds::D3D10ResourceDimension::Texture2D,
349 /// # alpha_mode: rusty_dds::AlphaMode::Straight,
350 /// # })?.write(&mut bytes)?;
351 /// let mut buf = Vec::new(); // hoisted out of the loop
352 /// for _ in 0..2 {
353 /// let dds = DdsView::read_into(&bytes[..], &mut buf)?;
354 /// assert_eq!(dds.get_width(), 64);
355 /// }
356 /// # Ok::<(), rusty_dds::Error>(())
357 /// ```
358 pub fn read_into<R: Read>(r: R, buf: &'a mut Vec<u8>) -> Result<DdsView<'a>, Error> {
359 Self::read_into_inner(r, buf, None)
360 }
361
362 /// [`DdsView::read_into`], refusing a payload larger than `max_data_len`.
363 ///
364 /// Same posture as [`Dds::read_limited`]: the limit covers the payload only,
365 /// and an overrun fails closed without buffering the rest. Use this for
366 /// bytes you did not produce — a mod archive, a download.
367 pub fn read_into_limited<R: Read>(
368 r: R,
369 buf: &'a mut Vec<u8>,
370 max_data_len: usize,
371 ) -> Result<DdsView<'a>, Error> {
372 Self::read_into_inner(r, buf, Some(max_data_len))
373 }
374
375 fn read_into_inner<R: Read>(
376 mut r: R,
377 buf: &'a mut Vec<u8>,
378 max_data_len: Option<usize>,
379 ) -> Result<DdsView<'a>, Error> {
380 let (header, header10) = read_headers(&mut r)?;
381 read_payload(r, buf, max_data_len)?;
382 Ok(DdsBase {
383 header,
384 header10,
385 data: &buf[..],
386 })
387 }
388}
389
390impl<D: AsRef<[u8]>> DdsBase<D> {
391 /// Write to a DDS file
392 pub fn write<W: Write>(&self, w: &mut W) -> Result<(), Error> {
393 w.write_u32::<LittleEndian>(Dds::MAGIC)?;
394 self.header.write(w)?;
395 if let Some(ref header10) = self.header10 {
396 header10.write(w)?;
397 }
398 w.write_all(self.data.as_ref())?;
399 Ok(())
400 }
401
402 /// Attempt to get the format of this DDS, presuming it is a D3DFormat.
403 pub fn get_d3d_format(&self) -> Option<D3DFormat> {
404 // FIXME: some d3d formats are equivalent to some dxgi formats.
405 // but we dont have a try_from() between them yet.
406 // Right now we will yield None if the format is dxgi, but
407 // later on we should try to convert.
408
409 D3DFormat::try_from_pixel_format(&self.header.spf)
410 }
411
412 /// Attempt to get the format of this DDS, presuming it is a DxgiFormat.
413 pub fn get_dxgi_format(&self) -> Option<DxgiFormat> {
414 // FIXME: some d3d formats are equivalent to some dxgi formats.
415 // but we dont have a try_from() between them yet.
416 // Right now we will yield None if the format is d3d, but
417 // later on we should try to convert.
418 if let Some(ref h10) = self.header10 {
419 Some(h10.dxgi_format)
420 } else {
421 DxgiFormat::try_from_pixel_format(&self.header.spf)
422 }
423 }
424
425 /// The format by value, without the `Box` that [`Dds::get_format`] costs.
426 ///
427 /// Every internal caller uses this. `get_format` allocates, and it is called
428 /// underneath every subresource offset computation.
429 pub(crate) fn format_of(&self) -> Option<crate::format::FormatOf> {
430 use crate::format::FormatOf;
431 if let Some(dxgi) = self.get_dxgi_format() {
432 return Some(FormatOf::Dxgi(dxgi));
433 }
434 if let Some(d3d) = self.get_d3d_format() {
435 return Some(FormatOf::D3d(d3d));
436 }
437 None
438 }
439
440 /// Get the format of the DDS as a trait (type-erasure)
441 pub fn get_format(&self) -> Option<Box<dyn DataFormat>> {
442 if let Some(dxgi) = self.get_dxgi_format() {
443 Some(Box::new(dxgi))
444 } else if let Some(d3d) = self.get_d3d_format() {
445 Some(Box::new(d3d))
446 } else {
447 None
448 }
449 }
450
451 pub fn get_width(&self) -> u32 {
452 self.header.width
453 }
454
455 pub fn get_height(&self) -> u32 {
456 self.header.height
457 }
458
459 pub fn get_depth(&self) -> u32 {
460 self.header.depth.unwrap_or(1)
461 }
462
463 pub fn get_bits_per_pixel(&self) -> Option<u32> {
464 // Try format first
465 if let Some(format) = self.format_of() {
466 if let Some(bpp) = format.get_bits_per_pixel() {
467 return Some(bpp as u32);
468 }
469 }
470 // Fall back to pixel_format rgb_bit_count field
471 if let Some(bpp) = self.header.spf.rgb_bit_count {
472 return Some(bpp);
473 }
474 None
475 }
476
477 pub fn get_pitch(&self) -> Option<u32> {
478 // Try format first
479 if let Some(format) = self.format_of() {
480 if let Some(pitch) = format.get_pitch(self.header.width) {
481 return Some(pitch);
482 }
483 }
484 // Then try header.pitch
485 if let Some(pitch) = self.header.pitch {
486 return Some(pitch);
487 }
488
489 // Then try to calculate it ourselves
490 if let Some(bpp) = self.get_bits_per_pixel() {
491 // Both operands come from the file; a header that overflows here is
492 // not describing a pitch we can honour.
493 return bpp
494 .checked_mul(self.get_width())
495 .and_then(|n| n.checked_add(7))
496 .map(|n| n / 8);
497 }
498 None
499 }
500
501 pub fn get_pitch_height(&self) -> u32 {
502 if let Some(format) = self.format_of() {
503 format.get_pitch_height()
504 } else {
505 1
506 }
507 }
508
509 pub fn get_main_texture_size(&self) -> Option<u32> {
510 get_texture_size(
511 self.get_pitch(),
512 self.header.linear_size,
513 self.get_pitch_height(),
514 self.header.height,
515 self.header.depth,
516 )
517 }
518
519 pub fn get_array_stride(&self) -> Result<u32, Error> {
520 let size = match self.get_main_texture_size() {
521 Some(s) => s,
522 None => return Err(Error::UnsupportedFormat),
523 };
524 let mml = self.get_num_mipmap_levels();
525 let min_mipmap_size = self.get_min_mipmap_size_in_bytes();
526 Ok(get_array_stride(size, min_mipmap_size, mml))
527 }
528
529 pub fn get_num_array_layers(&self) -> u32 {
530 if let Some(ref h10) = self.header10 {
531 h10.array_size
532 } else if self.header.caps2.contains(Caps2::CUBEMAP) {
533 6
534 } else {
535 1 // just the 1 layer
536 }
537 }
538
539 pub fn get_num_mipmap_levels(&self) -> u32 {
540 if let Some(mmc) = self.header.mip_map_count {
541 mmc
542 } else {
543 1 // just the main image
544 }
545 }
546
547 pub fn get_min_mipmap_size_in_bytes(&self) -> u32 {
548 if let Some(format) = self.format_of() {
549 if let Some(min) = format.get_minimum_mipmap_size_in_bytes() {
550 return min;
551 }
552 }
553 if let Some(bpp) = self.get_bits_per_pixel() {
554 // `bpp` can be the raw `rgb_bit_count` header field, so it is not
555 // bounded by any real format; saturate rather than overflow.
556 bpp.saturating_add(7) / 8
557 } else {
558 1
559 }
560 }
561
562 /// This gets a reference to the data at the given `array_layer` (which should be
563 /// 0 for textures with just one image).
564 pub fn get_data(&self, array_layer: u32) -> Result<&[u8], Error> {
565 let (offset, size) = self.get_offset_and_size(array_layer)?;
566 let offset = offset as usize;
567 let size = size as usize;
568 let end = offset.checked_add(size).ok_or(Error::OutOfBounds)?;
569 self.data.as_ref().get(offset..end).ok_or(Error::OutOfBounds)
570 }
571
572 fn get_offset_and_size(&self, array_layer: u32) -> Result<(u32, u32), Error> {
573 // Verify request bounds
574 if array_layer >= self.get_num_array_layers() {
575 return Err(Error::OutOfBounds);
576 }
577 let array_stride = self.get_array_stride()?;
578 let offset = array_layer
579 .checked_mul(array_stride)
580 .ok_or(Error::OutOfBounds)?;
581
582 Ok((offset, array_stride))
583 }
584}
585
586impl<D: AsRef<[u8]> + AsMut<[u8]>> DdsBase<D> {
587 /// This gets a mutable reference to the data at the given `array_layer`
588 /// (which should be 0 for textures with just one image).
589 ///
590 /// Only available when the payload is owned or mutably borrowed — a
591 /// [`DdsView`] over `&[u8]` cannot offer it.
592 pub fn get_mut_data(&mut self, array_layer: u32) -> Result<&mut [u8], Error> {
593 let (offset, size) = self.get_offset_and_size(array_layer)?;
594 let offset = offset as usize;
595 let size = size as usize;
596 let end = offset.checked_add(size).ok_or(Error::OutOfBounds)?;
597 self.data
598 .as_mut()
599 .get_mut(offset..end)
600 .ok_or(Error::OutOfBounds)
601 }
602}
603
604/// Magic, `DDS_HEADER`, and the DX10 extension when the pixel format asks for it.
605fn read_headers<R: Read>(r: &mut R) -> Result<(Header, Option<Header10>), Error> {
606 let magic = r.read_u32::<LittleEndian>()?;
607 if magic != Dds::MAGIC {
608 return Err(Error::BadMagicNumber);
609 }
610 // Reborrow: `Header::read` takes the reader by value, and `&mut R` is not
611 // `Copy`, so the second read needs a fresh borrow rather than the moved one.
612 let header = Header::read(&mut *r)?;
613 let header10 = if header.spf.fourcc == Some(FourCC(<FourCC>::DX10)) {
614 Some(Header10::read(&mut *r)?)
615 } else {
616 None
617 };
618 Ok((header, header10))
619}
620
621/// Fill `data` with the payload, honouring an optional byte budget.
622///
623/// `data` is cleared, not reallocated: a caller that reuses one buffer across
624/// many textures keeps its pages resident, which is the entire point of
625/// [`DdsView::read_into`].
626fn read_payload<R: Read>(r: R, data: &mut Vec<u8>, max_data_len: Option<usize>) -> Result<(), Error> {
627 data.clear();
628 match max_data_len {
629 None => {
630 let mut r = r;
631 r.read_to_end(data)?;
632 }
633 Some(limit) => {
634 // Read one byte past the budget: if the reader still had bytes to
635 // give, the payload is over the limit and we stop there rather than
636 // buffering the rest.
637 let mut capped = r.take(limit as u64 + 1);
638 capped.read_to_end(data)?;
639 if data.len() > limit {
640 return Err(Error::SizeLimitExceeded {
641 limit,
642 // Only ever `limit + 1` here — the true length is unknown by
643 // construction, and that is the point.
644 at_least: data.len(),
645 });
646 }
647 }
648 }
649 Ok(())
650}
651
652/// Bytes for one mip-0 surface, or `None` when the header's own fields cannot
653/// describe one.
654///
655/// Every input here is an attacker-controlled `u32` straight out of the file,
656/// so the arithmetic is checked throughout: an overflow means the header is
657/// describing a texture that cannot exist, and the honest answer is `None`
658/// (which callers turn into [`Error::UnsupportedFormat`]), not a wrapped size
659/// that would then be used to slice the payload.
660fn get_texture_size(
661 pitch: Option<u32>,
662 linear_size: Option<u32>,
663 pitch_height: u32,
664 height: u32,
665 depth: Option<u32>,
666) -> Option<u32> {
667 let depth = depth.unwrap_or(1);
668
669 if let Some(ls) = linear_size {
670 return Some(ls);
671 }
672 let pitch = pitch?;
673 // A zero pitch height would divide by zero; a format that reports one is
674 // not describing a layout we can compute.
675 if pitch_height == 0 {
676 return None;
677 }
678 let row_height = height.checked_add(pitch_height - 1)? / pitch_height;
679 pitch.checked_mul(row_height)?.checked_mul(depth)
680}
681
682/// Total bytes of one mip chain.
683///
684/// `mipmap_levels` comes from the file, so it can be up to `u32::MAX`. Once the
685/// mip size has bottomed out at `min_mipmap_size` every remaining level
686/// contributes exactly that much, so the tail is computed in closed form rather
687/// than iterated — otherwise a header claiming `mip_map_count = 0xFFFF_FFFF`
688/// would spin for billions of iterations on every metadata query. Accumulation
689/// saturates for the same reason `get_texture_size` is checked: a wrapped
690/// stride would be used to index the payload.
691fn get_array_stride(texture_size: u32, min_mipmap_size: u32, mipmap_levels: u32) -> u32 {
692 let mut stride: u32 = 0;
693 let mut current_mipsize: u32 = texture_size;
694 let mut level: u32 = 0;
695 while level < mipmap_levels {
696 stride = stride.saturating_add(current_mipsize);
697 level += 1;
698 current_mipsize /= 4;
699 if current_mipsize <= min_mipmap_size {
700 let remaining = mipmap_levels - level;
701 return stride.saturating_add(remaining.saturating_mul(min_mipmap_size));
702 }
703 }
704 stride
705}
706
707impl<D: AsRef<[u8]>> fmt::Debug for DdsBase<D> {
708 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
709 writeln!(f, "Dds:")?;
710 if let Some(d3dformat) = self.get_d3d_format() {
711 writeln!(f, " Format: {:?}", d3dformat)?;
712 } else if let Some(dxgiformat) = self.get_dxgi_format() {
713 writeln!(f, " Format: {:?}", dxgiformat)?;
714 } else if let Some(ref fourcc) = self.header.spf.fourcc {
715 writeln!(f, " Format: FOURCC={:?} (Unknown)", fourcc)?;
716 } else {
717 writeln!(f, " Format UNSPECIFIED")?;
718 }
719 write!(f, "{:?}", self.header)?;
720 if let Some(ref h10) = self.header10 {
721 write!(f, "{:?}", h10)?;
722 }
723 writeln!(f, " (data elided)")?;
724 Ok(())
725 }
726}