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, matching the rest of the moq
26//! stack.
27
28use bytes::{Buf, Bytes, BytesMut};
29
30/// Property IDs recognized by this implementation.
31const PROP_TIMESTAMP: u64 = 0x06;
32const PROP_TIMESCALE: u64 = 0x08;
33
34/// Maximum value representable as a 62-bit QUIC varint.
35const VARINT_MAX: u64 = (1u64 << 62) - 1;
36
37/// A decoded LOC frame.
38#[derive(Clone, Debug)]
39pub struct Frame {
40	/// Presentation timestamp, in units determined by the active timescale.
41	pub timestamp: u64,
42
43	/// Per-frame timescale override (property 0x08).
44	///
45	/// `Some` when the frame carried an explicit timescale, `None` when it
46	/// relies on the catalog's default.
47	pub timescale: Option<u64>,
48
49	/// Codec bitstream payload (the bytes after the properties block).
50	pub payload: Bytes,
51}
52
53/// Errors from LOC frame encode/decode.
54#[derive(Debug, thiserror::Error)]
55#[non_exhaustive]
56pub enum Error {
57	/// The frame's property block did not contain a 0x06 (Timestamp) entry.
58	#[error("loc frame missing required timestamp property")]
59	MissingTimestamp,
60
61	/// The property block ran past `properties_length` or was otherwise malformed.
62	#[error("malformed loc properties")]
63	MalformedProperties,
64
65	/// A varint did not fit in the buffer.
66	#[error("short buffer")]
67	ShortBuffer,
68
69	/// A value exceeds the 62-bit varint range.
70	#[error("value out of range")]
71	OutOfRange,
72}
73
74/// Decode a LOC frame.
75///
76/// Consumes the properties_length prefix, walks the bounded property block,
77/// and returns the remainder as `payload`.
78pub fn decode(mut buf: Bytes) -> Result<Frame, Error> {
79	let properties_length = read_varint(&mut buf)?;
80	let properties_length: usize = properties_length.try_into().map_err(|_| Error::MalformedProperties)?;
81
82	if properties_length > buf.remaining() {
83		return Err(Error::MalformedProperties);
84	}
85
86	let mut props = buf.split_to(properties_length);
87
88	let mut timestamp: Option<u64> = None;
89	let mut timescale: Option<u64> = None;
90	let mut prev_type: u64 = 0;
91	let mut first = true;
92
93	while props.has_remaining() {
94		let delta = read_varint(&mut props)?;
95		let abs = if first {
96			first = false;
97			delta
98		} else {
99			prev_type.checked_add(delta).ok_or(Error::MalformedProperties)?
100		};
101		prev_type = abs;
102
103		if abs % 2 == 0 {
104			let value = read_varint(&mut props)?;
105			match abs {
106				PROP_TIMESTAMP => timestamp = Some(value),
107				PROP_TIMESCALE => {
108					if value == 0 {
109						return Err(Error::MalformedProperties);
110					}
111					timescale = Some(value);
112				}
113				_ => {}
114			}
115		} else {
116			let len = read_varint(&mut props)?;
117			let len: usize = len.try_into().map_err(|_| Error::MalformedProperties)?;
118			if len > props.remaining() {
119				return Err(Error::MalformedProperties);
120			}
121			// We don't care about any odd-typed property today; PROP_VIDEO_CONFIG
122			// (0x0d) and any unknown ID are skipped.
123			props.advance(len);
124		}
125	}
126
127	let timestamp = timestamp.ok_or(Error::MissingTimestamp)?;
128
129	Ok(Frame {
130		timestamp,
131		timescale,
132		payload: buf,
133	})
134}
135
136/// Encode a LOC frame with a single 0x06 Timestamp property.
137///
138/// Per-frame 0x08 timescale is never emitted. The encoder relies on the
139/// catalog timescale to interpret `timestamp`.
140pub fn encode(timestamp: u64, payload: &[u8]) -> Result<Bytes, Error> {
141	let mut props = BytesMut::with_capacity(16);
142	write_varint(&mut props, PROP_TIMESTAMP)?;
143	write_varint(&mut props, timestamp)?;
144
145	let mut out = BytesMut::with_capacity(props.len() + payload.len() + 8);
146	write_varint(&mut out, props.len() as u64)?;
147	out.extend_from_slice(&props);
148	out.extend_from_slice(payload);
149
150	Ok(out.freeze())
151}
152
153/// Decode a QUIC-style varint (2-bit length tag in top bits).
154fn read_varint<B: Buf>(buf: &mut B) -> Result<u64, Error> {
155	if !buf.has_remaining() {
156		return Err(Error::ShortBuffer);
157	}
158	let b = buf.get_u8();
159	let tag = b >> 6;
160	let mut bytes = [0u8; 8];
161	bytes[0] = b & 0b0011_1111;
162	let value = match tag {
163		0b00 => u64::from(bytes[0]),
164		0b01 => {
165			if buf.remaining() < 1 {
166				return Err(Error::ShortBuffer);
167			}
168			buf.copy_to_slice(&mut bytes[1..2]);
169			u64::from(u16::from_be_bytes(bytes[..2].try_into().unwrap()))
170		}
171		0b10 => {
172			if buf.remaining() < 3 {
173				return Err(Error::ShortBuffer);
174			}
175			buf.copy_to_slice(&mut bytes[1..4]);
176			u64::from(u32::from_be_bytes(bytes[..4].try_into().unwrap()))
177		}
178		0b11 => {
179			if buf.remaining() < 7 {
180				return Err(Error::ShortBuffer);
181			}
182			buf.copy_to_slice(&mut bytes[1..8]);
183			u64::from_be_bytes(bytes)
184		}
185		_ => unreachable!(),
186	};
187	Ok(value)
188}
189
190/// Encode a QUIC-style varint (2-bit length tag in top bits).
191fn write_varint<B: bytes::BufMut>(buf: &mut B, value: u64) -> Result<(), Error> {
192	if value > VARINT_MAX {
193		return Err(Error::OutOfRange);
194	}
195	if value < (1u64 << 6) {
196		if buf.remaining_mut() < 1 {
197			return Err(Error::ShortBuffer);
198		}
199		buf.put_u8(value as u8);
200	} else if value < (1u64 << 14) {
201		if buf.remaining_mut() < 2 {
202			return Err(Error::ShortBuffer);
203		}
204		buf.put_u16(value as u16 | 0b01 << 14);
205	} else if value < (1u64 << 30) {
206		if buf.remaining_mut() < 4 {
207			return Err(Error::ShortBuffer);
208		}
209		buf.put_u32(value as u32 | 0b10 << 30);
210	} else {
211		if buf.remaining_mut() < 8 {
212			return Err(Error::ShortBuffer);
213		}
214		buf.put_u64(value | 0b11 << 62);
215	}
216	Ok(())
217}
218
219#[cfg(test)]
220mod tests {
221	use super::*;
222
223	#[test]
224	fn roundtrip() {
225		let payload = Bytes::from_static(b"hello world");
226		let encoded = encode(12345, &payload).unwrap();
227
228		let frame = decode(encoded).unwrap();
229		assert_eq!(frame.timestamp, 12345);
230		assert_eq!(frame.timescale, None);
231		assert_eq!(frame.payload, payload);
232	}
233
234	#[test]
235	fn decode_per_frame_timescale() {
236		// Manually craft: properties = [delta=0x06 timestamp=96000, delta=0x02 (abs=0x08) timescale=48000]
237		let mut props = BytesMut::new();
238		write_varint(&mut props, PROP_TIMESTAMP).unwrap();
239		write_varint(&mut props, 96_000).unwrap();
240		write_varint(&mut props, PROP_TIMESCALE - PROP_TIMESTAMP).unwrap(); // delta = 2
241		write_varint(&mut props, 48_000).unwrap();
242
243		let mut frame = BytesMut::new();
244		write_varint(&mut frame, props.len() as u64).unwrap();
245		frame.extend_from_slice(&props);
246		frame.extend_from_slice(b"payload");
247
248		let decoded = decode(frame.freeze()).unwrap();
249		assert_eq!(decoded.timestamp, 96_000);
250		assert_eq!(decoded.timescale, Some(48_000));
251		assert_eq!(decoded.payload, Bytes::from_static(b"payload"));
252	}
253
254	#[test]
255	fn decode_skips_video_config() {
256		// properties = [delta=0x06 timestamp=10, delta=0x07 (abs=0x0d, video config) bytes=[1,2,3]]
257		let mut props = BytesMut::new();
258		write_varint(&mut props, PROP_TIMESTAMP).unwrap();
259		write_varint(&mut props, 10).unwrap();
260		write_varint(&mut props, 0x0d - PROP_TIMESTAMP).unwrap(); // delta = 7 -> abs 0x0d (Video Config)
261		write_varint(&mut props, 3).unwrap(); // length
262		props.extend_from_slice(&[0x01, 0x02, 0x03]);
263
264		let mut frame = BytesMut::new();
265		write_varint(&mut frame, props.len() as u64).unwrap();
266		frame.extend_from_slice(&props);
267		frame.extend_from_slice(b"data");
268
269		let decoded = decode(frame.freeze()).unwrap();
270		assert_eq!(decoded.timestamp, 10);
271		assert_eq!(decoded.timescale, None);
272		assert_eq!(decoded.payload, Bytes::from_static(b"data"));
273	}
274
275	#[test]
276	fn decode_missing_timestamp_errors() {
277		// properties = [delta=0x08 timescale=1000], no timestamp
278		let mut props = BytesMut::new();
279		write_varint(&mut props, PROP_TIMESCALE).unwrap();
280		write_varint(&mut props, 1000).unwrap();
281
282		let mut frame = BytesMut::new();
283		write_varint(&mut frame, props.len() as u64).unwrap();
284		frame.extend_from_slice(&props);
285		frame.extend_from_slice(b"x");
286
287		assert!(matches!(decode(frame.freeze()), Err(Error::MissingTimestamp)));
288	}
289
290	#[test]
291	fn decode_empty_properties_errors() {
292		let mut frame = BytesMut::new();
293		write_varint(&mut frame, 0).unwrap();
294		frame.extend_from_slice(b"payload");
295
296		assert!(matches!(decode(frame.freeze()), Err(Error::MissingTimestamp)));
297	}
298
299	#[test]
300	fn decode_rejects_zero_timescale() {
301		// Per-frame 0x08 timescale of 0 is invalid (would divide by zero).
302		let mut props = BytesMut::new();
303		write_varint(&mut props, PROP_TIMESTAMP).unwrap();
304		write_varint(&mut props, 10).unwrap();
305		write_varint(&mut props, PROP_TIMESCALE - PROP_TIMESTAMP).unwrap();
306		write_varint(&mut props, 0).unwrap();
307
308		let mut frame = BytesMut::new();
309		write_varint(&mut frame, props.len() as u64).unwrap();
310		frame.extend_from_slice(&props);
311		frame.extend_from_slice(b"x");
312
313		assert!(matches!(decode(frame.freeze()), Err(Error::MalformedProperties)));
314	}
315
316	#[test]
317	fn decode_overflowing_properties_length_errors() {
318		let mut frame = BytesMut::new();
319		write_varint(&mut frame, 100).unwrap(); // claims 100 bytes of properties
320		frame.extend_from_slice(&[0x06]); // only 1 byte follows
321
322		assert!(matches!(decode(frame.freeze()), Err(Error::MalformedProperties)));
323	}
324}