krafka 0.8.0

A pure Rust, async-native Apache Kafka client
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
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
//! Kafka protocol implementation.
//!
//! This module implements the Kafka wire protocol, including:
//! - Request/response framing
//! - Message encoding and decoding
//! - API version negotiation
//! - Record batch handling
//!
//! # Version Negotiation
//!
//! Krafka supports automatic API version negotiation with Kafka brokers.
//! On connection, the client fetches supported API versions from the broker
//! and negotiates the highest mutually supported version for each API.
//!
//! ## Client Supported Versions
//!
//! | API | Min | Max | Notes |
//! |-----|-----|-----|-------|
//! | Produce | 3 | 11 | v3+ for transactions, v9 flexible encoding, v11 ZStd compression |
//! | Fetch | 4 | 12 | v7 fetch sessions, v9 leader epoch fencing, v11 closest-replica (KIP-392), v12 flexible encoding |
//! | ListOffsets | 1 | 8 | v2 isolation level, v4 leader epoch, v6 flexible, v7 max_timestamp, v8 tiered-storage |
//! | Metadata | 1 | 13 | v9 flexible, v10 topic UUIDs, v12 topic_id works, v13 top-level error_code |
//! | OffsetCommit | 2 | 9 | v5 drops retention_time, v6 leader epoch, v8 flexible, v9 KIP-848 |
//! | OffsetFetch | 1 | 9 | v6 flexible, v8 batched groups, v9 member_epoch (KIP-848) |
//! | FindCoordinator | 1 | 6 | v3 flexible, v4 batched keys, v5 KIP-890, v6 share groups (KIP-932) |
//! | JoinGroup | 4 | 9 | v4 group_instance_id (KIP-345), v6 flexible, v8 reason (KIP-800) |
//! | Heartbeat | 3 | 4 | v3 group_instance_id (KIP-345), v4 flexible encoding |
//! | SyncGroup | 3 | 5 | v3 group_instance_id, v4 flexible, v5 protocol_type/protocol_name (KIP-559) |
//! | LeaveGroup | 3 | 5 | v3 batch leave (KIP-345), v4 flexible, v5 reason (KIP-800) |
//! | CreateTopics | 2 | 7 | v5 flexible, v7 topic_id in response (KIP-464, KIP-525) |
//! | DeleteTopics | 1 | 6 | v4 flexible, v6 topic-ID-based deletion |
//! | CreatePartitions | 0 | 3 | v2 flexible, v3 KIP-599 |
//! | DescribeConfigs | 0 | 4 | v1 config_source + synonyms, v3 config_type + documentation, v4 flexible |
//! | IncrementalAlterConfigs | 0 | 1 | v0 non-flexible, v1 flexible encoding |
//! | DescribeAcls | 1 | 3 | v2 flexible, v3 user resource type |
//! | CreateAcls | 1 | 3 | v2 flexible, v3 user resource type |
//! | DeleteAcls | 1 | 3 | v2 flexible, v3 user resource type |
//! | DescribeGroups | 1 | 6 | v3 authorized_operations, v4 static members, v5 flexible, v6 KIP-1043 |
//! | ListGroups | 1 | 5 | v3 flexible, v4 state filter (KIP-518), v5 type filter (KIP-848) |
//! | DeleteRecords | 0 | 2 | v2 flexible encoding |
//! | OffsetForLeaderEpoch | 2 | 4 | v2 leader epoch fencing, v3 replica_id, v4 flexible |
//! | InitProducerId | 0 | 5 | v2 flexible, v3 epoch recovery, v5 KIP-890 |
//! | AddPartitionsToTxn | 0 | 5 | v3 flexible, v4 broker batched, v5 KIP-890 |
//! | AddOffsetsToTxn | 0 | 4 | v3 flexible, v4 KIP-890 |
//! | EndTxn | 0 | 5 | v3 flexible, v5 KIP-890 epoch bump |
//! | TxnOffsetCommit | 0 | 5 | v2 leader epoch, v3 flexible + consumer fields, v5 KIP-890 |
//! | CreateDelegationToken | 1 | 3 | v2 flexible, v3 owner override |
//! | RenewDelegationToken | 1 | 2 | v2 flexible encoding |
//! | ExpireDelegationToken | 1 | 2 | v2 flexible encoding |
//! | DescribeDelegationToken | 1 | 3 | v2 flexible, v3 token requester |
//! | DescribeClientQuotas | 0 | 1 | v1 flexible encoding |
//! | AlterClientQuotas | 0 | 1 | v1 flexible encoding |
//! | ApiVersions | 0 | 4 | v3 flexible, v4 SupportedFeatures fix (KAFKA-17011) |
//! | ConsumerGroupHeartbeat | 0 | 1 | KIP-848 baseline; v1 KIP-1082 (regex, client member-id) |
//! | DeleteGroups | 0 | 2 | v2 flexible encoding |
//! | DescribeCluster | 0 | 2 | v0 flexible (KIP-700), v1 endpoint_type (KIP-919), v2 is_fenced (KIP-1073) |
//! | ConsumerGroupDescribe | 0 | 1 | v0 KIP-848, v1 member_type (KIP-1099) |
//! | DescribeTopicPartitions | 0 | 0 | v0 KIP-966 paginated partition describe |
//! | UpdateFeatures | 0 | 1 | v0 AllowDowngrade, v1 UpgradeType + ValidateOnly (KIP-584) |
//! | ListClientMetricsResources | 0 | 0 | v0 KIP-714 telemetry discovery |
//! | AlterReplicaLogDirs | 1 | 2 | v1 non-flexible, v2 flexible encoding |
//! | OffsetDelete | 0 | 0 | v0 non-flexible (flexibleVersions: none) |
//! | DescribeUserScramCredentials | 0 | 0 | v0 KIP-554 |
//! | AlterUserScramCredentials | 0 | 0 | v0 KIP-554 |
//! | DescribeProducers | 0 | 0 | v0 KIP-664 transaction debug |
//! | DescribeTransactions | 0 | 0 | v0 KIP-664 transaction debug |
//! | ListTransactions | 0 | 1 | v0 KIP-664, v1 DurationFilter (KIP-994) |
//! | ElectLeaders | 0 | 2 | v1 ElectionType (KIP-460), v2 flexible |
//! | AlterPartitionReassignments | 0 | 0 | v0 all flexible (KIP-455) |
//! | ListPartitionReassignments | 0 | 0 | v0 all flexible (KIP-455) |
//!
//! ## Example
//!
//! ```rust,ignore
//! use krafka::protocol::ApiKey;
//!
//! // Negotiate the best version for Fetch
//! // Prefer Fetch v7..=v11; fall back to v4 if the broker doesn't support v7+.
//! let fetch_version = match conn.negotiate_api_version(ApiKey::Fetch, 11, 7).await {
//!     Some(v) => v,
//!     None => conn.negotiate_api_version(ApiKey::Fetch, 4, 4).await
//!         .expect("broker does not support any usable Fetch version"),
//! };
//! println!("Using Fetch v{}", fetch_version);
//! ```

mod api;
mod codec;
mod header;
mod messages;
mod primitives;
mod record;

pub use api::{
    ApiKey, ApiVersionRange, ApiVersionsRequest, ApiVersionsResponse, FinalizedFeature,
    SupportedFeature,
};
pub use codec::{Decoder, Encoder, MAX_MESSAGE_SIZE};
pub use header::{RequestHeader, ResponseHeader};
pub use messages::*;
pub use primitives::*;
pub use record::{
    Compression, LazyRecordBatch, LazyRecordIterator, Record, RecordBatch, RecordBatchBuilder,
    RecordHeader,
};

use crate::error::{KrafkaError, Result};

/// Maximum number of elements allowed in a single decoded array or loop.
///
/// Protects against malicious or corrupted broker responses that declare
/// extremely large array lengths. Without this cap, a crafted response with
/// `array_len = i32::MAX` would cause the decoder to spin billions of
/// iterations (each failing on an exhausted buffer) before returning an error.
///
/// The limit of 100,000 is generous for any realistic Kafka response while
/// preventing CPU-based denial-of-service amplification.
pub const MAX_DECODE_ARRAY_LEN: usize = 100_000;

/// Maximum number of headers allowed on a single producer record.
///
/// Header keys and values are encoded with varint-length prefixes in the
/// record-batch v2 format. This cap prevents excessively large batches from
/// bypassing `max_request_size` checks.
pub const MAX_RECORD_HEADERS: usize = 10_000;

/// Validate a topic name against the Kafka wire-format limit.
///
/// Fix for H6: the infallible [`Encode`] impl on [`KafkaString`] panics when
/// a value exceeds `i16::MAX` bytes. Rather than refactoring every call site
/// through the fallible [`TryEncode`] path, we validate at the public API
/// boundary so the panic path is structurally unreachable in production —
/// matching Kafka's Java client and `librdkafka`, which reject oversize
/// inputs at ingress with `InvalidTopicException` / `RD_KAFKA_RESP_ERR__INVALID_ARG`.
///
/// This helper is **not** a full broker-side topic-name validator (Kafka's
/// broker limit is 249 chars of a restricted charset); it enforces only the
/// wire-format prerequisite for panic-free encoding. Brokers remain the
/// authority on semantic validity.
///
/// Checks:
/// - Non-empty.
/// - Length fits in an `i16` length prefix (the `KafkaString` wire limit of
///   32 767 bytes).
///
/// Use at every public ingress where a user-supplied topic name reaches a
/// request encoder (see call sites in [`crate::admin`] and
/// [`crate::producer::ProducerRecord::validate`]).
#[inline]
pub fn validate_topic_name(name: &str) -> Result<()> {
    if name.is_empty() {
        return Err(KrafkaError::protocol("topic name cannot be empty"));
    }
    if name.len() > i16::MAX as usize {
        return Err(KrafkaError::protocol(format!(
            "topic name length {} exceeds protocol limit of {}",
            name.len(),
            i16::MAX
        )));
    }
    Ok(())
}

/// Validate every topic name in `names` via [`validate_topic_name`].
///
/// Short-circuits on the first invalid name encountered in iteration order.
/// For inputs with deterministic iteration order (e.g. slices or `Vec`s) the
/// surfaced error is also deterministic and matches the single-name helper's
/// message exactly. Preferred over `for name in names { validate_topic_name(name)? }`
/// sprinkled across call sites, because the shared implementation keeps
/// the H6 coverage surface easy to audit.
#[inline]
pub fn validate_topic_names<'a, I>(names: I) -> Result<()>
where
    I: IntoIterator<Item = &'a str>,
{
    for name in names {
        validate_topic_name(name)?;
    }
    Ok(())
}

/// Convert a collection length to i32, returning an error if it overflows.
#[inline]
pub(crate) fn array_len_i32(len: usize) -> Result<i32> {
    i32::try_from(len)
        .map_err(|_| KrafkaError::protocol(format!("array length {len} exceeds i32::MAX")))
}

/// Encode a compact array length (Kafka flexible versions: `count + 1` as unsigned varint).
#[inline]
pub(crate) fn encode_compact_array_len(len: usize, buf: &mut impl bytes::BufMut) -> Result<()> {
    let wire =
        u32::try_from(len.checked_add(1).ok_or_else(|| {
            KrafkaError::protocol(format!("compact array length {len} overflows"))
        })?)
        .map_err(|_| {
            KrafkaError::protocol(format!("compact array length {len} exceeds u32::MAX"))
        })?;
    crate::util::varint::encode_unsigned_varint(wire, buf);
    Ok(())
}

/// Validate and convert a decoded array length from `i32` to `usize`.
///
/// Returns an error if the count is negative or exceeds [`MAX_DECODE_ARRAY_LEN`].
/// Use this before every inline decode loop to bound iteration count.
#[inline]
pub(crate) fn check_decode_array_len(len: i32) -> Result<usize> {
    if len < 0 {
        return Err(KrafkaError::protocol(format!(
            "negative array length {len} in decode (use check_decode_nullable_array_len for fields where -1 means null)"
        )));
    }
    let len = len as usize;
    if len > MAX_DECODE_ARRAY_LEN {
        return Err(KrafkaError::protocol(format!(
            "array length {len} exceeds safety limit {MAX_DECODE_ARRAY_LEN}"
        )));
    }
    Ok(len)
}

/// Like [`check_decode_array_len`], but treats `-1` as a null array (returns 0).
///
/// In the Kafka wire protocol, some array fields are "nullable": a length of
/// `-1` signals an absent/null array. Use this variant for those fields
/// (e.g. `aborted_transactions` in FetchResponse).
#[inline]
pub(crate) fn check_decode_nullable_array_len(len: i32) -> Result<usize> {
    if len == -1 {
        return Ok(0);
    }
    check_decode_array_len(len)
}

/// Validate a non-nullable compact array length (varint-encoded as `actual_len + 1`).
///
/// In flexible Kafka versions, compact arrays encode the element count plus one
/// as a varint. A raw value of `1` represents an empty array (`len == 0`).
/// A raw value of `0` represents a null array and is **invalid** for
/// non-nullable fields — use [`check_compact_nullable_array_len`] for fields
/// where null is permitted.
///
/// Values exceeding [`MAX_DECODE_ARRAY_LEN`] are rejected to prevent OOM from
/// malicious or corrupted broker responses.
#[inline]
pub(crate) fn check_compact_array_len(raw: u32) -> Result<usize> {
    if raw == 0 {
        return Err(KrafkaError::protocol(
            "compact array raw value 0 (null) is invalid for a non-nullable field; \
             use check_compact_nullable_array_len for nullable arrays",
        ));
    }
    let len = (raw - 1) as usize;
    if len > MAX_DECODE_ARRAY_LEN {
        return Err(KrafkaError::protocol(format!(
            "compact array length {len} exceeds safety limit {MAX_DECODE_ARRAY_LEN}"
        )));
    }
    Ok(len)
}

/// Like [`check_compact_array_len`], but treats a raw value of `0` as a null
/// array (returns `Ok(0)`).
///
/// In the Kafka wire protocol, some compact array fields are "nullable": a raw
/// varint of `0` signals an absent/null array. Use this variant for those
/// fields (e.g. `aborted_transactions` in FetchResponse v12+).
#[inline]
pub(crate) fn check_compact_nullable_array_len(raw: u32) -> Result<usize> {
    if raw == 0 {
        return Ok(0);
    }
    let len = (raw - 1) as usize;
    if len > MAX_DECODE_ARRAY_LEN {
        return Err(KrafkaError::protocol(format!(
            "compact array length {len} exceeds safety limit {MAX_DECODE_ARRAY_LEN}"
        )));
    }
    Ok(len)
}

/// Client-supported API version ranges.
///
/// This module defines the version ranges that Krafka implements encode/decode
/// for. `*_MIN` sets the floor (we never send versions below it), `*_MAX` sets
/// the ceiling. These drive version negotiation with Kafka brokers.
///
/// **MIN strategy:** Kafka 3.9+ floor. Our MINs match the versions Kafka 4.0
/// itself kept — Produce v3+, Fetch v4+, etc. Legacy encode/decode paths below
/// MIN are deleted.
///
/// **Important**: MAX must match the highest version with a working
/// encode+decode pair. Advertising a higher version than implemented
/// causes protocol parse failures.
pub mod versions {
    // ── Produce (API key 0) ──────────────────────────────────────────────
    /// Minimum supported Produce version. Kafka 4.0 removed v0–v2.
    pub const PRODUCE_MIN: i16 = 3;
    /// Maximum supported Produce version (v11 ZStd compression).
    ///
    /// v9 introduces flexible encoding, v11 adds ZStd compression support.
    /// Encode/decode for v12-v13 exists but is not yet activated (topic-UUID
    /// based producing needs end-to-end integration testing).
    pub const PRODUCE_MAX: i16 = 11;

    // ── Fetch (API key 1) ────────────────────────────────────────────────
    /// Minimum supported Fetch version. Kafka 4.0 removed v0–v3.
    pub const FETCH_MIN: i16 = 4;
    /// Maximum supported Fetch version (v12 flexible encoding).
    ///
    /// v12 adds flexible encoding. Encode/decode for v13+ exists but uses
    /// topic-UUID-based fetching which needs end-to-end integration testing.
    pub const FETCH_MAX: i16 = 12;

    // ── ListOffsets (API key 2) ──────────────────────────────────────────
    /// Minimum supported ListOffsets version. Kafka 4.0 removed v0.
    pub const LIST_OFFSETS_MIN: i16 = 1;
    /// Maximum supported ListOffsets version (v8 tiered-storage awareness).
    ///
    /// v4 adds leader epoch validation, v6 flexible encoding, v7 max_timestamp,
    /// v8 tiered-storage. Encode/decode for v9+ exists but is not yet activated.
    pub const LIST_OFFSETS_MAX: i16 = 8;

    // ── Metadata (API key 3) ─────────────────────────────────────────────
    /// Minimum supported Metadata version. v0 lacks essential fields.
    pub const METADATA_MIN: i16 = 1;
    /// Maximum supported Metadata version (v13 top-level error_code).
    ///
    /// v9 flexible encoding, v10 topic UUIDs (enables KIP-848 topic name
    /// resolution), v12 real topic_id-based lookup, v13 top-level error_code.
    pub const METADATA_MAX: i16 = 13;

    // ── OffsetCommit (API key 8) ─────────────────────────────────────────
    /// Minimum supported OffsetCommit version. Kafka 4.0 removed v0–v1.
    pub const OFFSET_COMMIT_MIN: i16 = 2;
    /// Maximum supported OffsetCommit version (v9 KIP-848 member_epoch).
    ///
    /// v5 drops retention_time, v6 leader epoch, v7 group_instance_id,
    /// v8 flexible encoding, v9 KIP-848 member_epoch.
    /// Encode/decode for v10 exists but is not yet activated.
    pub const OFFSET_COMMIT_MAX: i16 = 9;

    // ── OffsetFetch (API key 9) ──────────────────────────────────────────
    /// Minimum supported OffsetFetch version. Kafka 4.0 removed v0.
    pub const OFFSET_FETCH_MIN: i16 = 1;
    /// Maximum supported OffsetFetch version (v9 KIP-848 member_epoch).
    ///
    /// v2 top-level error, v6 flexible, v8 batched groups, v9 KIP-848.
    /// Encode/decode for v10 exists but is not yet activated.
    pub const OFFSET_FETCH_MAX: i16 = 9;

    // ── FindCoordinator (API key 10) ─────────────────────────────────────
    /// Minimum supported FindCoordinator version.
    pub const FIND_COORDINATOR_MIN: i16 = 1;
    /// Maximum supported FindCoordinator version (v6 share groups, KIP-932).
    ///
    /// v2 same wire as v1, v3 flexible, v4 batched keys (KIP-699),
    /// v5 KIP-890, v6 share groups (KIP-932).
    pub const FIND_COORDINATOR_MAX: i16 = 6;

    // ── JoinGroup (API key 11) ───────────────────────────────────────────
    /// Minimum supported JoinGroup version. v4+ adds group_instance_id (KIP-345).
    pub const JOIN_GROUP_MIN: i16 = 4;
    /// Maximum supported JoinGroup version (v9 latest).
    ///
    /// v6 flexible, v7 skip_assignment, v8 reason (KIP-800), v9 latest.
    pub const JOIN_GROUP_MAX: i16 = 9;

    // ── Heartbeat (API key 12) ───────────────────────────────────────────
    /// Minimum supported Heartbeat version. v3+ adds group_instance_id (KIP-345).
    pub const HEARTBEAT_MIN: i16 = 3;
    /// Maximum supported Heartbeat version (v4 flexible encoding).
    pub const HEARTBEAT_MAX: i16 = 4;

    // ── LeaveGroup (API key 13) ──────────────────────────────────────────
    /// Minimum supported LeaveGroup version. v3+ adds batch leave (KIP-345).
    pub const LEAVE_GROUP_MIN: i16 = 3;
    /// Maximum supported LeaveGroup version (v5 reason, KIP-800).
    pub const LEAVE_GROUP_MAX: i16 = 5;

    // ── SyncGroup (API key 14) ───────────────────────────────────────────
    /// Minimum supported SyncGroup version. v3+ adds group_instance_id (KIP-345).
    pub const SYNC_GROUP_MIN: i16 = 3;
    /// Maximum supported SyncGroup version (v5 protocol_type/name, KIP-559).
    pub const SYNC_GROUP_MAX: i16 = 5;

    // ── ApiVersions (API key 18) ─────────────────────────────────────────
    /// Minimum supported ApiVersions version.
    pub const API_VERSIONS_MIN: i16 = 0;
    /// Maximum supported ApiVersions version (v4 KAFKA-17011 SupportedFeatures fix).
    #[cfg(not(feature = "unstable-protocol"))]
    pub const API_VERSIONS_MAX: i16 = 4;
    /// Maximum supported ApiVersions version (v5 KIP-1242 ClusterId/NodeId).
    #[cfg(feature = "unstable-protocol")]
    pub const API_VERSIONS_MAX: i16 = 5;

    // ── CreateTopics (API key 19) ────────────────────────────────────────
    /// Minimum supported CreateTopics version. Kafka 4.0 removed v0–v1.
    pub const CREATE_TOPICS_MIN: i16 = 2;
    /// Maximum supported CreateTopics version (v7 topic_id in response).
    ///
    /// v5 flexible encoding, v7 topic_id (KIP-464, KIP-525).
    pub const CREATE_TOPICS_MAX: i16 = 7;

    // ── DeleteTopics (API key 20) ────────────────────────────────────────
    /// Minimum supported DeleteTopics version. Kafka 4.0 removed v0.
    pub const DELETE_TOPICS_MIN: i16 = 1;
    /// Maximum supported DeleteTopics version (v6 topic-ID-based deletion).
    ///
    /// v4 flexible, v5 error_message, v6 topic-ID-based deletion.
    pub const DELETE_TOPICS_MAX: i16 = 6;

    // ── CreatePartitions (API key 37) ────────────────────────────────────
    /// Minimum supported CreatePartitions version.
    pub const CREATE_PARTITIONS_MIN: i16 = 0;
    /// Maximum supported CreatePartitions version (v3 KIP-599).
    ///
    /// v2 flexible encoding, v3 KIP-599.
    pub const CREATE_PARTITIONS_MAX: i16 = 3;

    // ── DescribeConfigs (API key 32) ─────────────────────────────────────
    /// Minimum supported DescribeConfigs version.
    pub const DESCRIBE_CONFIGS_MIN: i16 = 0;
    /// Maximum supported DescribeConfigs version (v4 flexible encoding).
    ///
    /// v1 synonyms, v3 config_type + documentation, v4 flexible.
    pub const DESCRIBE_CONFIGS_MAX: i16 = 4;

    // ── DescribeAcls (API key 29) ────────────────────────────────────────
    /// Minimum supported DescribeAcls version. Kafka 4.0 removed v0.
    pub const DESCRIBE_ACLS_MIN: i16 = 1;
    /// Maximum supported DescribeAcls version (v3 user resource type).
    pub const DESCRIBE_ACLS_MAX: i16 = 3;

    // ── CreateAcls (API key 30) ──────────────────────────────────────────
    /// Minimum supported CreateAcls version. Kafka 4.0 removed v0.
    pub const CREATE_ACLS_MIN: i16 = 1;
    /// Maximum supported CreateAcls version (v3 user resource type).
    pub const CREATE_ACLS_MAX: i16 = 3;

    // ── DeleteAcls (API key 31) ──────────────────────────────────────────
    /// Minimum supported DeleteAcls version. Kafka 4.0 removed v0.
    pub const DELETE_ACLS_MIN: i16 = 1;
    /// Maximum supported DeleteAcls version (v3 user resource type).
    pub const DELETE_ACLS_MAX: i16 = 3;

    // ── DescribeGroups (API key 15) ──────────────────────────────────────
    /// Minimum supported DescribeGroups version.
    pub const DESCRIBE_GROUPS_MIN: i16 = 1;
    /// Maximum supported DescribeGroups version (v6 KIP-1043).
    ///
    /// v3 authorized_operations, v4 static members, v5 flexible, v6 KIP-1043.
    pub const DESCRIBE_GROUPS_MAX: i16 = 6;

    // ── ListGroups (API key 16) ──────────────────────────────────────────
    /// Minimum supported ListGroups version.
    pub const LIST_GROUPS_MIN: i16 = 1;
    /// Maximum supported ListGroups version (v5 type filter, KIP-848).
    ///
    /// v3 flexible, v4 state filter (KIP-518), v5 type filter (KIP-848).
    pub const LIST_GROUPS_MAX: i16 = 5;

    // ── DeleteRecords (API key 21) ───────────────────────────────────────
    /// Minimum supported DeleteRecords version.
    pub const DELETE_RECORDS_MIN: i16 = 0;
    /// Maximum supported DeleteRecords version (v2 flexible encoding).
    pub const DELETE_RECORDS_MAX: i16 = 2;

    // ── DescribeLogDirs (API key 35) ─────────────────────────────────────
    /// Minimum supported DescribeLogDirs version.
    /// v0 was removed in Kafka 4.0; v1 is the baseline.
    pub const DESCRIBE_LOG_DIRS_MIN: i16 = 1;
    /// Maximum supported DescribeLogDirs version (v4 adds TotalBytes/UsableBytes).
    ///
    /// v1 baseline, v2 flexible encoding, v3 top-level error_code,
    /// v4 TotalBytes + UsableBytes per log dir.
    pub const DESCRIBE_LOG_DIRS_MAX: i16 = 4;

    // ── OffsetForLeaderEpoch (API key 23) ────────────────────────────────
    /// Minimum supported OffsetForLeaderEpoch version. v2+ adds leader epoch.
    pub const OFFSET_FOR_LEADER_EPOCH_MIN: i16 = 2;
    /// Maximum supported OffsetForLeaderEpoch version (v4 flexible encoding).
    pub const OFFSET_FOR_LEADER_EPOCH_MAX: i16 = 4;

    // ── InitProducerId (API key 22) ──────────────────────────────────────
    /// Minimum supported InitProducerId version.
    pub const INIT_PRODUCER_ID_MIN: i16 = 0;
    /// Maximum supported InitProducerId version (v4 producer_id/epoch recovery).
    ///
    /// v2 flexible, v3 epoch recovery (producer_id + epoch for fencing),
    /// v4 latest stable. Encode/decode for v5-v6 (KIP-890) exists but is
    /// not yet activated.
    pub const INIT_PRODUCER_ID_MAX: i16 = 4;

    // ── AddPartitionsToTxn (API key 24) ──────────────────────────────────
    /// Minimum supported AddPartitionsToTxn version.
    pub const ADD_PARTITIONS_TO_TXN_MIN: i16 = 0;
    /// Maximum supported AddPartitionsToTxn version (v3 flexible encoding).
    ///
    /// v3 flexible. Encode/decode for v4-v5 (broker batched, KIP-890)
    /// exists but is not yet activated.
    pub const ADD_PARTITIONS_TO_TXN_MAX: i16 = 3;

    // ── AddOffsetsToTxn (API key 25) ─────────────────────────────────────
    /// Minimum supported AddOffsetsToTxn version.
    pub const ADD_OFFSETS_TO_TXN_MIN: i16 = 0;
    /// Maximum supported AddOffsetsToTxn version (v3 flexible encoding).
    ///
    /// v3 flexible. Encode/decode for v4 (KIP-890) exists but is not yet
    /// activated.
    pub const ADD_OFFSETS_TO_TXN_MAX: i16 = 3;

    // ── EndTxn (API key 26) ──────────────────────────────────────────────
    /// Minimum supported EndTxn version.
    pub const END_TXN_MIN: i16 = 0;
    /// Maximum supported EndTxn version (v3 flexible encoding).
    ///
    /// v3 flexible. Encode/decode for v4-v5 (KIP-890 epoch bump) exists
    /// but is not yet activated.
    pub const END_TXN_MAX: i16 = 3;

    // ── TxnOffsetCommit (API key 28) ─────────────────────────────────────
    /// Minimum supported TxnOffsetCommit version.
    pub const TXN_OFFSET_COMMIT_MIN: i16 = 0;
    /// Maximum supported TxnOffsetCommit version (v3 flexible + consumer fields).
    ///
    /// v2 leader epoch, v3 flexible + group_instance_id + generation_id.
    /// Encode/decode for v4-v5 (KIP-890) exists but is not yet activated.
    pub const TXN_OFFSET_COMMIT_MAX: i16 = 3;

    // ── Delegation Token APIs (38–41) ────────────────────────────────────
    /// Minimum supported CreateDelegationToken version. Kafka 4.0 removed v0.
    pub const CREATE_DELEGATION_TOKEN_MIN: i16 = 1;
    /// Maximum supported CreateDelegationToken version.
    /// v2 flexible encoding, v3 adds owner principal override.
    pub const CREATE_DELEGATION_TOKEN_MAX: i16 = 3;
    /// Minimum supported RenewDelegationToken version. Kafka 4.0 removed v0.
    pub const RENEW_DELEGATION_TOKEN_MIN: i16 = 1;
    /// Maximum supported RenewDelegationToken version.
    /// v2 flexible encoding.
    pub const RENEW_DELEGATION_TOKEN_MAX: i16 = 2;
    /// Minimum supported ExpireDelegationToken version. Kafka 4.0 removed v0.
    pub const EXPIRE_DELEGATION_TOKEN_MIN: i16 = 1;
    /// Maximum supported ExpireDelegationToken version.
    /// v2 flexible encoding.
    pub const EXPIRE_DELEGATION_TOKEN_MAX: i16 = 2;
    /// Minimum supported DescribeDelegationToken version. Kafka 4.0 removed v0.
    pub const DESCRIBE_DELEGATION_TOKEN_MIN: i16 = 1;
    /// Maximum supported DescribeDelegationToken version.
    /// v2 flexible encoding, v3 adds token requester fields.
    pub const DESCRIBE_DELEGATION_TOKEN_MAX: i16 = 3;

    // ── Client Quotas APIs (48–49) ───────────────────────────────────────
    /// Minimum supported DescribeClientQuotas version.
    pub const DESCRIBE_CLIENT_QUOTAS_MIN: i16 = 0;
    /// Maximum supported DescribeClientQuotas version (v1 flexible encoding).
    pub const DESCRIBE_CLIENT_QUOTAS_MAX: i16 = 1;
    /// Minimum supported AlterClientQuotas version.
    pub const ALTER_CLIENT_QUOTAS_MIN: i16 = 0;
    /// Maximum supported AlterClientQuotas version (v1 flexible encoding).
    pub const ALTER_CLIENT_QUOTAS_MAX: i16 = 1;

    // ── ConsumerGroupHeartbeat (API key 68) ──────────────────────────────
    /// Minimum supported ConsumerGroupHeartbeat version.
    pub const CONSUMER_GROUP_HEARTBEAT_MIN: i16 = 0;
    /// Maximum supported ConsumerGroupHeartbeat version (v1 KIP-1082 regex + client member-id).
    pub const CONSUMER_GROUP_HEARTBEAT_MAX: i16 = 1;

    // ── IncrementalAlterConfigs (API key 44) ─────────────────────────────
    /// Minimum supported IncrementalAlterConfigs version.
    pub const INCREMENTAL_ALTER_CONFIGS_MIN: i16 = 0;
    /// Maximum supported IncrementalAlterConfigs version.
    pub const INCREMENTAL_ALTER_CONFIGS_MAX: i16 = 1;

    // ── DeleteGroups (API key 42) ────────────────────────────────────────
    /// Minimum supported DeleteGroups version.
    pub const DELETE_GROUPS_MIN: i16 = 0;
    /// Maximum supported DeleteGroups version.
    pub const DELETE_GROUPS_MAX: i16 = 2;

    // ── DescribeCluster (API key 60) ─────────────────────────────────────
    /// Minimum supported DescribeCluster version.
    pub const DESCRIBE_CLUSTER_MIN: i16 = 0;
    /// Maximum supported DescribeCluster version.
    pub const DESCRIBE_CLUSTER_MAX: i16 = 2;

    // ── UpdateFeatures (API key 57) — KIP-584 ───────────────────────────
    /// Minimum supported UpdateFeatures version.
    pub const UPDATE_FEATURES_MIN: i16 = 0;
    /// Maximum supported UpdateFeatures version (v1 adds UpgradeType + ValidateOnly).
    pub const UPDATE_FEATURES_MAX: i16 = 1;

    // ── ElectLeaders (API key 43) ────────────────────────────────────────
    /// Minimum supported ElectLeaders version.
    /// v0 preferred-only election; v1 adds ElectionType (KIP-460);
    /// v2 flexible encoding.
    pub const ELECT_LEADERS_MIN: i16 = 0;
    /// Maximum supported ElectLeaders version.
    pub const ELECT_LEADERS_MAX: i16 = 2;

    // ── AlterPartitionReassignments (API key 45) ─────────────────────────
    /// Minimum supported AlterPartitionReassignments version.
    /// All versions use flexible encoding.
    pub const ALTER_PARTITION_REASSIGNMENTS_MIN: i16 = 0;
    /// Maximum supported AlterPartitionReassignments version.
    pub const ALTER_PARTITION_REASSIGNMENTS_MAX: i16 = 0;

    // ── ListPartitionReassignments (API key 46) ──────────────────────────
    /// Minimum supported ListPartitionReassignments version.
    /// All versions use flexible encoding.
    pub const LIST_PARTITION_REASSIGNMENTS_MIN: i16 = 0;
    /// Maximum supported ListPartitionReassignments version.
    pub const LIST_PARTITION_REASSIGNMENTS_MAX: i16 = 0;

    // ── AlterReplicaLogDirs (API key 34) ─────────────────────────────────
    /// Minimum supported AlterReplicaLogDirs version.
    /// v1 non-flexible; v2 flexible encoding.
    pub const ALTER_REPLICA_LOG_DIRS_MIN: i16 = 1;
    /// Maximum supported AlterReplicaLogDirs version.
    pub const ALTER_REPLICA_LOG_DIRS_MAX: i16 = 2;

    // ── OffsetDelete (API key 47) ────────────────────────────────────────
    /// Minimum supported OffsetDelete version.
    /// flexibleVersions: "none" — always non-flexible.
    pub const OFFSET_DELETE_MIN: i16 = 0;
    /// Maximum supported OffsetDelete version.
    pub const OFFSET_DELETE_MAX: i16 = 0;

    // ── DescribeUserScramCredentials (API key 50) ────────────────────────
    /// Minimum supported DescribeUserScramCredentials version.
    /// All versions use flexible encoding.
    pub const DESCRIBE_USER_SCRAM_CREDENTIALS_MIN: i16 = 0;
    /// Maximum supported DescribeUserScramCredentials version.
    pub const DESCRIBE_USER_SCRAM_CREDENTIALS_MAX: i16 = 0;

    // ── AlterUserScramCredentials (API key 51) ───────────────────────────
    /// Minimum supported AlterUserScramCredentials version.
    /// All versions use flexible encoding.
    pub const ALTER_USER_SCRAM_CREDENTIALS_MIN: i16 = 0;
    /// Maximum supported AlterUserScramCredentials version.
    pub const ALTER_USER_SCRAM_CREDENTIALS_MAX: i16 = 0;

    // ── DescribeProducers (API key 61) ───────────────────────────────────
    /// Minimum supported DescribeProducers version.
    /// All versions use flexible encoding.
    pub const DESCRIBE_PRODUCERS_MIN: i16 = 0;
    /// Maximum supported DescribeProducers version.
    pub const DESCRIBE_PRODUCERS_MAX: i16 = 0;

    // ── DescribeTransactions (API key 65) ────────────────────────────────
    /// Minimum supported DescribeTransactions version.
    /// All versions use flexible encoding.
    pub const DESCRIBE_TRANSACTIONS_MIN: i16 = 0;
    /// Maximum supported DescribeTransactions version.
    pub const DESCRIBE_TRANSACTIONS_MAX: i16 = 0;

    // ── ListTransactions (API key 66) ────────────────────────────────────
    /// Minimum supported ListTransactions version.
    /// All versions use flexible encoding.
    pub const LIST_TRANSACTIONS_MIN: i16 = 0;
    /// Maximum supported ListTransactions version.
    /// v1 adds DurationFilter (KIP-994).
    pub const LIST_TRANSACTIONS_MAX: i16 = 1;

    // ── ConsumerGroupDescribe (API key 69) ───────────────────────────────
    /// Minimum supported ConsumerGroupDescribe version.
    pub const CONSUMER_GROUP_DESCRIBE_MIN: i16 = 0;
    /// Maximum supported ConsumerGroupDescribe version.
    pub const CONSUMER_GROUP_DESCRIBE_MAX: i16 = 1;

    // ── DescribeTopicPartitions (API key 75) ─────────────────────────────
    /// Minimum supported DescribeTopicPartitions version.
    pub const DESCRIBE_TOPIC_PARTITIONS_MIN: i16 = 0;
    /// Maximum supported DescribeTopicPartitions version.
    pub const DESCRIBE_TOPIC_PARTITIONS_MAX: i16 = 0;

    // ── ListClientMetricsResources (API key 74) ──────────────────────────
    /// Minimum supported ListClientMetricsResources version.
    pub const LIST_CLIENT_METRICS_RESOURCES_MIN: i16 = 0;
    /// Maximum supported ListClientMetricsResources version.
    pub const LIST_CLIENT_METRICS_RESOURCES_MAX: i16 = 0;

    // ── WriteTxnMarkers (API key 27) ─────────────────────────────────────
    /// Minimum supported WriteTxnMarkers version.
    /// v0 was removed in Kafka 4.0; v1 is the baseline (flexible encoding).
    pub const WRITE_TXN_MARKERS_MIN: i16 = 1;
    /// Maximum supported WriteTxnMarkers version.
    /// v1 flexible baseline; v2 adds TransactionVersion (KIP-1228).
    pub const WRITE_TXN_MARKERS_MAX: i16 = 1;

    // ── DescribeQuorum (API key 55) ──────────────────────────────────────
    /// Minimum supported DescribeQuorum version.
    /// All versions use flexible encoding.
    pub const DESCRIBE_QUORUM_MIN: i16 = 0;
    /// Maximum supported DescribeQuorum version.
    /// v0 baseline; v1 adds timestamps (KIP-836); v2 adds Nodes (KIP-853).
    pub const DESCRIBE_QUORUM_MAX: i16 = 0;

    // ── GetTelemetrySubscriptions (API key 71) — KIP-714 ─────────────────
    /// Minimum supported GetTelemetrySubscriptions version.
    #[cfg(feature = "telemetry")]
    #[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
    pub const GET_TELEMETRY_SUBSCRIPTIONS_MIN: i16 = 0;
    /// Maximum supported GetTelemetrySubscriptions version.
    #[cfg(feature = "telemetry")]
    #[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
    pub const GET_TELEMETRY_SUBSCRIPTIONS_MAX: i16 = 0;

    // ── PushTelemetry (API key 72) — KIP-714 ────────────────────────────
    /// Minimum supported PushTelemetry version.
    #[cfg(feature = "telemetry")]
    #[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
    pub const PUSH_TELEMETRY_MIN: i16 = 0;
    /// Maximum supported PushTelemetry version.
    #[cfg(feature = "telemetry")]
    #[cfg_attr(docsrs, doc(cfg(feature = "telemetry")))]
    pub const PUSH_TELEMETRY_MAX: i16 = 0;

    // ── ShareGroupHeartbeat (API key 76) — KIP-932 ──────────────────────
    /// Minimum supported ShareGroupHeartbeat version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_GROUP_HEARTBEAT_MIN: i16 = 1;
    /// Maximum supported ShareGroupHeartbeat version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_GROUP_HEARTBEAT_MAX: i16 = 1;

    // ── ShareGroupDescribe (API key 77) — KIP-932 ───────────────────────
    /// Minimum supported ShareGroupDescribe version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_GROUP_DESCRIBE_MIN: i16 = 1;
    /// Maximum supported ShareGroupDescribe version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_GROUP_DESCRIBE_MAX: i16 = 1;

    // ── ShareFetch (API key 78) — KIP-932 ───────────────────────────────
    /// Minimum supported ShareFetch version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_FETCH_MIN: i16 = 1;
    /// Maximum supported ShareFetch version (KIP-1206 + KIP-1222).
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_FETCH_MAX: i16 = 2;

    // ── ShareAcknowledge (API key 79) — KIP-932 ─────────────────────────
    /// Minimum supported ShareAcknowledge version.
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_ACKNOWLEDGE_MIN: i16 = 1;
    /// Maximum supported ShareAcknowledge version (KIP-1222).
    #[cfg(feature = "unstable-protocol")]
    pub const SHARE_ACKNOWLEDGE_MAX: i16 = 2;
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
    use super::*;

    #[test]
    fn check_decode_array_len_valid() {
        assert_eq!(check_decode_array_len(0).unwrap(), 0);
        assert_eq!(check_decode_array_len(1).unwrap(), 1);
        assert_eq!(check_decode_array_len(100_000).unwrap(), 100_000);
    }

    #[test]
    fn validate_topic_name_accepts_valid() {
        assert!(validate_topic_name("t").is_ok());
        assert!(validate_topic_name("my.topic-0_1").is_ok());
        // Boundary: exactly i16::MAX bytes is accepted.
        let max_ok = "x".repeat(i16::MAX as usize);
        assert!(validate_topic_name(&max_ok).is_ok());
    }

    #[test]
    fn validate_topic_name_rejects_empty() {
        let err = validate_topic_name("").unwrap_err().to_string();
        assert!(err.contains("cannot be empty"), "got: {err}");
    }

    #[test]
    fn validate_topic_name_rejects_oversize() {
        let too_big = "x".repeat(i16::MAX as usize + 1);
        let err = validate_topic_name(&too_big).unwrap_err().to_string();
        assert!(err.contains("exceeds protocol limit"), "got: {err}");
    }

    #[test]
    fn validate_topic_names_short_circuits_on_first_error() {
        // Plural helper rejects on the first invalid entry.
        let names = ["ok", "", "also-ok"];
        let err = validate_topic_names(names.iter().copied())
            .unwrap_err()
            .to_string();
        assert!(err.contains("cannot be empty"), "got: {err}");

        // All-valid input passes.
        assert!(validate_topic_names(["a", "b", "c"].iter().copied()).is_ok());
    }

    #[test]
    fn check_decode_array_len_rejects_negative() {
        assert!(check_decode_array_len(-1).is_err());
        assert!(check_decode_array_len(i32::MIN).is_err());
    }

    #[test]
    fn check_decode_array_len_rejects_oversized() {
        assert!(check_decode_array_len(100_001).is_err());
        assert!(check_decode_array_len(i32::MAX).is_err());
    }

    #[test]
    fn check_decode_nullable_array_len_null() {
        assert_eq!(check_decode_nullable_array_len(-1).unwrap(), 0);
    }

    #[test]
    fn check_decode_nullable_array_len_valid() {
        assert_eq!(check_decode_nullable_array_len(0).unwrap(), 0);
        assert_eq!(check_decode_nullable_array_len(5).unwrap(), 5);
    }

    #[test]
    fn check_decode_nullable_array_len_rejects_other_negative() {
        assert!(check_decode_nullable_array_len(-2).is_err());
        assert!(check_decode_nullable_array_len(i32::MIN).is_err());
    }

    // --- compact array helpers (varint-encoded, raw = count + 1) ---

    #[test]
    fn compact_array_len_rejects_null() {
        // raw 0 means null — invalid for non-nullable fields
        assert!(check_compact_array_len(0).is_err());
    }

    #[test]
    fn compact_array_len_empty() {
        // raw 1 → actual length 0
        assert_eq!(check_compact_array_len(1).unwrap(), 0);
    }

    #[test]
    fn compact_array_len_valid() {
        assert_eq!(check_compact_array_len(2).unwrap(), 1);
        assert_eq!(check_compact_array_len(101).unwrap(), 100);
    }

    #[test]
    fn compact_array_len_rejects_oversized() {
        let over = (MAX_DECODE_ARRAY_LEN as u32) + 2; // raw = limit + 1 + 1
        assert!(check_compact_array_len(over).is_err());
    }

    #[test]
    fn compact_nullable_array_len_null() {
        // raw 0 → null → Ok(0)
        assert_eq!(check_compact_nullable_array_len(0).unwrap(), 0);
    }

    #[test]
    fn compact_nullable_array_len_empty() {
        // raw 1 → actual length 0
        assert_eq!(check_compact_nullable_array_len(1).unwrap(), 0);
    }

    #[test]
    fn compact_nullable_array_len_valid() {
        assert_eq!(check_compact_nullable_array_len(2).unwrap(), 1);
        assert_eq!(check_compact_nullable_array_len(101).unwrap(), 100);
    }

    #[test]
    fn compact_nullable_array_len_rejects_oversized() {
        let over = (MAX_DECODE_ARRAY_LEN as u32) + 2;
        assert!(check_compact_nullable_array_len(over).is_err());
    }
}