1use broadcast_common::{Parse, Serialize};
38
39use crate::RtmpError;
40
41type Result<T> = core::result::Result<T, RtmpError>;
42
43pub mod marker {
45 pub const NUMBER: u8 = 0x00;
47 pub const BOOLEAN: u8 = 0x01;
49 pub const STRING: u8 = 0x02;
51 pub const OBJECT: u8 = 0x03;
53 pub const NULL: u8 = 0x05;
55 pub const UNDEFINED: u8 = 0x06;
57 pub const ECMA_ARRAY: u8 = 0x08;
60 pub const OBJECT_END: u8 = 0x09;
63 pub const STRICT_ARRAY: u8 = 0x0A;
65 pub const DATE: u8 = 0x0B;
68 pub const LONG_STRING: u8 = 0x0C;
71}
72
73pub const MAX_AMF0_DEPTH: usize = 32;
80
81const MARKER_LEN: usize = 1;
82const NUMBER_LEN: usize = 8;
83const BOOLEAN_LEN: usize = 1;
84const U16_LEN: usize = 2;
85const U32_LEN: usize = 4;
86const DATE_RESERVED_LEN: usize = 2;
87const OBJECT_END_LEN: usize = 3;
89
90#[non_exhaustive]
102#[derive(Debug, Clone, PartialEq)]
103#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
104pub enum Amf0Value {
105 Number(f64),
107 Boolean(bool),
109 String(String),
111 Object(Vec<(String, Amf0Value)>),
113 Null,
115 Undefined,
117 EcmaArray(Vec<(String, Amf0Value)>),
120 StrictArray(Vec<Amf0Value>),
122 Date(f64),
124 LongString(String),
127}
128
129fn buffer_too_short(need: usize, have: usize, what: &'static str) -> RtmpError {
130 RtmpError::BufferTooShort { need, have, what }
131}
132
133fn read_utf8_short(bytes: &[u8], what: &'static str) -> Result<(String, usize)> {
138 if bytes.len() < U16_LEN {
139 return Err(buffer_too_short(U16_LEN, bytes.len(), what));
140 }
141 let len = u16::from_be_bytes([bytes[0], bytes[1]]) as usize;
142 let total = U16_LEN
148 .checked_add(len)
149 .ok_or(RtmpError::Malformed { what })?;
150 if bytes.len() < total {
151 return Err(buffer_too_short(total, bytes.len(), what));
152 }
153 let s = String::from_utf8(bytes[U16_LEN..total].to_vec())
154 .map_err(|_| RtmpError::Malformed { what })?;
155 Ok((s, total))
156}
157
158fn read_utf8_long(bytes: &[u8], what: &'static str) -> Result<(String, usize)> {
160 if bytes.len() < U32_LEN {
161 return Err(buffer_too_short(U32_LEN, bytes.len(), what));
162 }
163 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
164 let total = U32_LEN
170 .checked_add(len)
171 .ok_or(RtmpError::Malformed { what })?;
172 if bytes.len() < total {
173 return Err(buffer_too_short(total, bytes.len(), what));
174 }
175 let s = String::from_utf8(bytes[U32_LEN..total].to_vec())
176 .map_err(|_| RtmpError::Malformed { what })?;
177 Ok((s, total))
178}
179
180fn parse_pairs(bytes: &[u8], depth: usize) -> Result<(Vec<(String, Amf0Value)>, usize)> {
186 let mut consumed = 0;
187 let mut pairs = Vec::new();
188 loop {
189 let (key, key_len) = read_utf8_short(&bytes[consumed..], "amf0 object key")?;
190 let after_key = consumed + key_len;
191 if key.is_empty() {
192 if bytes.len() < after_key + MARKER_LEN {
193 return Err(buffer_too_short(
194 after_key + MARKER_LEN,
195 bytes.len(),
196 "amf0 object-end marker",
197 ));
198 }
199 if bytes[after_key] == marker::OBJECT_END {
200 return Ok((pairs, after_key + MARKER_LEN));
201 }
202 }
203 let value = parse_value(&bytes[after_key..], depth)?;
204 let value_len = value.serialized_len();
205 pairs.push((key, value));
206 consumed = after_key + value_len;
207 }
208}
209
210fn parse_value(bytes: &[u8], depth: usize) -> Result<Amf0Value> {
217 if bytes.is_empty() {
218 return Err(buffer_too_short(MARKER_LEN, 0, "amf0 value marker"));
219 }
220 let body = &bytes[MARKER_LEN..];
221 match bytes[0] {
222 marker::NUMBER => {
223 if body.len() < NUMBER_LEN {
224 return Err(buffer_too_short(NUMBER_LEN, body.len(), "amf0 number"));
225 }
226 let mut b = [0u8; NUMBER_LEN];
227 b.copy_from_slice(&body[..NUMBER_LEN]);
228 Ok(Amf0Value::Number(f64::from_be_bytes(b)))
229 }
230 marker::BOOLEAN => {
231 if body.is_empty() {
232 return Err(buffer_too_short(BOOLEAN_LEN, 0, "amf0 boolean"));
233 }
234 Ok(Amf0Value::Boolean(body[0] != 0))
235 }
236 marker::STRING => {
237 let (s, _) = read_utf8_short(body, "amf0 string")?;
238 Ok(Amf0Value::String(s))
239 }
240 marker::OBJECT => {
241 if depth >= MAX_AMF0_DEPTH {
242 return Err(RtmpError::Unsupported {
243 what: "amf0 nesting depth exceeded",
244 });
245 }
246 let (pairs, _) = parse_pairs(body, depth + 1)?;
247 Ok(Amf0Value::Object(pairs))
248 }
249 marker::NULL => Ok(Amf0Value::Null),
250 marker::UNDEFINED => Ok(Amf0Value::Undefined),
251 marker::ECMA_ARRAY => {
252 if depth >= MAX_AMF0_DEPTH {
253 return Err(RtmpError::Unsupported {
254 what: "amf0 nesting depth exceeded",
255 });
256 }
257 if body.len() < U32_LEN {
258 return Err(buffer_too_short(
259 U32_LEN,
260 body.len(),
261 "amf0 ecma array count",
262 ));
263 }
264 let (pairs, _) = parse_pairs(&body[U32_LEN..], depth + 1)?;
269 Ok(Amf0Value::EcmaArray(pairs))
270 }
271 marker::STRICT_ARRAY => {
272 if depth >= MAX_AMF0_DEPTH {
273 return Err(RtmpError::Unsupported {
274 what: "amf0 nesting depth exceeded",
275 });
276 }
277 if body.len() < U32_LEN {
278 return Err(buffer_too_short(
279 U32_LEN,
280 body.len(),
281 "amf0 strict array count",
282 ));
283 }
284 let count = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
285 let mut rest = &body[U32_LEN..];
286 let mut values = Vec::new();
287 for _ in 0..count {
288 let value = parse_value(rest, depth + 1)?;
289 let consumed = value.serialized_len();
290 values.push(value);
291 rest = &rest[consumed..];
292 }
293 Ok(Amf0Value::StrictArray(values))
294 }
295 marker::DATE => {
296 if body.len() < NUMBER_LEN + DATE_RESERVED_LEN {
297 return Err(buffer_too_short(
298 NUMBER_LEN + DATE_RESERVED_LEN,
299 body.len(),
300 "amf0 date",
301 ));
302 }
303 let mut b = [0u8; NUMBER_LEN];
304 b.copy_from_slice(&body[..NUMBER_LEN]);
305 let tz = u16::from_be_bytes([body[NUMBER_LEN], body[NUMBER_LEN + 1]]);
306 if tz != 0 {
307 return Err(RtmpError::Malformed {
308 what: "amf0 date reserved time zone (must be 0x0000)",
309 });
310 }
311 Ok(Amf0Value::Date(f64::from_be_bytes(b)))
312 }
313 marker::LONG_STRING => {
314 let (s, _) = read_utf8_long(body, "amf0 long string")?;
315 Ok(Amf0Value::LongString(s))
316 }
317 _ => Err(RtmpError::Unsupported {
318 what: "amf0 value marker (reserved, legacy, or amf3-switch)",
319 }),
320 }
321}
322
323fn pairs_body_len(pairs: &[(String, Amf0Value)]) -> usize {
324 pairs
325 .iter()
326 .map(|(k, v)| U16_LEN + k.len() + v.serialized_len())
327 .sum::<usize>()
328 + OBJECT_END_LEN
329}
330
331fn write_pairs(pairs: &[(String, Amf0Value)], buf: &mut [u8]) -> Result<usize> {
332 let mut offset = 0;
333 for (k, v) in pairs {
334 let key_total = U16_LEN + k.len();
335 if buf.len() < offset + key_total {
336 return Err(buffer_too_short(
337 offset + key_total,
338 buf.len(),
339 "amf0 object key output",
340 ));
341 }
342 buf[offset..offset + U16_LEN].copy_from_slice(&(k.len() as u16).to_be_bytes());
343 buf[offset + U16_LEN..offset + key_total].copy_from_slice(k.as_bytes());
344 offset += key_total;
345 offset += v.serialize_into(&mut buf[offset..])?;
346 }
347 if buf.len() < offset + OBJECT_END_LEN {
348 return Err(buffer_too_short(
349 offset + OBJECT_END_LEN,
350 buf.len(),
351 "amf0 object-end output",
352 ));
353 }
354 buf[offset] = 0;
355 buf[offset + 1] = 0;
356 buf[offset + 2] = marker::OBJECT_END;
357 Ok(offset + OBJECT_END_LEN)
358}
359
360impl<'a> Parse<'a> for Amf0Value {
361 type Error = RtmpError;
362
363 fn parse(bytes: &'a [u8]) -> Result<Self> {
364 parse_value(bytes, 0)
365 }
366}
367
368impl Serialize for Amf0Value {
369 type Error = RtmpError;
370
371 fn serialized_len(&self) -> usize {
372 MARKER_LEN
373 + match self {
374 Amf0Value::Number(_) | Amf0Value::Date(_) => NUMBER_LEN,
375 Amf0Value::Boolean(_) => BOOLEAN_LEN,
376 Amf0Value::String(s) => U16_LEN + s.len(),
377 Amf0Value::LongString(s) => U32_LEN + s.len(),
378 Amf0Value::Object(pairs) => pairs_body_len(pairs),
379 Amf0Value::Null | Amf0Value::Undefined => 0,
380 Amf0Value::EcmaArray(pairs) => U32_LEN + pairs_body_len(pairs),
381 Amf0Value::StrictArray(values) => {
382 U32_LEN + values.iter().map(Serialize::serialized_len).sum::<usize>()
383 }
384 }
385 + match self {
386 Amf0Value::Date(_) => DATE_RESERVED_LEN,
387 _ => 0,
388 }
389 }
390
391 fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
392 let written = self.serialized_len();
393 if buf.len() < written {
394 return Err(buffer_too_short(written, buf.len(), "amf0 value output"));
395 }
396 let (marker_byte, body) = buf[..written].split_at_mut(MARKER_LEN);
397 match self {
398 Amf0Value::Number(v) => {
399 marker_byte[0] = marker::NUMBER;
400 body[..NUMBER_LEN].copy_from_slice(&v.to_be_bytes());
401 }
402 Amf0Value::Boolean(v) => {
403 marker_byte[0] = marker::BOOLEAN;
404 body[0] = u8::from(*v);
405 }
406 Amf0Value::String(s) => {
407 if s.len() > usize::from(u16::MAX) {
408 return Err(RtmpError::Unsupported {
409 what: "amf0 string exceeds u16 length (use long string)",
410 });
411 }
412 marker_byte[0] = marker::STRING;
413 body[..U16_LEN].copy_from_slice(&(s.len() as u16).to_be_bytes());
414 body[U16_LEN..].copy_from_slice(s.as_bytes());
415 }
416 Amf0Value::LongString(s) => {
417 marker_byte[0] = marker::LONG_STRING;
418 body[..U32_LEN].copy_from_slice(&(s.len() as u32).to_be_bytes());
419 body[U32_LEN..].copy_from_slice(s.as_bytes());
420 }
421 Amf0Value::Object(pairs) => {
422 marker_byte[0] = marker::OBJECT;
423 write_pairs(pairs, body)?;
424 }
425 Amf0Value::Null => marker_byte[0] = marker::NULL,
426 Amf0Value::Undefined => marker_byte[0] = marker::UNDEFINED,
427 Amf0Value::EcmaArray(pairs) => {
428 marker_byte[0] = marker::ECMA_ARRAY;
429 body[..U32_LEN].copy_from_slice(&(pairs.len() as u32).to_be_bytes());
430 write_pairs(pairs, &mut body[U32_LEN..])?;
431 }
432 Amf0Value::StrictArray(values) => {
433 marker_byte[0] = marker::STRICT_ARRAY;
434 body[..U32_LEN].copy_from_slice(&(values.len() as u32).to_be_bytes());
435 let mut offset = U32_LEN;
436 for v in values {
437 offset += v.serialize_into(&mut body[offset..])?;
438 }
439 }
440 Amf0Value::Date(v) => {
441 marker_byte[0] = marker::DATE;
442 body[..NUMBER_LEN].copy_from_slice(&v.to_be_bytes());
443 body[NUMBER_LEN..NUMBER_LEN + DATE_RESERVED_LEN].copy_from_slice(&[0, 0]);
444 }
445 }
446 Ok(written)
447 }
448}
449
450#[derive(Debug, Clone, PartialEq)]
459#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
460pub struct Command {
461 pub name: String,
464 pub transaction_id: f64,
467 pub arguments: Vec<Amf0Value>,
470}
471
472impl Command {
473 pub fn parse(payload: &[u8]) -> Result<Self> {
482 let name_value = Amf0Value::parse(payload)?;
483 let mut offset = name_value.serialized_len();
484 let name = match name_value {
485 Amf0Value::String(s) => s,
486 _ => {
487 return Err(RtmpError::Malformed {
488 what: "rtmp command name (expected amf0 string)",
489 });
490 }
491 };
492
493 let txn_value = Amf0Value::parse(&payload[offset..])?;
494 offset += txn_value.serialized_len();
495 let transaction_id = match txn_value {
496 Amf0Value::Number(n) => n,
497 _ => {
498 return Err(RtmpError::Malformed {
499 what: "rtmp command transaction id (expected amf0 number)",
500 });
501 }
502 };
503
504 let mut arguments = Vec::new();
505 while offset < payload.len() {
506 let value = Amf0Value::parse(&payload[offset..])?;
507 offset += value.serialized_len();
508 arguments.push(value);
509 }
510
511 Ok(Command {
512 name,
513 transaction_id,
514 arguments,
515 })
516 }
517
518 #[must_use]
521 pub fn to_body(&self) -> Vec<u8> {
522 let mut out = Amf0Value::String(self.name.clone()).to_bytes();
523 out.extend(Amf0Value::Number(self.transaction_id).to_bytes());
524 for arg in &self.arguments {
525 out.extend(arg.to_bytes());
526 }
527 out
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534
535 fn round_trip(v: &Amf0Value) {
536 let bytes = v.to_bytes();
537 assert_eq!(bytes.len(), v.serialized_len());
538 let parsed = Amf0Value::parse(&bytes).expect("parse");
539 assert_eq!(&parsed, v);
540 assert_eq!(parsed.to_bytes(), bytes);
542 }
543
544 #[test]
545 fn number_round_trips() {
546 round_trip(&Amf0Value::Number(0.0));
547 round_trip(&Amf0Value::Number(-1.5));
548 round_trip(&Amf0Value::Number(1_000_000.25));
549 }
550
551 #[test]
552 fn boolean_round_trips() {
553 round_trip(&Amf0Value::Boolean(true));
554 round_trip(&Amf0Value::Boolean(false));
555 }
556
557 #[test]
558 fn string_round_trips_including_empty_and_multibyte() {
559 round_trip(&Amf0Value::String(String::new()));
560 round_trip(&Amf0Value::String("live".to_string()));
561 round_trip(&Amf0Value::String("héllo wörld 日本語".to_string()));
562 }
563
564 #[test]
565 fn null_and_undefined_round_trip() {
566 round_trip(&Amf0Value::Null);
567 round_trip(&Amf0Value::Undefined);
568 }
569
570 #[test]
571 fn date_round_trips() {
572 round_trip(&Amf0Value::Date(1_700_000_000_000.0));
573 }
574
575 #[test]
576 fn long_string_round_trips() {
577 round_trip(&Amf0Value::LongString("x".repeat(70_000)));
578 }
579
580 #[test]
581 fn object_round_trips_including_nested_object() {
582 round_trip(&Amf0Value::Object(vec![]));
583 round_trip(&Amf0Value::Object(vec![
584 ("app".to_string(), Amf0Value::String("live".to_string())),
585 ("audioSampleRate".to_string(), Amf0Value::Number(44100.0)),
586 ("live".to_string(), Amf0Value::Boolean(true)),
587 ]));
588 round_trip(&Amf0Value::Object(vec![(
591 "capabilities".to_string(),
592 Amf0Value::Object(vec![("videoCodecs".to_string(), Amf0Value::Number(252.0))]),
593 )]));
594 }
595
596 #[test]
597 fn ecma_array_round_trips() {
598 round_trip(&Amf0Value::EcmaArray(vec![]));
599 round_trip(&Amf0Value::EcmaArray(vec![
600 ("duration".to_string(), Amf0Value::Number(0.0)),
601 ("width".to_string(), Amf0Value::Number(1920.0)),
602 ]));
603 }
604
605 #[test]
606 fn strict_array_round_trips() {
607 round_trip(&Amf0Value::StrictArray(vec![]));
608 round_trip(&Amf0Value::StrictArray(vec![
609 Amf0Value::Number(1.0),
610 Amf0Value::String("two".to_string()),
611 Amf0Value::Boolean(false),
612 Amf0Value::Object(vec![("k".to_string(), Amf0Value::Null)]),
613 ]));
614 }
615
616 #[test]
617 fn ecma_array_count_is_informational_not_cross_checked() {
618 let mut bytes = vec![marker::ECMA_ARRAY];
622 bytes.extend_from_slice(&999u32.to_be_bytes()); bytes.extend_from_slice(&1u16.to_be_bytes());
624 bytes.extend_from_slice(b"k");
625 bytes.push(marker::NULL);
626 bytes.extend_from_slice(&[0, 0, marker::OBJECT_END]);
627
628 let parsed = Amf0Value::parse(&bytes).expect("parse");
629 assert_eq!(
630 parsed,
631 Amf0Value::EcmaArray(vec![("k".to_string(), Amf0Value::Null)])
632 );
633 }
634
635 #[test]
636 fn depth_guard_rejects_pathological_nesting_without_stack_overflow() {
637 let mut inner = vec![marker::NULL];
642 for _ in 0..(MAX_AMF0_DEPTH * 4) {
643 let mut wrapped = vec![marker::OBJECT];
644 wrapped.extend_from_slice(&1u16.to_be_bytes());
645 wrapped.push(b'a');
646 wrapped.extend_from_slice(&inner);
647 wrapped.extend_from_slice(&[0, 0, marker::OBJECT_END]);
648 inner = wrapped;
649 }
650
651 let result = Amf0Value::parse(&inner);
652 assert!(matches!(result, Err(RtmpError::Unsupported { .. })));
653 }
654
655 #[test]
656 fn depth_guard_allows_nesting_at_the_limit() {
657 let mut inner = vec![marker::NULL];
658 for _ in 0..(MAX_AMF0_DEPTH - 1) {
659 let mut wrapped = vec![marker::OBJECT];
660 wrapped.extend_from_slice(&1u16.to_be_bytes());
661 wrapped.push(b'a');
662 wrapped.extend_from_slice(&inner);
663 wrapped.extend_from_slice(&[0, 0, marker::OBJECT_END]);
664 inner = wrapped;
665 }
666 assert!(Amf0Value::parse(&inner).is_ok());
667 }
668
669 #[test]
670 fn dropping_object_end_marker_is_rejected() {
671 let full = Amf0Value::Object(vec![("k".to_string(), Amf0Value::Null)]).to_bytes();
674 let truncated = &full[..full.len() - 3];
675 assert!(Amf0Value::parse(truncated).is_err());
676 }
677
678 #[test]
679 fn mis_sized_string_length_is_rejected() {
680 let mut bytes = vec![marker::STRING];
683 bytes.extend_from_slice(&100u16.to_be_bytes()); bytes.extend_from_slice(b"short"); assert!(matches!(
686 Amf0Value::parse(&bytes),
687 Err(RtmpError::BufferTooShort { .. })
688 ));
689 }
690
691 #[test]
692 fn invalid_utf8_string_is_malformed() {
693 let mut bytes = vec![marker::STRING];
694 bytes.extend_from_slice(&2u16.to_be_bytes());
695 bytes.extend_from_slice(&[0xFF, 0xFE]); assert!(matches!(
697 Amf0Value::parse(&bytes),
698 Err(RtmpError::Malformed { .. })
699 ));
700 }
701
702 #[test]
703 fn unsupported_marker_is_rejected_not_panicking() {
704 assert!(matches!(
705 Amf0Value::parse(&[0x11]), Err(RtmpError::Unsupported { .. })
707 ));
708 assert!(matches!(
709 Amf0Value::parse(&[0x07]), Err(RtmpError::Unsupported { .. })
711 ));
712 }
713
714 #[test]
715 fn date_rejects_nonzero_reserved_timezone() {
716 let mut bytes = vec![marker::DATE];
717 bytes.extend_from_slice(&0.0f64.to_be_bytes());
718 bytes.extend_from_slice(&1u16.to_be_bytes()); assert!(matches!(
720 Amf0Value::parse(&bytes),
721 Err(RtmpError::Malformed { .. })
722 ));
723 }
724
725 #[test]
726 fn empty_buffer_and_truncated_marker_are_buffer_too_short_not_panics() {
727 assert!(matches!(
728 Amf0Value::parse(&[]),
729 Err(RtmpError::BufferTooShort { .. })
730 ));
731 assert!(matches!(
732 Amf0Value::parse(&[marker::NUMBER, 0, 0, 0]),
733 Err(RtmpError::BufferTooShort { .. })
734 ));
735 }
736
737 fn connect_command() -> Command {
740 Command {
741 name: "connect".to_string(),
742 transaction_id: 1.0,
743 arguments: vec![Amf0Value::Object(vec![
744 ("app".to_string(), Amf0Value::String("live".to_string())),
745 (
746 "flashVer".to_string(),
747 Amf0Value::String("FMLE/3.0".to_string()),
748 ),
749 (
750 "tcUrl".to_string(),
751 Amf0Value::String("rtmp://example.test/live".to_string()),
752 ),
753 ("fpad".to_string(), Amf0Value::Boolean(false)),
754 ])],
755 }
756 }
757
758 fn publish_command() -> Command {
759 Command {
760 name: "publish".to_string(),
761 transaction_id: 5.0,
762 arguments: vec![
763 Amf0Value::Null,
764 Amf0Value::String("stream_key_123".to_string()),
765 Amf0Value::String("live".to_string()),
766 ],
767 }
768 }
769
770 #[test]
771 fn connect_command_round_trips_byte_identically() {
772 let cmd = connect_command();
773 let bytes = cmd.to_body();
774 let parsed = Command::parse(&bytes).expect("parse connect");
775 assert_eq!(parsed, cmd);
776 assert_eq!(parsed.to_body(), bytes);
777 }
778
779 #[test]
780 fn publish_command_round_trips_byte_identically() {
781 let cmd = publish_command();
782 let bytes = cmd.to_body();
783 let parsed = Command::parse(&bytes).expect("parse publish");
784 assert_eq!(parsed, cmd);
785 assert_eq!(parsed.to_body(), bytes);
786 }
787
788 #[test]
789 fn command_name_must_be_string() {
790 let bytes = Amf0Value::Number(1.0).to_bytes();
791 assert!(matches!(
792 Command::parse(&bytes),
793 Err(RtmpError::Malformed { .. })
794 ));
795 }
796
797 #[test]
798 fn command_transaction_id_must_be_number() {
799 let mut bytes = Amf0Value::String("connect".to_string()).to_bytes();
800 bytes.extend(Amf0Value::String("not a number".to_string()).to_bytes());
801 assert!(matches!(
802 Command::parse(&bytes),
803 Err(RtmpError::Malformed { .. })
804 ));
805 }
806
807 #[test]
810 fn long_string_length_overflowing_usize_is_rejected_not_wrapped() {
811 let mut bytes = vec![marker::LONG_STRING];
816 bytes.extend_from_slice(&(u32::MAX - 1).to_be_bytes());
817 bytes.extend_from_slice(b"short");
818 let err = Amf0Value::parse(&bytes).unwrap_err();
819 assert!(matches!(
820 err,
821 RtmpError::Malformed { .. } | RtmpError::BufferTooShort { .. }
822 ));
823 }
824
825 #[cfg(feature = "serde")]
828 #[test]
829 fn amf0_value_and_command_serde_round_trip() {
830 let value = Amf0Value::Object(vec![
831 ("app".to_string(), Amf0Value::String("live".to_string())),
832 ("live".to_string(), Amf0Value::Boolean(true)),
833 ("duration".to_string(), Amf0Value::Number(0.0)),
834 (
835 "items".to_string(),
836 Amf0Value::StrictArray(vec![Amf0Value::Null, Amf0Value::Undefined]),
837 ),
838 ]);
839 let json = serde_json::to_string(&value).expect("serialize Amf0Value");
840 let back: Amf0Value = serde_json::from_str(&json).expect("deserialize Amf0Value");
841 assert_eq!(back, value);
842
843 let cmd = publish_command();
844 let json = serde_json::to_string(&cmd).expect("serialize Command");
845 let back: Command = serde_json::from_str(&json).expect("deserialize Command");
846 assert_eq!(back, cmd);
847 }
848}