piaf 0.4.0

A library for reading and interpreting display capability data (EDID).
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
mod metadata;
mod timing;

#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::capabilities::DisplayCapabilities;
use crate::model::capabilities::StaticContext;
use crate::model::diagnostics::EdidWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::diagnostics::ParseWarning;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::extension::ExtensionHandler;
use crate::model::extension::StaticExtensionHandler;
#[cfg(any(feature = "alloc", feature = "std"))]
use crate::model::prelude::{Arc, Vec};

#[cfg(any(feature = "alloc", feature = "std"))]
use metadata::scan_all_metadata_blocks;
use timing::process_data_blocks;

#[cfg(any(feature = "alloc", feature = "std"))]
pub use display_types::DisplayIdCapabilities;

/// Processes DisplayID extension blocks (tag `0x70`).
///
/// A single handler handles both DisplayID 1.x and 2.x — both versions use tag `0x70`
/// and the dispatch layer cannot distinguish them before the handler receives the payload.
/// Version-specific logic is selected internally after inspecting the version byte.
#[derive(Debug)]
pub struct DisplayIdHandler;

/// Minimum version byte for DisplayID 1.x (0x10 = version 1, revision 0).
const DISPLAYID_V1_MIN: u8 = 0x10;
/// Maximum version byte for DisplayID 1.x (0x1F = version 1, revision 15).
const DISPLAYID_V1_MAX: u8 = 0x1F;
/// Version byte for DisplayID 2.x.
const DISPLAYID_V2: u8 = 0x20;

use display_types::displayid::tag;

const TAG_PRODUCT_ID: u8 = tag::PRODUCT_ID;
const TAG_DISPLAY_PARAMS: u8 = tag::DISPLAY_PARAMS;
const TAG_COLOR_CHARACTERISTICS: u8 = tag::COLOR_CHARACTERISTICS;
const TAG_TYPE_I_TIMING: u8 = tag::TYPE_I_TIMING;
const TAG_TYPE_II_TIMING: u8 = tag::TYPE_II_TIMING;
const TAG_TYPE_III_TIMING: u8 = tag::TYPE_III_TIMING;
const TAG_TYPE_IV_TIMING: u8 = tag::TYPE_IV_TIMING;
const TAG_VESA_VIDEO_TIMING: u8 = tag::VESA_VIDEO_TIMING;
const TAG_CTA_VIDEO_TIMING: u8 = tag::CTA_VIDEO_TIMING;
const TAG_VIDEO_TIMING_RANGE: u8 = tag::VIDEO_TIMING_RANGE;
const TAG_SERIAL_NUMBER: u8 = tag::SERIAL_NUMBER;
const TAG_ASCII_STRING: u8 = tag::ASCII_STRING;
const TAG_DISPLAY_DEVICE_DATA: u8 = tag::DISPLAY_DEVICE_DATA;
const TAG_POWER_SEQUENCING: u8 = tag::POWER_SEQUENCING;
const TAG_TRANSFER_CHARACTERISTICS: u8 = tag::TRANSFER_CHARACTERISTICS;
const TAG_DISPLAY_INTERFACE: u8 = tag::DISPLAY_INTERFACE;
const TAG_STEREO_DISPLAY_INTERFACE: u8 = tag::STEREO_DISPLAY_INTERFACE;
const TAG_TYPE_V_TIMING: u8 = tag::TYPE_V_TIMING;
const TAG_TILED_TOPOLOGY: u8 = tag::TILED_TOPOLOGY;
const TAG_TYPE_VI_TIMING: u8 = tag::TYPE_VI_TIMING;

/// Calls `f(tag, revision, block_payload)` for each well-formed data block in `payload`.
///
/// `revision` is the second byte of the 3-byte data block header and carries block-specific
/// flags (e.g., the code-space selector for Type IV timing blocks).
///
/// Stops at the end-of-section sentinel (tag `0x00`, length `0`) or when a block's
/// declared length would extend past the available payload.
///
/// Note: `TAG_PRODUCT_ID` (`0x00`) shares the sentinel's tag byte, but a valid Product
/// Identification Block always has a non-zero length, so the two are unambiguous.
fn for_each_data_block(payload: &[u8], mut f: impl FnMut(u8, u8, &[u8])) {
    let mut offset = 0;
    while offset + 3 <= payload.len() {
        let tag = payload[offset];
        let revision = payload[offset + 1];
        let length = payload[offset + 2] as usize;

        // End-of-section sentinel: tag 0x00 with length 0. Unambiguous because a valid
        // Product Identification Block (also tag 0x00) always has a non-zero length.
        if tag == 0x00 && length == 0 {
            break;
        }

        let block_end = offset + 3 + length;
        if block_end > payload.len() {
            // Malformed block — extends past payload; stop iterating.
            break;
        }

        f(tag, revision, &payload[offset + 3..block_end]);
        offset = block_end;
    }
}

/// Parses the 4-byte section header common to all DisplayID fragments.
///
/// Returns `(version, section_byte_count, product_type, extension_count)`.
/// - `version`: byte 1 of the block (DisplayID version/revision)
/// - `section_byte_count`: byte 2, count of data block bytes in this fragment
/// - `product_type`: bits 2:0 of byte 3 (display product primary use case)
/// - `extension_count`: bits 7:3 of byte 3 (number of continuation blocks after the first)
fn parse_section_header(block: &[u8; 128]) -> (u8, u8, u8, u8) {
    let version = block[1];
    let section_byte_count = block[2];
    let packed = block[3];
    let product_type = packed & 0x07;
    let extension_count = (packed >> 3) & 0x1F;
    (version, section_byte_count, product_type, extension_count)
}

/// Returns the data-block payload slice for a single DisplayID fragment.
///
/// Extracts `block[4..end]` where `end = min(4 + section_byte_count, 127)`.
fn fragment_payload(block: &[u8; 128]) -> &[u8] {
    let section_byte_count = block[2] as usize;
    let end = (4 + section_byte_count).min(127);
    if end > 4 { &block[4..end] } else { &[] }
}

/// Validates the DisplayID section checksum for `block`.
///
/// Returns `None` when the checksum is valid (no warning to emit).
/// Returns `Some(EdidWarning::DisplayIdSectionBytesOutOfRange)` when `section_byte_count`
/// is so large that the checksum position falls outside bytes 1–126 of the block.
/// Returns `Some(EdidWarning::DisplayIdChecksumMismatch)` when the checksum byte does not
/// bring the sum of `block[1..=checksum_pos]` to zero mod 256.
///
/// The checksum byte sits at `block[4 + section_byte_count]`, immediately after the data
/// blocks. A valid section holds at most 122 data bytes, placing the checksum no later
/// than byte 126.
fn check_displayid_section(block: &[u8; 128]) -> Option<EdidWarning> {
    let n = block[2] as usize;
    let checksum_pos = 4 + n;
    if checksum_pos > 126 {
        return Some(EdidWarning::DisplayIdSectionBytesOutOfRange(block[2]));
    }
    let ok = block[1..=checksum_pos]
        .iter()
        .fold(0u8, |acc, &x| acc.wrapping_add(x))
        == 0;
    if ok {
        None
    } else {
        Some(EdidWarning::DisplayIdChecksumMismatch)
    }
}

#[cfg(any(feature = "alloc", feature = "std"))]
impl ExtensionHandler for DisplayIdHandler {
    fn process(
        &self,
        blocks: &[&[u8; 128]],
        caps: &mut DisplayCapabilities,
        warnings: &mut Vec<ParseWarning>,
    ) {
        let Some(first) = blocks.first() else { return };

        let (version, _section_byte_count, product_type, extension_count) =
            parse_section_header(first);

        // Validate version range.
        match version {
            DISPLAYID_V1_MIN..=DISPLAYID_V1_MAX | DISPLAYID_V2 => {}
            v => {
                warnings.push(Arc::new(EdidWarning::DisplayIdVersionUnknown(v)));
                return;
            }
        }

        // Validate extension count vs actual number of continuation blocks.
        let actual_continuation = blocks.len().saturating_sub(1);
        if extension_count as usize != actual_continuation {
            warnings.push(Arc::new(EdidWarning::DisplayIdExtensionCountMismatch {
                declared: extension_count,
                found: actual_continuation.min(u8::MAX as usize) as u8,
            }));
        }

        // Store rich capabilities.
        caps.set_extension_data(0x70, DisplayIdCapabilities::new(version, product_type));

        // Process data blocks from all fragments.
        for block in blocks {
            if let Some(w) = check_displayid_section(block) {
                warnings.push(Arc::new(w));
            }
            let payload = fragment_payload(block);
            process_data_blocks(payload, caps);
            scan_all_metadata_blocks(payload, caps);
        }
    }
}

impl StaticExtensionHandler for DisplayIdHandler {
    fn tag(&self) -> u8 {
        0x70
    }

    fn process(&self, blocks: &[&[u8; 128]], ctx: &mut StaticContext<'_>) {
        let Some(first) = blocks.first() else { return };

        let (version, _section_byte_count, _product_type, extension_count) =
            parse_section_header(first);

        // Validate version range.
        match version {
            DISPLAYID_V1_MIN..=DISPLAYID_V1_MAX | DISPLAYID_V2 => {}
            v => {
                ctx.push_warning(EdidWarning::DisplayIdVersionUnknown(v));
                return;
            }
        }

        // Validate extension count vs actual number of continuation blocks.
        // In bare no_std the dispatch layer calls once per block, so blocks.len() == 1 always;
        // the mismatch warning may fire once per block in that case. In alloc builds the full
        // slice is provided and the warning fires at most once.
        let actual_continuation = blocks.len().saturating_sub(1);
        if extension_count as usize != actual_continuation {
            ctx.push_warning(EdidWarning::DisplayIdExtensionCountMismatch {
                declared: extension_count,
                found: actual_continuation.min(u8::MAX as usize) as u8,
            });
        }

        // Process data blocks from all fragments.
        for block in blocks {
            if let Some(w) = check_displayid_section(block) {
                ctx.push_warning(w);
            }
            process_data_blocks(fragment_payload(block), ctx);
        }
    }
}

/// Data block tags decoded by this handler.
///
/// Must be kept in sync with the tag dispatches in `timing::process_data_blocks`
/// (timing blocks) and `metadata::scan_all_metadata_blocks` (metadata blocks).
/// `test_all_block_tags_accounted_for` verifies that the union of implemented,
/// deferred, and reserved ranges covers every value 0x00–0xFF.
#[cfg(test)]
const IMPLEMENTED_BLOCK_TAGS: &[u8] = &[
    TAG_PRODUCT_ID,               // 0x00 — Product Identification Block
    TAG_DISPLAY_PARAMS,           // 0x01 — Display Parameters Block
    TAG_COLOR_CHARACTERISTICS,    // 0x02 — Color Characteristics Block
    TAG_TYPE_I_TIMING,            // 0x03 — Detailed Timings Block (Type I descriptors)
    TAG_TYPE_II_TIMING,           // 0x04 — Video Timing Modes Type II — Detailed Timings Block
    TAG_TYPE_III_TIMING,          // 0x05 — Video Timing Modes Type III — Short Timings Block
    TAG_TYPE_IV_TIMING,           // 0x06 — Video Timing Modes Type IV — DMT/VIC Code Block
    TAG_VESA_VIDEO_TIMING,        // 0x07 — VESA Video Timing Block (DMT presence bitmap)
    TAG_CTA_VIDEO_TIMING,         // 0x08 — CTA-861 Video Timing Block (VIC presence bitmap)
    TAG_VIDEO_TIMING_RANGE,       // 0x09 — Video Timing Range Limits Block
    TAG_SERIAL_NUMBER,            // 0x0A — Product Serial Number Block
    TAG_ASCII_STRING,             // 0x0B — General Purpose ASCII String Block
    TAG_DISPLAY_DEVICE_DATA,      // 0x0C — Display Device Data Block
    TAG_POWER_SEQUENCING,         // 0x0D — Interface Power Sequencing Block
    TAG_TRANSFER_CHARACTERISTICS, // 0x0E — Transfer Characteristics Block
    TAG_DISPLAY_INTERFACE,        // 0x0F — Display Interface Data Block
    TAG_STEREO_DISPLAY_INTERFACE, // 0x10 — Stereo Display Interface Data Block
    TAG_TYPE_V_TIMING,            // 0x11 — Video Timing Modes Type V — Short Timings Block
    TAG_TILED_TOPOLOGY,           // 0x12 — Tiled Display Topology Data Block
    TAG_TYPE_VI_TIMING,           // 0x13 — Video Timing Modes Type VI — Detailed Timings Block
];

/// DisplayID 1.x data block tags that are defined by the specification but not
/// yet decoded, plus tag ranges reserved or unassigned by the specification.
///
/// Each entry is an inclusive `(first, last)` range. When a new block type is
/// implemented, remove its tag from here and add it to `IMPLEMENTED_BLOCK_TAGS`.
#[cfg(test)]
const DEFERRED_OR_RESERVED_TAG_RANGES: &[(u8, u8)] = &[
    (0x14, 0x7E), // Reserved for future use in DisplayID 1.x
    (0x7F, 0x7F), // Vendor-specific
    (0x80, 0xFF), // Undefined (outside the DisplayID 1.x tag space)
];

/// Pre-built static reference to the built-in DisplayID handler.
///
/// Suitable for inclusion in a `&[&dyn StaticExtensionHandler]` slice alongside
/// [`CEA861_HANDLER`][crate::CEA861_HANDLER].
pub static DISPLAYID_HANDLER: &dyn StaticExtensionHandler = &DisplayIdHandler;

#[cfg(test)]
#[cfg(any(feature = "alloc", feature = "std"))]
mod tests {
    use super::*;
    use crate::model::extension::ExtensionHandler;
    use crate::model::manufacture::ManufacturerId;

    // -----------------------------------------------------------------------
    // Shared test helpers
    // -----------------------------------------------------------------------

    fn make_displayid_block(version: u8, data_blocks: &[u8]) -> [u8; 128] {
        let mut block = [0u8; 128];
        block[0] = 0x70; // extension tag
        block[1] = version;
        let section_byte_count = data_blocks.len().min(122);
        block[2] = section_byte_count as u8;
        block[3] = 0x00; // product_type=0, extension_count=0
        let end = 4 + section_byte_count;
        block[4..end].copy_from_slice(&data_blocks[..section_byte_count]);
        // Set DisplayID section checksum at block[4 + section_byte_count].
        let sum: u8 = block[1..end]
            .iter()
            .fold(0u8, |acc, &x| acc.wrapping_add(x));
        block[end] = 0u8.wrapping_sub(sum);
        // EDID extension block checksum at block[127] (covers all 128 bytes).
        let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
        block[127] = 0u8.wrapping_sub(edid_sum);
        block
    }

    /// Recomputes the DisplayID section checksum and the EDID extension block checksum
    /// after any modification to the block's header or data bytes.
    fn fix_checksums(block: &mut [u8; 128]) {
        let n = block[2] as usize;
        let checksum_pos = 4 + n;
        if checksum_pos <= 126 {
            let sum: u8 = block[1..checksum_pos]
                .iter()
                .fold(0u8, |acc, &x| acc.wrapping_add(x));
            block[checksum_pos] = 0u8.wrapping_sub(sum);
        }
        let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
        block[127] = 0u8.wrapping_sub(edid_sum);
    }

    fn make_type_i_descriptor(
        pixel_clock_10khz: u16,
        h_active: u16,
        h_blank: u16,
        h_fp: u16,
        h_sw: u16,
        v_active: u16,
        v_blank: u16,
        v_fp: u16,
        v_sw: u16,
        flags: u8,
    ) -> [u8; 20] {
        let mut d = [0u8; 20];
        d[0] = 0x00;
        d[1..3].copy_from_slice(&pixel_clock_10khz.to_le_bytes());
        d[3..5].copy_from_slice(&h_active.to_le_bytes());
        d[5..7].copy_from_slice(&h_blank.to_le_bytes());
        d[7..9].copy_from_slice(&h_fp.to_le_bytes());
        d[9..11].copy_from_slice(&h_sw.to_le_bytes());
        d[11..13].copy_from_slice(&v_active.to_le_bytes());
        d[13..15].copy_from_slice(&v_blank.to_le_bytes());
        d[15..17].copy_from_slice(&v_fp.to_le_bytes());
        d[17..19].copy_from_slice(&v_sw.to_le_bytes());
        d[19] = flags;
        d
    }

    fn make_type_i_data_block(descriptor: &[u8; 20]) -> [u8; 23] {
        let mut db = [0u8; 23];
        db[0] = TAG_TYPE_I_TIMING;
        db[1] = 0x00;
        db[2] = 20;
        db[3..23].copy_from_slice(descriptor);
        db
    }

    // -----------------------------------------------------------------------
    // Handler-level tests (warnings, capabilities, multi-fragment)
    // -----------------------------------------------------------------------

    #[test]
    fn test_unknown_version_emits_warning() {
        let block = make_displayid_block(0x05, &[]);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert_eq!(warnings.len(), 1);
        let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
        assert_eq!(*w, EdidWarning::DisplayIdVersionUnknown(0x05));
    }

    #[test]
    fn test_extension_count_mismatch_warning() {
        // Declare 1 continuation block but provide none.
        let mut block = make_displayid_block(0x10, &[]);
        block[3] = 0x08; // extension_count = 1, product_type = 0
        fix_checksums(&mut block);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert_eq!(warnings.len(), 1);
        let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
        assert_eq!(
            *w,
            EdidWarning::DisplayIdExtensionCountMismatch {
                declared: 1,
                found: 0
            }
        );
    }

    #[test]
    fn test_v2_accepted_without_warning() {
        let block = make_displayid_block(0x20, &[]);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert!(warnings.is_empty());
    }

    #[test]
    fn test_displayid_capabilities_stored() {
        // version 0x13 (DisplayID 1.3), product_type = 2 (packed in byte 3 as 0x02)
        let mut block = make_displayid_block(0x13, &[]);
        block[3] = 0x02; // extension_count=0, product_type=2
        fix_checksums(&mut block);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert!(warnings.is_empty());
        let did = caps
            .get_extension_data::<DisplayIdCapabilities>(0x70)
            .unwrap();
        assert_eq!(did.version, 0x13);
        assert_eq!(did.product_type, 2);
    }

    #[test]
    fn test_multi_fragment_reassembly() {
        // First fragment: 1920×1080@60. Declares one continuation block.
        let desc1 = make_type_i_descriptor(14850, 1920, 280, 88, 44, 1080, 45, 4, 5, 0x00);
        let db1 = make_type_i_data_block(&desc1);
        let mut block1 = make_displayid_block(0x10, &db1);
        block1[3] = 0x08; // extension_count=1 (bits 7:3), product_type=0
        fix_checksums(&mut block1);

        // Second fragment (continuation): 2560×1440@60.
        let desc2 = make_type_i_descriptor(22118, 2560, 440, 80, 32, 1440, 41, 4, 5, 0x00);
        let db2 = make_type_i_data_block(&desc2);
        let block2 = make_displayid_block(0x10, &db2);

        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(
            &DisplayIdHandler,
            &[&block1, &block2],
            &mut caps,
            &mut warnings,
        );

        assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
        assert_eq!(caps.supported_modes.len(), 2);
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 1920 && m.height == 1080)
        );
        assert!(
            caps.supported_modes
                .iter()
                .any(|m| m.width == 2560 && m.height == 1440)
        );
    }

    #[test]
    fn test_valid_checksum_no_warning() {
        // make_displayid_block already sets a valid checksum; no warning expected.
        let block = make_displayid_block(0x10, &[]);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert!(warnings.is_empty(), "unexpected warnings: {:?}", warnings);
    }

    #[test]
    fn test_invalid_checksum_emits_warning() {
        let mut block = make_displayid_block(0x10, &[]);
        // Corrupt the DisplayID section checksum (at block[4 + section_byte_count]).
        let n = block[2] as usize;
        block[4 + n] ^= 0xFF;
        // Also fix the EDID extension block checksum so parsing reaches our code.
        let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
        block[127] = 0u8.wrapping_sub(edid_sum);

        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);
        assert_eq!(warnings.len(), 1, "expected exactly one warning");
        let w = (*warnings[0]).downcast_ref::<EdidWarning>().unwrap();
        assert_eq!(*w, EdidWarning::DisplayIdChecksumMismatch);
    }

    #[test]
    fn test_invalid_checksum_static_pipeline_emits_warning() {
        let mut block = make_displayid_block(0x10, &[]);
        let n = block[2] as usize;
        block[4 + n] ^= 0xFF;
        let edid_sum: u8 = block[..127].iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
        block[127] = 0u8.wrapping_sub(edid_sum);

        let mut caps = crate::model::StaticDisplayCapabilities::<4>::default();
        let mut ctx = crate::model::StaticContext::new(&mut caps);
        StaticExtensionHandler::process(&DisplayIdHandler, &[&block], &mut ctx);
        assert_eq!(caps.num_warnings, 1, "expected exactly one warning");
        assert_eq!(
            caps.warnings[0],
            Some(EdidWarning::DisplayIdChecksumMismatch)
        );
    }

    #[test]
    fn test_product_id_and_timing_in_same_block() {
        // Product ID block followed by a Type I timing block — both must be decoded.
        let ca = (b'S' - b'A' + 1) as u16;
        let cb = (b'A' - b'A' + 1) as u16;
        let cc = (b'M' - b'A' + 1) as u16;
        let packed: u16 = (ca << 10) | (cb << 5) | cc;
        let mut pid_payload = Vec::new();
        pid_payload.extend_from_slice(&packed.to_be_bytes());
        pid_payload.extend_from_slice(&0xABCDu16.to_le_bytes());
        pid_payload.extend_from_slice(&0u32.to_le_bytes()); // serial = 0
        pid_payload.push(0); // week
        pid_payload.push(0); // year
        let mut pid_db = vec![TAG_PRODUCT_ID, 0x00, pid_payload.len() as u8];
        pid_db.extend_from_slice(&pid_payload);

        let desc = make_type_i_descriptor(14850, 1920, 280, 88, 44, 1080, 45, 4, 5, 0x00);
        let timing_db = make_type_i_data_block(&desc);

        let mut payload = Vec::new();
        payload.extend_from_slice(&pid_db);
        payload.extend_from_slice(&timing_db);

        let block = make_displayid_block(0x10, &payload);
        let mut caps = DisplayCapabilities::default();
        let mut warnings: Vec<ParseWarning> = Vec::new();
        ExtensionHandler::process(&DisplayIdHandler, &[&block], &mut caps, &mut warnings);

        assert!(warnings.is_empty());
        assert_eq!(caps.manufacturer, Some(ManufacturerId(*b"SAM")));
        assert_eq!(caps.product_code, Some(0xABCD));
        assert_eq!(caps.supported_modes.len(), 1);
        assert_eq!(caps.supported_modes[0].width, 1920);
    }

    // -----------------------------------------------------------------------
    // Block tag coverage
    // -----------------------------------------------------------------------

    #[test]
    fn test_all_block_tags_accounted_for() {
        for tag in 0u16..=255 {
            let tag = tag as u8;
            let implemented = IMPLEMENTED_BLOCK_TAGS.contains(&tag);
            let deferred_or_reserved = DEFERRED_OR_RESERVED_TAG_RANGES
                .iter()
                .any(|&(lo, hi)| tag >= lo && tag <= hi);
            assert!(
                implemented || deferred_or_reserved,
                "DisplayID block tag 0x{:02X} is unaccounted for: \
                 add it to IMPLEMENTED_BLOCK_TAGS or DEFERRED_OR_RESERVED_TAG_RANGES",
                tag
            );
        }
    }

    #[test]
    fn test_implemented_and_deferred_are_disjoint() {
        for &tag in IMPLEMENTED_BLOCK_TAGS {
            let in_deferred = DEFERRED_OR_RESERVED_TAG_RANGES
                .iter()
                .any(|&(lo, hi)| tag >= lo && tag <= hi);
            assert!(
                !in_deferred,
                "DisplayID block tag 0x{:02X} appears in both IMPLEMENTED_BLOCK_TAGS \
                 and DEFERRED_OR_RESERVED_TAG_RANGES",
                tag
            );
        }
    }
}