Skip to main content

iggy_binary_protocol/primitives/
options.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Key-value options block for `Create*` requests.
19//!
20//! Options reuse the user-headers TLV encoding and its structural validator
21//! wholesale, so unknown value kind codes remain forwardable across
22//! mixed-version clusters. On top of the structural walk this layer enforces
23//! the stricter contract options need and messages do not: string keys only,
24//! no duplicate keys, and hard bounds on entry count, key length, and total
25//! block size, all rejected at the wire edge before a request reaches
26//! admission.
27
28use std::borrow::Cow;
29
30use bytes::{BufMut, Bytes, BytesMut};
31
32use crate::WireError;
33use crate::codec::WireEncode;
34use crate::primitives::user_headers::{
35    WireHeaderKind, WireUserHeaderEntry, WireUserHeaderIterator, validate_user_headers,
36};
37
38/// Maximum number of key-value entries in one options block.
39pub const MAX_OPTIONS: u32 = 1024;
40
41/// Maximum total byte length of an encoded options block.
42///
43/// Mirrors `iggy_common::MAX_USER_HEADERS_SIZE`: options ride the user-headers
44/// codec, so they inherit its budget rather than inventing a second one. The
45/// constant is duplicated because `iggy_common` depends on this crate, not the
46/// other way round; `options_block_budget_matches_user_headers` pins the two
47/// together from the side that can see both.
48///
49/// Sized so [`MAX_OPTIONS`] is actually reachable: the cheapest entry is 12
50/// bytes (kind + length + one byte, for key and value each), so 1024 entries
51/// need at least 12 KiB. A smaller budget would silently cap the entry count
52/// below `MAX_OPTIONS` and make that limit a lie.
53pub const MAX_OPTIONS_BYTES: usize = 100 * 1000;
54
55/// Wire kind code for UTF-8 string fields, matching `HeaderKind::String`
56/// in `iggy_common`.
57const STRING_KIND: WireHeaderKind = WireHeaderKind(2);
58
59/// Validate the structural and semantic integrity of an options byte buffer.
60///
61/// Runs [`validate_user_headers`] first, then enforces the options contract:
62///
63/// - Total block size within [`MAX_OPTIONS_BYTES`]
64/// - At most [`MAX_OPTIONS`] entries
65/// - Every key is a UTF-8 string (length already bounded by the codec)
66/// - No duplicate keys
67///
68/// Value kind codes are deliberately NOT restricted to the currently defined
69/// range, mirroring the user-headers forward-compatibility contract.
70///
71/// Returns the number of key-value pairs on success.
72///
73/// # Errors
74///
75/// Returns `WireError::UnexpectedEof` if the buffer is truncated mid-entry,
76/// or `WireError::Validation` if any constraint above is violated.
77pub fn validate_options(buf: &[u8]) -> Result<u32, WireError> {
78    if buf.len() > MAX_OPTIONS_BYTES {
79        return Err(WireError::Validation(Cow::Owned(format!(
80            "options block is {} bytes, exceeds maximum {MAX_OPTIONS_BYTES}",
81            buf.len()
82        ))));
83    }
84
85    let pairs = validate_user_headers(buf)?;
86    if pairs == 0 {
87        return Ok(0);
88    }
89    if pairs > MAX_OPTIONS {
90        return Err(WireError::Validation(Cow::Owned(format!(
91            "options block has {pairs} entries, exceeds maximum {MAX_OPTIONS}"
92        ))));
93    }
94
95    let mut keys: Vec<&[u8]> = Vec::with_capacity(pairs as usize);
96    for entry in WireUserHeaderIterator::new(buf) {
97        if entry.key_kind != STRING_KIND {
98            return Err(WireError::Validation(Cow::Owned(format!(
99                "option key kind {} is not a string",
100                entry.key_kind.0
101            ))));
102        }
103        if std::str::from_utf8(entry.key).is_err() {
104            return Err(WireError::Validation(Cow::Borrowed(
105                "option key is not valid UTF-8",
106            )));
107        }
108        keys.push(entry.key);
109    }
110
111    keys.sort_unstable();
112    for pair in keys.windows(2) {
113        if pair[0] == pair[1] {
114            return Err(WireError::Validation(Cow::Owned(format!(
115                "duplicate option key: {}",
116                String::from_utf8_lossy(pair[0])
117            ))));
118        }
119    }
120
121    Ok(pairs)
122}
123
124/// Encoded size of a `u32`-length-prefixed options block.
125///
126/// Response elements (topic/stream/user entries in a list) cannot run their
127/// options to end-of-payload the way requests do, so they carry the block
128/// behind a length prefix.
129#[must_use]
130pub fn options_prefixed_size(options: &WireOptions) -> usize {
131    4 + options.encoded_size()
132}
133
134/// Encode a `u32`-length-prefixed options block.
135pub fn encode_options_prefixed(options: &WireOptions, buf: &mut BytesMut) {
136    #[allow(clippy::cast_possible_truncation)]
137    buf.put_u32_le(options.encoded_size() as u32);
138    options.encode(buf);
139}
140
141/// Decode a `u32`-length-prefixed options block at `pos`.
142///
143/// Returns the block and the total bytes consumed (prefix included).
144///
145/// # Errors
146///
147/// Returns `WireError::UnexpectedEof` on truncation, or
148/// `WireError::Validation` when the block violates [`validate_options`].
149pub fn decode_options_prefixed(buf: &[u8], pos: usize) -> Result<(WireOptions, usize), WireError> {
150    let length = crate::codec::read_u32_le(buf, pos)? as usize;
151    let start = pos + 4;
152    // `checked_add`: on a 32-bit target a wire-supplied length wraps, the
153    // truncation guard below then passes, and the slice panics instead.
154    let end = start
155        .checked_add(length)
156        .ok_or_else(|| WireError::UnexpectedEof {
157            offset: start,
158            need: length,
159            have: buf.len().saturating_sub(start),
160        })?;
161    if buf.len() < end {
162        return Err(WireError::UnexpectedEof {
163            offset: start,
164            need: length,
165            have: buf.len().saturating_sub(start),
166        });
167    }
168    let options = WireOptions::from_slice(&buf[start..end])?;
169    Ok((options, 4 + length))
170}
171
172/// Pre-validated options as a contiguous TLV byte buffer.
173///
174/// Construction validates both structural TLV integrity and the options
175/// contract via [`validate_options`]. The inner bytes are immutable.
176///
177/// Like `WireUserHeaders`, this type is opaque at the wire layer. Domain
178/// interpretation of value kind codes happens in `iggy_common`.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct WireOptions(Bytes);
181
182impl WireOptions {
183    /// Validate and wrap a borrowed byte slice (copies into new `Bytes`).
184    ///
185    /// # Errors
186    /// Returns `WireError` if the buffer violates [`validate_options`].
187    pub fn from_slice(buf: &[u8]) -> Result<Self, WireError> {
188        validate_options(buf)?;
189        Ok(Self(Bytes::copy_from_slice(buf)))
190    }
191
192    /// Validate and wrap an owned `Bytes` buffer (zero-copy).
193    ///
194    /// # Errors
195    /// Returns `WireError` if the buffer violates [`validate_options`].
196    pub fn from_bytes(buf: Bytes) -> Result<Self, WireError> {
197        validate_options(&buf)?;
198        Ok(Self(buf))
199    }
200
201    /// Wrap pre-validated bytes without re-checking.
202    ///
203    /// # Safety contract (not `unsafe`, but caller must ensure):
204    /// The bytes must satisfy [`validate_options`]. Iterating invalid bytes
205    /// will panic.
206    pub const fn from_validated(buf: Bytes) -> Self {
207        Self(buf)
208    }
209
210    /// Empty options (zero-length buffer).
211    #[must_use]
212    pub const fn empty() -> Self {
213        Self(Bytes::new())
214    }
215
216    #[must_use]
217    pub const fn is_empty(&self) -> bool {
218        self.0.is_empty()
219    }
220
221    /// The raw validated bytes, suitable for wire transmission.
222    #[must_use]
223    pub fn as_bytes(&self) -> &[u8] {
224        &self.0
225    }
226
227    /// Consume into the inner `Bytes` handle (zero-copy).
228    #[must_use]
229    pub fn into_bytes(self) -> Bytes {
230        self.0
231    }
232
233    /// Zero-copy iteration over the pre-validated entries.
234    #[must_use]
235    pub fn iter(&self) -> WireUserHeaderIterator<'_> {
236        WireUserHeaderIterator::new(&self.0)
237    }
238}
239
240impl<'a> IntoIterator for &'a WireOptions {
241    type Item = WireUserHeaderEntry<'a>;
242    type IntoIter = WireUserHeaderIterator<'a>;
243
244    fn into_iter(self) -> Self::IntoIter {
245        self.iter()
246    }
247}
248
249impl WireEncode for WireOptions {
250    fn encoded_size(&self) -> usize {
251        self.0.len()
252    }
253
254    fn encode(&self, buf: &mut BytesMut) {
255        buf.put_slice(&self.0);
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262    use crate::primitives::user_headers::encode_user_headers;
263
264    const STRING: u8 = 2;
265    const UINT64: u8 = 12;
266
267    fn encode(entries: &[(u8, &[u8], u8, &[u8])]) -> BytesMut {
268        let mut buf = BytesMut::new();
269        encode_user_headers(entries, &mut buf);
270        buf
271    }
272
273    #[test]
274    fn empty_buffer_is_zero_options() {
275        assert_eq!(validate_options(&[]).unwrap(), 0);
276        assert!(WireOptions::empty().is_empty());
277    }
278
279    #[test]
280    fn valid_block_roundtrips() {
281        let value = 1_073_741_824u64.to_le_bytes();
282        let buf = encode(&[
283            (STRING, b"segment_size", UINT64, &value),
284            (STRING, b"message_expiry", STRING, b"7 days"),
285        ]);
286        assert_eq!(validate_options(&buf).unwrap(), 2);
287
288        let options = WireOptions::from_slice(&buf).unwrap();
289        let entries: Vec<_> = options.iter().collect();
290        assert_eq!(entries.len(), 2);
291        assert_eq!(entries[0].key, b"segment_size");
292        assert_eq!(entries[0].value, value);
293        assert_eq!(entries[1].key, b"message_expiry");
294        assert_eq!(entries[1].value, b"7 days");
295    }
296
297    #[test]
298    fn unknown_value_kind_is_forwardable() {
299        let buf = encode(&[(STRING, b"future_option", 200, b"opaque")]);
300        assert_eq!(validate_options(&buf).unwrap(), 1);
301    }
302
303    #[test]
304    fn duplicate_key_is_rejected() {
305        let buf = encode(&[
306            (STRING, b"segment_size", STRING, b"1 GiB"),
307            (STRING, b"segment_size", STRING, b"2 GiB"),
308        ]);
309        assert!(matches!(
310            validate_options(&buf),
311            Err(WireError::Validation(_))
312        ));
313    }
314
315    #[test]
316    fn non_string_key_kind_is_rejected() {
317        let buf = encode(&[(UINT64, &1u64.to_le_bytes(), STRING, b"value")]);
318        assert!(matches!(
319            validate_options(&buf),
320            Err(WireError::Validation(_))
321        ));
322    }
323
324    #[test]
325    fn non_utf8_key_is_rejected() {
326        let buf = encode(&[(STRING, &[0xFF, 0xFE], STRING, b"value")]);
327        assert!(matches!(
328            validate_options(&buf),
329            Err(WireError::Validation(_))
330        ));
331    }
332
333    #[test]
334    fn key_length_is_bounded_by_the_user_headers_codec() {
335        // Options add no key-length rule of their own: every field is already
336        // bounded to 1..=255 by `validate_user_headers`, so the codec's limit
337        // is the option key's limit.
338        let over = [b'k'; 256];
339        let buf = encode(&[(STRING, &over, STRING, b"value")]);
340        assert!(matches!(
341            validate_options(&buf),
342            Err(WireError::Validation(_))
343        ));
344
345        let at_limit = [b'k'; 255];
346        let buf = encode(&[(STRING, &at_limit, STRING, b"value")]);
347        assert_eq!(validate_options(&buf).unwrap(), 1);
348    }
349
350    #[test]
351    fn entry_count_over_limit_is_rejected() {
352        let keys: Vec<String> = (0..=MAX_OPTIONS).map(|i| format!("key_{i}")).collect();
353        let entries: Vec<(u8, &[u8], u8, &[u8])> = keys
354            .iter()
355            .map(|key| (STRING, key.as_bytes(), STRING, b"v".as_slice()))
356            .collect();
357        let buf = encode(&entries);
358        assert!(matches!(
359            validate_options(&buf),
360            Err(WireError::Validation(_))
361        ));
362
363        let buf = encode(&entries[..MAX_OPTIONS as usize]);
364        assert_eq!(validate_options(&buf).unwrap(), MAX_OPTIONS);
365    }
366
367    #[test]
368    fn block_over_byte_limit_is_rejected() {
369        // 200 maximum-size entries are ~104 KB, over the byte budget while
370        // still well under `MAX_OPTIONS`, so this proves the byte cap trips
371        // independently of the entry count.
372        let value = [b'v'; 255];
373        let keys: Vec<String> = (0..200).map(|i| format!("{i:0>255}")).collect();
374        let entries: Vec<(u8, &[u8], u8, &[u8])> = keys
375            .iter()
376            .map(|key| (STRING, key.as_bytes(), STRING, value.as_slice()))
377            .collect();
378        let buf = encode(&entries);
379        assert!(buf.len() > MAX_OPTIONS_BYTES);
380        assert!(matches!(
381            validate_options(&buf),
382            Err(WireError::Validation(_))
383        ));
384    }
385
386    #[test]
387    fn truncated_block_errors() {
388        let buf = encode(&[(STRING, b"segment_size", STRING, b"1 GiB")]);
389        for i in 1..buf.len() {
390            assert!(
391                validate_options(&buf[..i]).is_err(),
392                "expected error for truncation at byte {i}"
393            );
394        }
395    }
396
397    #[test]
398    fn wire_options_constructors_validate() {
399        let buf = encode(&[
400            (STRING, b"preallocate_segments", STRING, b"true"),
401            (STRING, b"preallocate_segments", STRING, b"false"),
402        ]);
403        assert!(WireOptions::from_slice(&buf).is_err());
404        assert!(WireOptions::from_bytes(buf.freeze()).is_err());
405    }
406
407    #[test]
408    fn encoded_size_matches_bytes() {
409        let buf = encode(&[(STRING, b"key", STRING, b"value")]);
410        let options = WireOptions::from_slice(&buf).unwrap();
411        assert_eq!(options.encoded_size(), buf.len());
412        assert_eq!(options.as_bytes(), &buf[..]);
413    }
414
415    #[test]
416    fn prefixed_roundtrip() {
417        let buf = encode(&[(STRING, b"key", STRING, b"value")]);
418        let options = WireOptions::from_slice(&buf).unwrap();
419        let mut encoded = BytesMut::new();
420        encode_options_prefixed(&options, &mut encoded);
421        assert_eq!(encoded.len(), options_prefixed_size(&options));
422        let (decoded, consumed) = decode_options_prefixed(&encoded, 0).unwrap();
423        assert_eq!(consumed, encoded.len());
424        assert_eq!(decoded, options);
425    }
426
427    /// The cross-SDK golden vector for an options block.
428    ///
429    /// Every SDK writes this block from its own encoder, so "it round-trips
430    /// through my own decoder" proves nothing about interoperability. The bytes
431    /// here are the contract: `foreign/{node,go,java}` pin the identical vector
432    /// in their own unit tests, and a change to the TLV layout has to break all
433    /// of them together instead of leaving one SDK talking to itself.
434    ///
435    /// The vector covers Bool, Uint64 and String values in insertion order.
436    /// It deliberately differs from Rust's sorted map order. Decoders accept
437    /// either ordering, and the bytes pin the per-entry layout across SDKs.
438    const GOLDEN_OPTIONS_BLOCK: &[u8] = &[
439        2, 20, 0, 0, 0, // key kind String, length 20
440        b'p', b'r', b'e', b'a', b'l', b'l', b'o', b'c', b'a', b't', b'e', b'_', b's', b'e', b'g',
441        b'm', b'e', b'n', b't', b's', 3, 1, 0, 0, 0, 1, // value kind Bool, length 1, true
442        2, 12, 0, 0, 0, // key kind String, length 12
443        b's', b'e', b'g', b'm', b'e', b'n', b't', b'_', b's', b'i', b'z', b'e', 12, 8, 0, 0,
444        0, // value kind Uint64, length 8
445        0, 0, 0, 64, 0, 0, 0, 0, // 1 GiB little-endian
446        2, 10, 0, 0, 0, 100, 117, 114, 97, 98, 105, 108, 105, 116, 121, 2, 9, 0, 0, 0, 112, 101,
447        114, 115, 105, 115, 116, 101, 100,
448    ];
449
450    #[test]
451    fn golden_options_block_is_byte_stable() {
452        let encoded = encode(&[
453            (STRING, b"preallocate_segments", 3, &[1]),
454            (
455                STRING,
456                b"segment_size",
457                UINT64,
458                &1_073_741_824u64.to_le_bytes(),
459            ),
460            (STRING, b"durability", STRING, b"persisted"),
461        ]);
462
463        assert_eq!(
464            &encoded[..],
465            GOLDEN_OPTIONS_BLOCK,
466            "the options TLV layout changed; update every SDK's copy of this vector"
467        );
468        assert_eq!(validate_options(GOLDEN_OPTIONS_BLOCK).unwrap(), 3);
469    }
470
471    #[test]
472    fn unsorted_keys_are_accepted() {
473        let unsorted = encode(&[
474            (
475                STRING,
476                b"segment_size",
477                UINT64,
478                &1_073_741_824u64.to_le_bytes(),
479            ),
480            (STRING, b"preallocate_segments", 3, &[1]),
481        ]);
482
483        assert_ne!(&unsorted[..], GOLDEN_OPTIONS_BLOCK);
484        assert_eq!(validate_options(&unsorted).unwrap(), 2);
485    }
486
487    #[test]
488    fn prefixed_truncation_errors() {
489        let buf = encode(&[(STRING, b"key", STRING, b"value")]);
490        let options = WireOptions::from_slice(&buf).unwrap();
491        let mut encoded = BytesMut::new();
492        encode_options_prefixed(&options, &mut encoded);
493        for i in 0..encoded.len() {
494            assert!(
495                decode_options_prefixed(&encoded[..i], 0).is_err(),
496                "expected error for truncation at byte {i}"
497            );
498        }
499    }
500}