Skip to main content

moq_loc/
lib.rs

1//! Wire encoding for the Low Overhead Container (LOC) defined in
2//! [draft-ietf-moq-loc](https://www.ietf.org/archive/id/draft-ietf-moq-loc-00.html).
3//!
4//! A LOC frame is laid out as:
5//!
6//! ```text
7//! [varint: properties_length]
8//! [properties_block: properties_length bytes of KVPs]
9//! [codec_bitstream: remaining bytes]
10//! ```
11//!
12//! Each KVP starts with a delta-encoded type id. Even types carry a single
13//! varint value, odd types carry length-prefixed bytes. Recognized types:
14//!
15//! | ID   | Name        | Decoded into       |
16//! |------|-------------|--------------------|
17//! | 0x06 | Timestamp   | [`Frame::timestamp`] (required) |
18//! | 0x08 | Timescale   | [`Frame::timescale`] (optional, per-frame override) |
19//! | 0x0d | Video Config | Skipped. The hang catalog's `description` is authoritative. |
20//!
21//! Any other property is silently skipped on decode and never emitted on
22//! encode. Public properties are not handled here. They belong in the MoQ
23//! object header and are stripped by the transport layer.
24//!
25//! Varint encoding is QUIC-style throughout via [`moq_net::VarInt`].
26
27use bytes::{Buf, Bytes, BytesMut};
28use moq_net::{BoundsExceeded, DecodeError, EncodeError, VarInt};
29
30/// Property IDs recognized by this implementation.
31const PROP_TIMESTAMP: u64 = 0x06;
32const PROP_TIMESCALE: u64 = 0x08;
33
34/// A decoded LOC frame.
35#[derive(Clone, Debug)]
36pub struct Frame {
37	/// Presentation timestamp, in units determined by the active timescale.
38	pub timestamp: u64,
39
40	/// Per-frame timescale override (property 0x08).
41	///
42	/// `Some` when the frame carried an explicit timescale, `None` when it
43	/// relies on the catalog's default.
44	pub timescale: Option<u64>,
45
46	/// Codec bitstream payload (the bytes after the properties block).
47	pub payload: Bytes,
48}
49
50/// Errors from LOC frame encode/decode.
51#[derive(Debug, Clone, thiserror::Error)]
52#[non_exhaustive]
53pub enum Error {
54	/// The frame's property block did not contain a 0x06 (Timestamp) entry.
55	#[error("loc frame missing required timestamp property")]
56	MissingTimestamp,
57
58	/// The property block ran past `properties_length` or was otherwise malformed.
59	#[error("malformed loc properties")]
60	MalformedProperties,
61
62	/// A varint did not fit in the buffer.
63	#[error("short buffer")]
64	ShortBuffer,
65
66	/// A value exceeds the 62-bit varint range.
67	#[error("value out of range: {0}")]
68	OutOfRange(#[from] BoundsExceeded),
69}
70
71// DecodeError / EncodeError intentionally collapse into ShortBuffer vs the
72// caller's catch-all variant, so they stay as manual From impls; #[from] can't
73// express that mapping.
74impl From<DecodeError> for Error {
75	fn from(err: DecodeError) -> Self {
76		match err {
77			DecodeError::Short => Self::ShortBuffer,
78			_ => Self::MalformedProperties,
79		}
80	}
81}
82
83impl From<EncodeError> for Error {
84	fn from(err: EncodeError) -> Self {
85		match err {
86			EncodeError::Short => Self::ShortBuffer,
87			_ => Self::OutOfRange(BoundsExceeded),
88		}
89	}
90}
91
92/// Decode a LOC frame.
93///
94/// Consumes the properties_length prefix, walks the bounded property block,
95/// and returns the remainder as `payload`.
96pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
97	let properties_length: u64 = VarInt::decode_quic(&mut buf)?.into();
98	let properties_length: usize = properties_length.try_into().map_err(|_| Error::MalformedProperties)?;
99
100	if properties_length > buf.remaining() {
101		return Err(Error::MalformedProperties);
102	}
103
104	let mut props = buf.split_to(properties_length);
105
106	let mut timestamp: Option<u64> = None;
107	let mut timescale: Option<u64> = None;
108	let mut prev_type: u64 = 0;
109	let mut first = true;
110
111	while props.has_remaining() {
112		let delta: u64 = VarInt::decode_quic(&mut props)?.into();
113		let abs = if first {
114			first = false;
115			delta
116		} else {
117			prev_type.checked_add(delta).ok_or(Error::MalformedProperties)?
118		};
119		prev_type = abs;
120
121		if abs % 2 == 0 {
122			let value: u64 = VarInt::decode_quic(&mut props)?.into();
123			match abs {
124				PROP_TIMESTAMP => timestamp = Some(value),
125				PROP_TIMESCALE => {
126					if value == 0 {
127						return Err(Error::MalformedProperties);
128					}
129					timescale = Some(value);
130				}
131				_ => {}
132			}
133		} else {
134			let len: u64 = VarInt::decode_quic(&mut props)?.into();
135			let len: usize = len.try_into().map_err(|_| Error::MalformedProperties)?;
136			if len > props.remaining() {
137				return Err(Error::MalformedProperties);
138			}
139			// We don't care about any odd-typed property today; PROP_VIDEO_CONFIG
140			// (0x0d) and any unknown ID are skipped.
141			props.advance(len);
142		}
143	}
144
145	let timestamp = timestamp.ok_or(Error::MissingTimestamp)?;
146
147	Ok(Frame {
148		timestamp,
149		timescale,
150		payload: buf,
151	})
152}
153
154/// Encode a LOC frame with a single 0x06 Timestamp property.
155///
156/// Per-frame 0x08 timescale is never emitted. The encoder relies on the
157/// catalog timescale to interpret `timestamp`.
158pub fn encode(timestamp: u64, payload: &[u8]) -> Result<Bytes, Error> {
159	let mut props = BytesMut::with_capacity(16);
160	VarInt::try_from(PROP_TIMESTAMP)?.encode_quic(&mut props)?;
161	VarInt::try_from(timestamp)?.encode_quic(&mut props)?;
162
163	let mut out = BytesMut::with_capacity(props.len() + payload.len() + 8);
164	VarInt::try_from(props.len() as u64)?.encode_quic(&mut out)?;
165	out.extend_from_slice(&props);
166	out.extend_from_slice(payload);
167
168	Ok(out.freeze())
169}
170
171#[cfg(test)]
172mod tests {
173	use super::*;
174
175	/// Test helper: write a u64 as a QUIC varint into `buf`.
176	fn write_varint(buf: &mut BytesMut, value: u64) {
177		VarInt::try_from(value).unwrap().encode_quic(buf).unwrap();
178	}
179
180	#[test]
181	fn roundtrip() {
182		let payload = Bytes::from_static(b"hello world");
183		let encoded = encode(12345, &payload).unwrap();
184
185		let frame = decode(encoded).unwrap();
186		assert_eq!(frame.timestamp, 12345);
187		assert_eq!(frame.timescale, None);
188		assert_eq!(frame.payload, payload);
189	}
190
191	#[test]
192	fn decode_per_frame_timescale() {
193		// Manually craft: properties = [delta=0x06 timestamp=96000, delta=0x02 (abs=0x08) timescale=48000]
194		let mut props = BytesMut::new();
195		write_varint(&mut props, PROP_TIMESTAMP);
196		write_varint(&mut props, 96_000);
197		write_varint(&mut props, PROP_TIMESCALE - PROP_TIMESTAMP); // delta = 2
198		write_varint(&mut props, 48_000);
199
200		let mut frame = BytesMut::new();
201		write_varint(&mut frame, props.len() as u64);
202		frame.extend_from_slice(&props);
203		frame.extend_from_slice(b"payload");
204
205		let decoded = decode(frame.freeze()).unwrap();
206		assert_eq!(decoded.timestamp, 96_000);
207		assert_eq!(decoded.timescale, Some(48_000));
208		assert_eq!(decoded.payload, Bytes::from_static(b"payload"));
209	}
210
211	#[test]
212	fn decode_skips_video_config() {
213		// properties = [delta=0x06 timestamp=10, delta=0x07 (abs=0x0d, video config) bytes=[1,2,3]]
214		let mut props = BytesMut::new();
215		write_varint(&mut props, PROP_TIMESTAMP);
216		write_varint(&mut props, 10);
217		write_varint(&mut props, 0x0d - PROP_TIMESTAMP); // delta = 7 -> abs 0x0d (Video Config)
218		write_varint(&mut props, 3); // length
219		props.extend_from_slice(&[0x01, 0x02, 0x03]);
220
221		let mut frame = BytesMut::new();
222		write_varint(&mut frame, props.len() as u64);
223		frame.extend_from_slice(&props);
224		frame.extend_from_slice(b"data");
225
226		let decoded = decode(frame.freeze()).unwrap();
227		assert_eq!(decoded.timestamp, 10);
228		assert_eq!(decoded.timescale, None);
229		assert_eq!(decoded.payload, Bytes::from_static(b"data"));
230	}
231
232	#[test]
233	fn decode_missing_timestamp_errors() {
234		// properties = [delta=0x08 timescale=1000], no timestamp
235		let mut props = BytesMut::new();
236		write_varint(&mut props, PROP_TIMESCALE);
237		write_varint(&mut props, 1000);
238
239		let mut frame = BytesMut::new();
240		write_varint(&mut frame, props.len() as u64);
241		frame.extend_from_slice(&props);
242		frame.extend_from_slice(b"x");
243
244		assert!(matches!(decode(frame.freeze()), Err(Error::MissingTimestamp)));
245	}
246
247	#[test]
248	fn decode_empty_properties_errors() {
249		let mut frame = BytesMut::new();
250		write_varint(&mut frame, 0);
251		frame.extend_from_slice(b"payload");
252
253		assert!(matches!(decode(frame.freeze()), Err(Error::MissingTimestamp)));
254	}
255
256	#[test]
257	fn decode_rejects_zero_timescale() {
258		// Per-frame 0x08 timescale of 0 is invalid (would divide by zero).
259		let mut props = BytesMut::new();
260		write_varint(&mut props, PROP_TIMESTAMP);
261		write_varint(&mut props, 10);
262		write_varint(&mut props, PROP_TIMESCALE - PROP_TIMESTAMP);
263		write_varint(&mut props, 0);
264
265		let mut frame = BytesMut::new();
266		write_varint(&mut frame, props.len() as u64);
267		frame.extend_from_slice(&props);
268		frame.extend_from_slice(b"x");
269
270		assert!(matches!(decode(frame.freeze()), Err(Error::MalformedProperties)));
271	}
272
273	#[test]
274	fn decode_overflowing_properties_length_errors() {
275		let mut frame = BytesMut::new();
276		write_varint(&mut frame, 100); // claims 100 bytes of properties
277		frame.extend_from_slice(&[0x06]); // only 1 byte follows
278
279		assert!(matches!(decode(frame.freeze()), Err(Error::MalformedProperties)));
280	}
281}