Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ do not add another media abstraction or a renderer crate during stabilization.
- [E2EE API](/quest/m0/e2ee-api.md) - epoch-scoped ownership replaces raw crypto, catalog helpers, and process-global claims
- [Socket group](/quest/m0/sock-group.md) - complete formation and retained sockets precede usable serving handles
- [uring identity](/quest/m0/uring-identity.md) - sockets and connections carry their worker and steering identity
- [Archive ranges](/quest/m0/archive-ranges.md) - one finite inclusive range convention replaces reversed integer pairs
- [Archive listing](/quest/m0/archive-listing.md) - one recording-scoped query exposes only supported listing behavior
- [Vulkan/CUDA surfaces](/quest/m0/video-vulkan-cuda.md) - retain producer slots
and synchronize GPU access safely across Vulkan and CUDA
Expand Down
34 changes: 0 additions & 34 deletions quest/m0/archive-ranges.md

This file was deleted.

1 change: 0 additions & 1 deletion quest/m1/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ Public API: none beyond the required quests. Wire: none.
- [E2EE API](/quest/m0/e2ee-api.md) - expose epoch-scoped ownership and align the implemented profile
- [Socket group](/quest/m0/sock-group.md) - make partial reuseport groups and early socket drops unrepresentable
- [uring identity](/quest/m0/uring-identity.md) - bind sockets, connections, workers, and steering identity together
- [Archive ranges](/quest/m0/archive-ranges.md) - use one validated inclusive range contract
- [Archive listing](/quest/m0/archive-listing.md) - expose one recording-scoped listing query
- [Merge dev](/quest/m1/merge-dev.md) - the tree the release is cut from
- [Binding audio tests](/quest/m2/binding-audio-tests.md) - every binding proves the audio config it exposes
Expand Down
4 changes: 2 additions & 2 deletions rs/moq-archive/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ pub enum Error {
#[error("group sequences are not strictly ascending")]
Sequence,

/// A range object's table does not match its filename bounds.
#[error("table bounds {smallest}..={largest} do not match the key")]
/// A group range is empty, reversed, or does not match an object's table.
#[error("invalid or mismatched group bounds {smallest}..={largest}")]
Bounds { smallest: u64, largest: u64 },

/// The binary table is truncated, overlapping, gapped, or out of range.
Expand Down
7 changes: 7 additions & 0 deletions rs/moq-archive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
//! The crate owns the portable layout and codecs: percent-encoded track names, `.info` JSON,
//! the binary segment envelope, and put/get/list/delete. Callers that need runtime dispatch
//! supply `Arc<dyn ObjectStore>`; the archive API itself stays generic.
//!
//! Group bounds are finite inclusive ranges in first-to-last order:
//! ```
//! let key = moq_archive::Key::groups("video", 5..=7)?;
//! assert_eq!(key.track(), "video");
//! # Ok::<(), moq_archive::Error>(())
//! ```

pub use object_store;

Expand Down
99 changes: 70 additions & 29 deletions rs/moq-archive/src/path.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::ops::RangeInclusive;

use object_store::path::{Path, PathPart};
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};

Expand Down Expand Up @@ -58,10 +60,8 @@ pub enum Key {
Groups {
/// The unencoded track name.
track: String,
/// Inclusive last group sequence in the object.
largest: u64,
/// Inclusive first group sequence in the object.
smallest: u64,
/// Inclusive group sequences in the object, from first to last.
range: RangeInclusive<u64>,
},
/// `<encoded-track>/segments/<segment>`
Segments {
Expand All @@ -80,20 +80,12 @@ impl Key {
Ok(Self::Info { track })
}

/// A range-named groups object. `largest` is the last sequence, `smallest` the first.
pub fn groups(track: impl Into<String>, largest: u64, smallest: u64) -> Result<Self> {
/// A groups object named by its inclusive first-to-last sequence range.
pub fn groups(track: impl Into<String>, range: RangeInclusive<u64>) -> Result<Self> {
let track = track.into();
encode_track(&track)?;
check_id(largest)?;
check_id(smallest)?;
if largest < smallest {
return Err(Error::Bounds { smallest, largest });
}
Ok(Self::Groups {
track,
largest,
smallest,
})
check_range(&range)?;
Ok(Self::Groups { track, range })
}

/// A timeline object at `segments/<segment>`.
Expand All @@ -116,9 +108,9 @@ impl Key {
let path = push(prefix, &encode_track(self.track())?)?;
match self {
Self::Info { .. } => push(&path, ".info"),
Self::Groups { largest, smallest, .. } => {
Self::Groups { range, .. } => {
let path = push(&path, "groups")?;
push(&path, &range_name(*largest, *smallest)?)
push(&path, &range_name(range)?)
}
Self::Segments { segment, .. } => {
let path = push(&path, "segments")?;
Expand Down Expand Up @@ -155,12 +147,8 @@ fn parse_parts<'a>(mut parts: impl Iterator<Item = PathPart<'a>>, location: &Pat
if parts.next().is_some() {
return Err(Error::Path(location.to_string()));
}
let (largest, smallest) = parse_range(name.as_ref())?;
Ok(Key::Groups {
track,
largest,
smallest,
})
let range = parse_range(name.as_ref())?;
Ok(Key::Groups { track, range })
}
"segments" => {
let name = parts.next().ok_or_else(|| Error::Path(location.to_string()))?;
Expand Down Expand Up @@ -199,11 +187,13 @@ pub(crate) fn push(base: &Path, segment: &str) -> Result<Path> {
Ok(base.clone().join(part))
}

fn range_name(largest: u64, smallest: u64) -> Result<String> {
fn range_name(range: &RangeInclusive<u64>) -> Result<String> {
check_range(range)?;
let (smallest, largest) = (*range.start(), *range.end());
Ok(format!("{}.{}", format_id(largest)?, format_id(smallest)?))
}

fn parse_range(name: &str) -> Result<(u64, u64)> {
fn parse_range(name: &str) -> Result<RangeInclusive<u64>> {
let (largest, smallest) = name.split_once('.').ok_or_else(|| Error::Path(name.to_string()))?;
if smallest.contains('.') {
return Err(Error::Path(name.to_string()));
Expand All @@ -213,7 +203,17 @@ fn parse_range(name: &str) -> Result<(u64, u64)> {
if largest < smallest {
return Err(Error::Bounds { smallest, largest });
}
Ok((largest, smallest))
Ok(smallest..=largest)
}

pub(crate) fn check_range(range: &RangeInclusive<u64>) -> Result<()> {
let (smallest, largest) = (*range.start(), *range.end());
check_id(smallest)?;
check_id(largest)?;
if range.is_empty() {
return Err(Error::Bounds { smallest, largest });
}
Ok(())
}

#[cfg(test)]
Expand Down Expand Up @@ -282,7 +282,7 @@ mod tests {
let prefix = Path::from("rec/1");
for key in [
Key::info("catalog.json").unwrap(),
Key::groups("video", 10, 5).unwrap(),
Key::groups("video", 5..=10).unwrap(),
Key::segments("timeline.z", 0).unwrap(),
Key::segments("timeline.z", ID_MAX).unwrap(),
] {
Expand All @@ -299,13 +299,54 @@ mod tests {

#[test]
fn inverted_range_is_rejected() {
let (smallest, largest) = (2, 1);
assert!(matches!(
Key::groups("v", smallest..=largest),
Err(Error::Bounds {
smallest: 2,
largest: 1
})
));
}

#[test]
fn exhausted_range_is_rejected() {
let mut range = 1..=1;
assert_eq!(range.next(), Some(1));
assert!(matches!(Key::groups("v", range), Err(Error::Bounds { .. })));
}

#[test]
fn range_id_endpoints_are_valid() {
let key = Key::groups("v", 0..=ID_MAX).unwrap();
assert_eq!(
key.path(&Path::from("rec")).unwrap().as_ref(),
"rec/v/groups/0009007199254740991.0000000000000000000"
);
assert!(matches!(Key::groups("v", 0..=ID_MAX + 1), Err(Error::Id(_))));
assert!(matches!(Key::groups("v", ID_MAX + 1..=ID_MAX + 1), Err(Error::Id(_))));
}

#[test]
fn direct_inverted_range_is_rejected_when_serialized() {
let (smallest, largest) = (2, 1);
let key = Key::Groups {
track: "v".to_string(),
range: smallest..=largest,
};
assert!(matches!(
Key::groups("v", 1, 2),
key.path(&Path::from("rec")),
Err(Error::Bounds {
smallest: 2,
largest: 1
})
));

let key = Key::Groups {
track: "v".to_string(),
range: 0..=ID_MAX + 1,
};
assert!(matches!(key.path(&Path::from("rec")), Err(Error::Id(_))));
}

#[test]
Expand Down
43 changes: 28 additions & 15 deletions rs/moq-archive/src/segment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::ops::RangeInclusive;

use bytes::{Buf, BufMut, Bytes, BytesMut};
use moq_net::VarInt;

use crate::path::check_id;
use crate::path::{check_id, check_range};
use crate::{Error, Result, VERSION};

/// One complete group in a segment object, in sequence order.
Expand Down Expand Up @@ -30,10 +32,10 @@ pub struct Object {
}

impl Object {
/// Inclusive first and last group sequences.
pub fn bounds(&self) -> Result<(u64, u64)> {
/// Inclusive group sequences from first to last.
pub fn bounds(&self) -> Result<RangeInclusive<u64>> {
validate(&self.groups)?;
Ok((self.groups[0].sequence, self.groups.last().unwrap().sequence))
Ok(self.groups[0].sequence..=self.groups.last().unwrap().sequence)
}

/// Encode the binary envelope. The first sequence is absolute; later ones are `current - previous - 1`.
Expand Down Expand Up @@ -158,20 +160,22 @@ impl Object {
Ok(Self { groups })
}

/// Decode and require the table's first and last sequences to match `smallest` and `largest`.
pub fn decode_groups(buf: impl Buf, largest: u64, smallest: u64) -> Result<Self> {
/// Decode and require the table's sequences to match `range`.
pub fn decode_groups(buf: impl Buf, range: RangeInclusive<u64>) -> Result<Self> {
check_range(&range)?;
let object = Self::decode(buf)?;
object.check_bounds(largest, smallest)?;
object.check_bounds(range)?;
Ok(object)
}

/// Require this object's sequences to match a range-named key.
pub fn check_bounds(&self, largest: u64, smallest: u64) -> Result<()> {
let (got_smallest, got_largest) = self.bounds()?;
if got_smallest != smallest || got_largest != largest {
pub fn check_bounds(&self, range: RangeInclusive<u64>) -> Result<()> {
check_range(&range)?;
let got = self.bounds()?;
if got != range {
return Err(Error::Bounds {
smallest: got_smallest,
largest: got_largest,
smallest: *got.start(),
largest: *got.end(),
});
}
Ok(())
Expand Down Expand Up @@ -250,7 +254,8 @@ mod tests {
]);
let bytes = original.encode().unwrap();
assert_eq!(Object::decode(&bytes[..]).unwrap(), original);
original.check_bounds(4, 0).unwrap();
assert_eq!(original.bounds().unwrap(), 0..=4);
original.check_bounds(0..=4).unwrap();
}

#[test]
Expand Down Expand Up @@ -421,14 +426,22 @@ mod tests {
},
]);
let bytes = object.encode().unwrap();
assert!(Object::decode_groups(&bytes[..], 7, 5).is_ok());
assert!(Object::decode_groups(&bytes[..], 5..=7).is_ok());
assert!(matches!(
Object::decode_groups(&bytes[..], 7, 6),
Object::decode_groups(&bytes[..], 6..=7),
Err(Error::Bounds {
smallest: 5,
largest: 7
})
));
let (smallest, largest) = (7, 5);
assert!(matches!(
Object::decode_groups(&bytes[..], smallest..=largest),
Err(Error::Bounds {
smallest: 7,
largest: 5
})
));
}

#[test]
Expand Down
Loading
Loading