Skip to main content

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}