1use 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
38pub const MAX_OPTIONS: u32 = 1024;
40
41pub const MAX_OPTIONS_BYTES: usize = 100 * 1000;
54
55const STRING_KIND: WireHeaderKind = WireHeaderKind(2);
58
59pub 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#[must_use]
130pub fn options_prefixed_size(options: &WireOptions) -> usize {
131 4 + options.encoded_size()
132}
133
134pub 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
141pub 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 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#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct WireOptions(Bytes);
181
182impl WireOptions {
183 pub fn from_slice(buf: &[u8]) -> Result<Self, WireError> {
188 validate_options(buf)?;
189 Ok(Self(Bytes::copy_from_slice(buf)))
190 }
191
192 pub fn from_bytes(buf: Bytes) -> Result<Self, WireError> {
197 validate_options(&buf)?;
198 Ok(Self(buf))
199 }
200
201 pub const fn from_validated(buf: Bytes) -> Self {
207 Self(buf)
208 }
209
210 #[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 #[must_use]
223 pub fn as_bytes(&self) -> &[u8] {
224 &self.0
225 }
226
227 #[must_use]
229 pub fn into_bytes(self) -> Bytes {
230 self.0
231 }
232
233 #[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 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 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 const GOLDEN_OPTIONS_BLOCK: &[u8] = &[
439 2, 20, 0, 0, 0, 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, 2, 12, 0, 0, 0, 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, 0, 0, 0, 64, 0, 0, 0, 0, 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}