Skip to main content

ktx2/
lib.rs

1//! Parser for the [ktx2](https://github.khronos.org/KTX-Specification/ktxspec.v2.html) texture container format.
2//!
3//! ## Features
4//! - [x] Async reading
5//! - [x] Parsing
6//! - [x] Validating
7//! - [x] [Data format description](https://github.khronos.org/KTX-Specification/ktxspec.v2.html#_data_format_descriptor)
8//! - [x] [Key/value data](https://github.khronos.org/KTX-Specification/ktxspec.v2.html#_keyvalue_data)
9//!
10//! ## Example
11//! ```rust
12//! // Crate instance of reader. This validates the header
13//! # let file = include_bytes!("../data/test_tex.ktx2");
14//! let mut reader = ktx2::Reader::new(file).expect("Can't create reader"); // Crate instance of reader.
15//!
16//! // Get general texture information.
17//! let header = reader.header();
18//!
19//! // Read iterator over slices of each mipmap level.
20//! let levels = reader.levels().collect::<Vec<_>>();
21//! # let _ = (header, levels);
22//! ```
23//!
24//! ## MSRV
25//!
26//! The minimum supported Rust version is 1.56. MSRV bumps are treated as breaking changes.
27
28#![no_std]
29
30extern crate alloc;
31
32#[cfg(feature = "std")]
33extern crate std;
34
35pub mod dfd;
36mod enums;
37mod error;
38mod util;
39
40pub use crate::{
41    enums::{ColorModel, ColorPrimaries, Format, SupercompressionScheme, TransferFunction},
42    error::ParseError,
43};
44
45use alloc::vec::Vec;
46use core::convert::TryInto;
47
48/// Parses and validates a KTX2 texture container from an in-memory buffer.
49///
50/// All validation (magic bytes, bounds checks, DFD integrity, level index) is
51/// performed eagerly in [`Reader::new`]. Subsequent accessors are infallible.
52///
53/// `Data` can be any type that derefs to `[u8]` — `&[u8]`, `Vec<u8>`,
54/// `Arc<[u8]>`, etc.
55pub struct Reader<Data: AsRef<[u8]>> {
56    input: Data,
57    header: Header,
58    dfd_blocks: Vec<dfd::Block>,
59}
60
61impl<Data: AsRef<[u8]>> Reader<Data> {
62    /// Parse and validate a KTX2 buffer.
63    ///
64    /// Validates the header magic, all section bounds, the DFD, and the level
65    /// index. Returns [`ParseError`] on any structural problem.
66    pub fn new(input: Data) -> Result<Self, ParseError> {
67        let header_data = input
68            .as_ref()
69            .get(0..Header::LENGTH)
70            .ok_or(ParseError::UnexpectedEnd)?
71            .try_into()
72            .unwrap();
73        let header = Header::from_bytes(header_data)?;
74
75        // Check DFD bounds
76        let dfd_start = header
77            .index
78            .dfd_byte_offset
79            .checked_add(4)
80            .ok_or(ParseError::UnexpectedEnd)?;
81        let dfd_end = header
82            .index
83            .dfd_byte_offset
84            .checked_add(header.index.dfd_byte_length)
85            .ok_or(ParseError::UnexpectedEnd)?;
86        if dfd_end < dfd_start || dfd_end as usize >= input.as_ref().len() {
87            return Err(ParseError::UnexpectedEnd);
88        }
89
90        // Check SGD bounds
91        if header
92            .index
93            .sgd_byte_offset
94            .checked_add(header.index.sgd_byte_length)
95            .ok_or(ParseError::UnexpectedEnd)?
96            >= input.as_ref().len() as u64
97        {
98            return Err(ParseError::UnexpectedEnd);
99        }
100
101        // Check KVD bounds
102        if header
103            .index
104            .kvd_byte_offset
105            .checked_add(header.index.kvd_byte_length)
106            .ok_or(ParseError::UnexpectedEnd)? as usize
107            >= input.as_ref().len()
108        {
109            return Err(ParseError::UnexpectedEnd);
110        }
111
112        let mut result = Self {
113            input,
114            header,
115            // 1 is the most likely length, as 99.99% of KTX2 files have exactly 1 DFD block.
116            dfd_blocks: Vec::with_capacity(1),
117        };
118        result.parse_dfd()?;
119        // Creating the iterator validates the integrity of the level index.
120        let index = result.level_index()?;
121
122        // Check level data bounds
123        for level in index {
124            if level
125                .byte_offset
126                .checked_add(level.byte_length)
127                .ok_or(ParseError::UnexpectedEnd)?
128                > result.input.as_ref().len() as u64
129            {
130                return Err(ParseError::UnexpectedEnd);
131            }
132        }
133
134        Ok(result)
135    }
136
137    /// Eagerly parses all DFD blocks from the DFD section into `self.dfd_blocks`.
138    /// Fails if the section contains a partial or malformed block.
139    fn parse_dfd(&mut self) -> ParseResult<()> {
140        let dfd_start = self.header.index.dfd_byte_offset as usize;
141        let dfd_end = (self.header.index.dfd_byte_offset + self.header.index.dfd_byte_length) as usize;
142        // Skip the 4-byte DFD total length field
143        let mut data = &self.input.as_ref()[dfd_start + 4..dfd_end];
144
145        // If we ever encounter a partial DFD, we want to throw an error,
146        // not silently ignore the rest of the DFD data. We should end up consuming
147        // all of the DFD data. If we end up with unconsumed DFD data, we let
148        // dfd::Block::parse throw an error.
149        while !data.is_empty() {
150            let (block, consumed) = dfd::Block::parse(data)?;
151            self.dfd_blocks.push(block);
152            data = &data[consumed..];
153        }
154
155        Ok(())
156    }
157
158    /// Parses the level index table that immediately follows the 80-byte header.
159    /// Each entry is 24 bytes. Used internally; prefer [`levels`](Self::levels) for data access.
160    fn level_index(&self) -> ParseResult<impl ExactSizeIterator<Item = LevelIndex> + '_> {
161        let level_count = self.header().level_count.max(1) as usize;
162
163        let level_index_end_byte = Header::LENGTH
164            .checked_add(
165                level_count
166                    .checked_mul(LevelIndex::LENGTH)
167                    .ok_or(ParseError::UnexpectedEnd)?,
168            )
169            .ok_or(ParseError::UnexpectedEnd)?;
170        let level_index_bytes = self
171            .input
172            .as_ref()
173            .get(Header::LENGTH..level_index_end_byte)
174            .ok_or(ParseError::UnexpectedEnd)?;
175        Ok(level_index_bytes.chunks_exact(LevelIndex::LENGTH).map(|data| {
176            let level_data = data.try_into().unwrap();
177            LevelIndex::from_bytes(&level_data)
178        }))
179    }
180
181    /// The raw KTX2 file bytes backing this reader.
182    pub fn data(&self) -> &[u8] {
183        self.input.as_ref()
184    }
185
186    /// Container-level metadata (dimensions, format, compression, etc.).
187    pub fn header(&self) -> Header {
188        self.header
189    }
190
191    /// The color primaries used by this image (e.g. BT.709, BT.2020, etc.).
192    ///
193    /// Shorthand for [`dfd::Basic::color_primaries`]. Returns `None` if there is no basic DFD block.
194    pub fn color_primaries(&self) -> Option<ColorPrimaries> {
195        self.basic_dfd()?.color_primaries
196    }
197
198    /// The transfer function used by this image (e.g. Linear, sRGB, PQ, etc.).
199    ///
200    /// Shorthand for [`dfd::Basic::transfer_function`]. Returns `None` if there is no basic DFD block.
201    pub fn transfer_function(&self) -> Option<TransferFunction> {
202        self.basic_dfd()?.transfer_function
203    }
204
205    /// The color model used by this image (e.g. RGB, YUV, etc.). Note
206    /// that compressed formats will have a dedicated color model (e.g. BC1, ASTC)
207    /// rather than RGB, even if the uncompressed data would be RGB.
208    ///
209    /// Shorthand for [`dfd::Basic::color_model`]. Returns `None` if there is no basic DFD block.
210    pub fn color_model(&self) -> Option<ColorModel> {
211        self.basic_dfd()?.color_model
212    }
213
214    /// The alpha premuliplication state of the image. `true` if the RGB channels
215    /// are premultiplied by alpha, `false` if not.
216    ///
217    /// Shorthand for [`dfd::Basic::flags`]'s [`dfd::DataFormatFlags::ALPHA_PREMULTIPLIED`] flag.
218    /// Returns `None` if there is no basic DFD block.
219    pub fn is_alpha_premultiplied(&self) -> Option<bool> {
220        Some(
221            self.basic_dfd()?
222                .flags
223                .contains(dfd::DataFormatFlags::ALPHA_PREMULTIPLIED),
224        )
225    }
226
227    /// The program used to write this file, if specified by this file.
228    ///
229    /// Shorthand for retrieving the `KTXwriter` key from the key/value data.
230    ///
231    /// Returns None if:
232    /// - The file doesn't contain a `KTXwriter` key.
233    /// - The `KTXwriter` value is not valid UTF-8.
234    pub fn writer(&self) -> Option<&str> {
235        self.key_value_data()
236            .find(|(key, _)| *key == "KTXwriter")
237            .and_then(|(_, value)| core::str::from_utf8(value).ok())
238    }
239
240    /// Iterator over the texture's mip levels, ordered largest to smallest
241    /// (level 0 first, level *N-1* last). Each [`Level`] contains the raw
242    /// (possibly supercompressed) bytes for that level.
243    pub fn levels(&self) -> impl ExactSizeIterator<Item = Level<'_>> + '_ {
244        self.level_index().unwrap().map(move |level| Level {
245            // Bounds-checking previously performed in `new`
246            data: &self.input.as_ref()[level.byte_offset as usize..(level.byte_offset + level.byte_length) as usize],
247            uncompressed_byte_length: level.uncompressed_byte_length,
248        })
249    }
250
251    /// Supercompression Global Data (SGD) section. Currently only used by
252    /// BasisLZ (scheme 1) for codebooks and image descriptors. Empty for
253    /// other schemes.
254    pub fn supercompression_global_data(&self) -> &[u8] {
255        let header = self.header();
256        let start = header.index.sgd_byte_offset as usize;
257        // Bounds-checking previously performed in `new`
258        let end = (header.index.sgd_byte_offset + header.index.sgd_byte_length) as usize;
259        &self.input.as_ref()[start..end]
260    }
261
262    /// The Data Format Descriptor blocks. Most KTX2 files contain exactly
263    /// one [`dfd::Block::Basic`] block. Use this to inspect color model,
264    /// transfer function, primaries, and per-sample layout.
265    pub fn dfd_blocks(&self) -> &[dfd::Block] {
266        &self.dfd_blocks
267    }
268
269    /// The first [`dfd::Basic`] block, if present.
270    ///
271    /// Nearly all KTX2 files contain exactly one basic DFD block. Returns
272    /// `None` only for files that exclusively use non-standard
273    /// descriptor blocks.
274    pub fn basic_dfd(&self) -> Option<&dfd::Basic> {
275        self.dfd_blocks.iter().find_map(|block| match block {
276            dfd::Block::Basic(basic) => Some(basic),
277            _ => None,
278        })
279    }
280
281    /// Iterator over key/value metadata pairs. Keys are UTF-8 strings;
282    /// values are raw bytes (often NUL-terminated UTF-8, but not always).
283    ///
284    /// # Standard Keys
285    ///
286    /// The KTX specification defines a number of standard keys. Most commonly,
287    /// the `KTXwriter` key is used to indicate the tool that wrote the file.
288    ///
289    /// For a full list of standard keys, see the [KTX specification](https://github.khronos.org/KTX-Specification/ktxspec.v2.html#_keyvalue_data).
290    pub fn key_value_data(&self) -> KeyValueDataIterator<'_> {
291        let header = self.header();
292
293        let start = header.index.kvd_byte_offset as usize;
294        // Bounds-checking previously performed in `new`
295        let end = (header.index.kvd_byte_offset + header.index.kvd_byte_length) as usize;
296
297        KeyValueDataIterator::new(&self.input.as_ref()[start..end])
298    }
299}
300
301/// Iterator over KTX2 key/value metadata pairs.
302///
303/// Each item is `(key, value)` where `key` is a UTF-8 string and `value` is
304/// raw bytes (often UTF-8, but not guaranteed). Malformed entries are silently
305/// skipped. Prefer [`Reader::key_value_data`] over constructing this directly.
306pub struct KeyValueDataIterator<'data> {
307    data: &'data [u8],
308}
309
310impl<'data> KeyValueDataIterator<'data> {
311    /// Create a new iterator from the raw key/value data section bytes.
312    ///
313    /// The slice should span from [`Index::kvd_byte_offset`] to
314    /// `kvd_byte_offset + kvd_byte_length` relative to the start of the file.
315    /// Prefer [`Reader::key_value_data`] which handles this for you.
316    pub fn new(data: &'data [u8]) -> Self {
317        Self { data }
318    }
319}
320
321impl<'data> Iterator for KeyValueDataIterator<'data> {
322    type Item = (&'data str, &'data [u8]);
323
324    fn next(&mut self) -> Option<Self::Item> {
325        let mut offset = 0;
326
327        loop {
328            let length = util::bytes_to_u32(self.data, &mut offset).ok()?;
329
330            let start_offset = offset;
331
332            offset = offset.checked_add(length as usize)?;
333
334            let end_offset = offset;
335
336            // Ensure that we're 4-byte aligned
337            if offset % 4 != 0 {
338                offset += 4 - (offset % 4);
339            }
340
341            let key_and_value = match self.data.get(start_offset..end_offset) {
342                Some(key_and_value) => key_and_value,
343                None => continue,
344            };
345
346            // The key is terminated with a NUL character.
347            let key_end_index = match key_and_value.iter().position(|&c| c == b'\0') {
348                Some(index) => index,
349                None => continue,
350            };
351
352            let key = &key_and_value[..key_end_index];
353            let value = &key_and_value[key_end_index + 1..];
354
355            let key = match core::str::from_utf8(key) {
356                Ok(key) => key,
357                Err(_) => continue,
358            };
359
360            self.data = self.data.get(offset..).unwrap_or_default();
361
362            return Some((key, value));
363        }
364    }
365}
366
367/// 12-byte file identifier: `«KTX 20»\r\n\x1A\n`. Must appear at offset 0.
368pub const MAGIC: [u8; 12] = [0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A];
369
370/// Result of parsing data operation.
371type ParseResult<T> = Result<T, ParseError>;
372
373/// Container-level metadata (dimensions, format, layout) from the 80-byte KTX2 file header.
374#[derive(Copy, Clone, Eq, PartialEq, Debug)]
375pub struct Header {
376    /// Vulkan `VkFormat` enum value. `None` means `VK_FORMAT_UNDEFINED`,
377    /// used for supercompressed universal formats (e.g. Basis Universal)
378    /// where the actual format is determined at transcode time.
379    pub format: Option<Format>,
380    /// Size in bytes of the data type used for GPU upload, for endian conversion
381    /// on big-endian systems (all image data in KTX2 is little-endian).
382    ///
383    /// Must be `1` when `format` is `None` (VK_FORMAT_UNDEFINED) or for
384    /// block-compressed formats (`_BLOCK` suffix). For packed formats
385    /// (`_PACKxx`), equals the byte size of the packed unit `xx / 8`
386    /// (e.g. `4` for `_PACK32`). For unpacked formats, equals the byte size
387    /// of a single component (e.g. `2` for `R16G16B16_UNORM`). For combined
388    /// depth/stencil: `2` for `D16_UNORM_S8_UINT`, `4` for all others.
389    pub type_size: u32,
390    /// Texture width in texels. Always non-zero.
391    pub pixel_width: u32,
392    /// Texture height in texels. `0` for 1D textures.
393    pub pixel_height: u32,
394    /// Texture depth in texels. `0` for non-3D textures.
395    pub pixel_depth: u32,
396    /// Number of array layers. `0` means a non-array texture (1 implicit
397    /// layer). Use `layer_count.max(1)` when allocating storage.
398    pub layer_count: u32,
399    /// Number of cubemap faces. `6` for cubemaps, `1` otherwise.
400    pub face_count: u32,
401    /// Number of mip levels. `0` means the full mip chain should be
402    /// generated from level 0 by the application if needed.
403    /// Use `level_count.max(1)` when iterating stored levels.
404    pub level_count: u32,
405    /// Compression applied to mip level data. `None` means uncompressed.
406    /// When set, each [`Level::data`] must be decompressed before use.
407    pub supercompression_scheme: Option<SupercompressionScheme>,
408    /// Raw byte offsets/lengths for the DFD, KVD, and SGD sections.
409    /// For most use cases, prefer [`Reader::dfd_blocks`],
410    /// [`Reader::key_value_data`], and
411    /// [`Reader::supercompression_global_data`] instead.
412    pub index: Index,
413}
414
415/// Byte offsets and lengths (from start of file) for the DFD, KVD, and SGD sections.
416///
417/// You typically don't need these directly — use [`Reader::dfd_blocks`],
418/// [`Reader::key_value_data`], and [`Reader::supercompression_global_data`] instead.
419#[derive(Copy, Clone, Eq, PartialEq, Debug)]
420pub struct Index {
421    /// Byte offset of the Data Format Descriptor section.
422    pub dfd_byte_offset: u32,
423    /// Byte length of the Data Format Descriptor section.
424    pub dfd_byte_length: u32,
425    /// Byte offset of the Key/Value Data section.
426    pub kvd_byte_offset: u32,
427    /// Byte length of the Key/Value Data section. `0` if absent.
428    pub kvd_byte_length: u32,
429    /// Byte offset of the Supercompression Global Data section.
430    pub sgd_byte_offset: u64,
431    /// Byte length of the Supercompression Global Data section. `0` if absent.
432    pub sgd_byte_length: u64,
433}
434
435impl Header {
436    /// Size of the KTX2 header in bytes (12-byte magic + 68-byte fields).
437    pub const LENGTH: usize = 80;
438
439    /// Decode a header from exactly 80 bytes. Validates the magic bytes and
440    /// rejects zero `pixel_width` or zero `face_count`.
441    pub fn from_bytes(data: &[u8; Self::LENGTH]) -> ParseResult<Self> {
442        if !data.starts_with(&MAGIC) {
443            return Err(ParseError::BadMagic);
444        }
445
446        let header = Self {
447            format: Format::new(u32::from_le_bytes(data[12..16].try_into().unwrap())),
448            type_size: u32::from_le_bytes(data[16..20].try_into().unwrap()),
449            pixel_width: u32::from_le_bytes(data[20..24].try_into().unwrap()),
450            pixel_height: u32::from_le_bytes(data[24..28].try_into().unwrap()),
451            pixel_depth: u32::from_le_bytes(data[28..32].try_into().unwrap()),
452            layer_count: u32::from_le_bytes(data[32..36].try_into().unwrap()),
453            face_count: u32::from_le_bytes(data[36..40].try_into().unwrap()),
454            level_count: u32::from_le_bytes(data[40..44].try_into().unwrap()),
455            supercompression_scheme: SupercompressionScheme::new(u32::from_le_bytes(data[44..48].try_into().unwrap())),
456            index: Index {
457                dfd_byte_offset: u32::from_le_bytes(data[48..52].try_into().unwrap()),
458                dfd_byte_length: u32::from_le_bytes(data[52..56].try_into().unwrap()),
459                kvd_byte_offset: u32::from_le_bytes(data[56..60].try_into().unwrap()),
460                kvd_byte_length: u32::from_le_bytes(data[60..64].try_into().unwrap()),
461                sgd_byte_offset: u64::from_le_bytes(data[64..72].try_into().unwrap()),
462                sgd_byte_length: u64::from_le_bytes(data[72..80].try_into().unwrap()),
463            },
464        };
465
466        if header.pixel_width == 0 {
467            return Err(ParseError::ZeroWidth);
468        }
469        if header.face_count == 0 {
470            return Err(ParseError::ZeroFaceCount);
471        }
472
473        Ok(header)
474    }
475
476    /// Serialize this header back to 80 bytes (including magic).
477    pub fn as_bytes(&self) -> [u8; Self::LENGTH] {
478        let mut bytes = [0; Self::LENGTH];
479
480        let format = self.format.map(|format| format.value()).unwrap_or(0);
481        let supercompression_scheme = self.supercompression_scheme.map(|scheme| scheme.value()).unwrap_or(0);
482
483        bytes[0..12].copy_from_slice(&MAGIC);
484        bytes[12..16].copy_from_slice(&format.to_le_bytes()[..]);
485        bytes[16..20].copy_from_slice(&self.type_size.to_le_bytes()[..]);
486        bytes[20..24].copy_from_slice(&self.pixel_width.to_le_bytes()[..]);
487        bytes[24..28].copy_from_slice(&self.pixel_height.to_le_bytes()[..]);
488        bytes[28..32].copy_from_slice(&self.pixel_depth.to_le_bytes()[..]);
489        bytes[32..36].copy_from_slice(&self.layer_count.to_le_bytes()[..]);
490        bytes[36..40].copy_from_slice(&self.face_count.to_le_bytes()[..]);
491        bytes[40..44].copy_from_slice(&self.level_count.to_le_bytes()[..]);
492        bytes[44..48].copy_from_slice(&supercompression_scheme.to_le_bytes()[..]);
493        bytes[48..52].copy_from_slice(&self.index.dfd_byte_offset.to_le_bytes()[..]);
494        bytes[52..56].copy_from_slice(&self.index.dfd_byte_length.to_le_bytes()[..]);
495        bytes[56..60].copy_from_slice(&self.index.kvd_byte_offset.to_le_bytes()[..]);
496        bytes[60..64].copy_from_slice(&self.index.kvd_byte_length.to_le_bytes()[..]);
497        bytes[64..72].copy_from_slice(&self.index.sgd_byte_offset.to_le_bytes()[..]);
498        bytes[72..80].copy_from_slice(&self.index.sgd_byte_length.to_le_bytes()[..]);
499
500        bytes
501    }
502}
503
504/// A single mip level's data, returned by [`Reader::levels`].
505///
506/// If [`Header::supercompression_scheme`] is `Some`, `data` is still
507/// compressed — decompress it (e.g. via zstd/zlib) before interpreting
508/// the texels according to [`Header::format`].
509pub struct Level<'a> {
510    /// Raw (possibly supercompressed) bytes for this mip level.
511    ///
512    /// After decompression, the data is laid out as:
513    /// `layer → face → z-slice → row → texel/block`, where layers come
514    /// from `layer_count` (1 if 0), faces from `face_count` (6 for
515    /// cubemaps), and z-slices from `pixel_depth` (for 3D textures).
516    pub data: &'a [u8],
517    /// Size of `data` after decompression. Equals `data.len()` when no
518    /// supercompression is applied. `0` for BasisLZ (transcode instead).
519    pub uncompressed_byte_length: u64,
520}
521
522/// Offsets dictating the location of a [`Level`] within a file.
523///
524/// This is mainly useful for writing or low-level manipulation. Prefer [`Reader::levels`] for data access.
525#[derive(Debug, Eq, PartialEq, Copy, Clone)]
526pub struct LevelIndex {
527    /// Byte offset from the start of the file to this level's data.
528    pub byte_offset: u64,
529    /// Byte length of the (possibly supercompressed) level data.
530    pub byte_length: u64,
531    /// Byte length after decompression. `0` for BasisLZ.
532    pub uncompressed_byte_length: u64,
533}
534
535impl LevelIndex {
536    /// Size of one level index entry in bytes.
537    pub const LENGTH: usize = 24;
538
539    /// Decode a level index entry from 24 little-endian bytes.
540    pub fn from_bytes(data: &[u8; Self::LENGTH]) -> Self {
541        Self {
542            byte_offset: u64::from_le_bytes(data[0..8].try_into().unwrap()),
543            byte_length: u64::from_le_bytes(data[8..16].try_into().unwrap()),
544            uncompressed_byte_length: u64::from_le_bytes(data[16..24].try_into().unwrap()),
545        }
546    }
547
548    /// Serialize this entry back to 24 little-endian bytes.
549    pub fn as_bytes(&self) -> [u8; Self::LENGTH] {
550        let mut bytes = [0; Self::LENGTH];
551
552        bytes[0..8].copy_from_slice(&self.byte_offset.to_le_bytes()[..]);
553        bytes[8..16].copy_from_slice(&self.byte_length.to_le_bytes()[..]);
554        bytes[16..24].copy_from_slice(&self.uncompressed_byte_length.to_le_bytes()[..]);
555
556        bytes
557    }
558}
559
560#[cfg(test)]
561mod test {
562    use super::*;
563
564    #[test]
565    #[allow(clippy::octal_escapes)]
566    fn test_malformed_key_value_data_handling() {
567        let data = [
568            &0_u32.to_le_bytes()[..],
569            // Regular key-value pair
570            &7_u32.to_le_bytes()[..],
571            b"xyz\0123 ",
572            // Malformed key-value pair with missing NUL byte
573            &11_u32.to_le_bytes()[..],
574            b"abcdefghi!! ",
575            // Regular key-value pair again
576            &7_u32.to_le_bytes()[..],
577            b"abc\0987",
578            &1000_u32.to_le_bytes()[..],
579            &[1; 1000],
580            &u32::MAX.to_le_bytes()[..],
581        ];
582
583        let mut iterator = KeyValueDataIterator { data: &data.concat() };
584
585        assert_eq!(iterator.next(), Some(("xyz", &b"123"[..])));
586        assert_eq!(iterator.next(), Some(("abc", &b"987"[..])));
587        assert_eq!(iterator.next(), None);
588    }
589}