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}