pub mod compressed_module;
pub mod crc32;
pub mod est_download_time;
pub mod group_link;
pub mod info;
pub mod location;
pub mod module_link;
pub mod name;
pub mod ssu_module_type;
pub mod subgroup_association;
pub mod type_descriptor;
pub use compressed_module::CompressedModuleDescriptor;
pub use crc32::Crc32Descriptor;
pub use est_download_time::EstDownloadTimeDescriptor;
pub use group_link::GroupLinkDescriptor;
pub use info::InfoDescriptor;
pub use location::LocationDescriptor;
pub use module_link::{LinkPosition, ModuleLinkDescriptor};
pub use name::NameDescriptor;
pub use ssu_module_type::{SsuModuleType, SsuModuleTypeDescriptor};
pub use subgroup_association::{SubgroupAssociationDescriptor, SubgroupTag};
pub use type_descriptor::TypeDescriptor;
macro_rules! declare_carousel_descriptors {
(
$lt:lifetime;
$( $variant:ident = $tag:literal => $($path:ident)::+ $(<$plt:lifetime>)? ),+ $(,)?
) => {
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
#[non_exhaustive]
pub enum AnyCarouselDescriptor<$lt> {
$(
#[allow(missing_docs)]
$variant($($path)::+ $(<$plt>)?),
)+
Unknown {
tag: u8,
body: &$lt [u8],
},
}
$(
impl<$lt> From<$($path)::+ $(<$plt>)?> for AnyCarouselDescriptor<$lt> {
fn from(d: $($path)::+ $(<$plt>)?) -> Self {
Self::$variant(d)
}
}
)+
impl<$lt> AnyCarouselDescriptor<$lt> {
pub const DISPATCHED_TAGS: &'static [u8] = &[$($tag),+];
#[must_use]
pub fn name(&self) -> &'static str {
match self {
$(
Self::$variant(_) =>
<$($path)::+ as crate::traits::DescriptorDef>::NAME,
)+
Self::Unknown { .. } => "UNKNOWN",
}
}
pub(crate) fn dispatch(tag: u8, full: &$lt [u8]) -> Option<crate::error::Result<Self>> {
use broadcast_common::Parse;
match tag {
$(
$tag => Some(<$($path)::+>::parse(full).map(Self::$variant)),
)+
_ => None,
}
}
}
#[cfg(test)]
mod macro_drift {
#[test]
fn tag_literals_match_descriptor_def() {
use crate::traits::DescriptorDef;
$(
assert_eq!(
$tag,
<$($path)::+ as DescriptorDef>::TAG,
concat!("tag literal drift for ", stringify!($variant)),
);
assert!(
!<$($path)::+ as DescriptorDef>::NAME.is_empty(),
concat!("empty NAME for ", stringify!($variant)),
);
)+
}
}
};
}
declare_carousel_descriptors! {'a;
Type = 0x01 => crate::descriptors::carousel::type_descriptor::TypeDescriptor<'a>,
Name = 0x02 => crate::descriptors::carousel::name::NameDescriptor<'a>,
Info = 0x03 => crate::descriptors::carousel::info::InfoDescriptor<'a>,
ModuleLink = 0x04 => crate::descriptors::carousel::module_link::ModuleLinkDescriptor,
Crc32 = 0x05 => crate::descriptors::carousel::crc32::Crc32Descriptor,
Location = 0x06 => crate::descriptors::carousel::location::LocationDescriptor,
EstDownloadTime = 0x07 => crate::descriptors::carousel::est_download_time::EstDownloadTimeDescriptor,
GroupLink = 0x08 => crate::descriptors::carousel::group_link::GroupLinkDescriptor,
CompressedModule = 0x09 => crate::descriptors::carousel::compressed_module::CompressedModuleDescriptor,
SsuModuleType = 0x0A => crate::descriptors::carousel::ssu_module_type::SsuModuleTypeDescriptor,
SubgroupAssociation = 0x0B => crate::descriptors::carousel::subgroup_association::SubgroupAssociationDescriptor,
}
#[must_use]
pub fn parse_carousel_loop(bytes: &[u8]) -> CarouselDescriptorIter<'_> {
CarouselDescriptorIter {
bytes,
pos: 0,
fused: false,
}
}
#[derive(Debug, Clone)]
pub struct CarouselDescriptorIter<'a> {
bytes: &'a [u8],
pos: usize,
fused: bool,
}
impl<'a> Iterator for CarouselDescriptorIter<'a> {
type Item = crate::error::Result<AnyCarouselDescriptor<'a>>;
fn next(&mut self) -> Option<Self::Item> {
let (tag, full) = match crate::descriptors::any::next_loop_entry(
self.bytes,
&mut self.pos,
&mut self.fused,
)? {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
Some(match AnyCarouselDescriptor::dispatch(tag, full) {
Some(res) => res,
None => Ok(AnyCarouselDescriptor::Unknown {
tag,
body: &full[2..],
}),
})
}
}
impl core::iter::FusedIterator for CarouselDescriptorIter<'_> {}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default)]
#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
pub struct CarouselDescriptorLoop<'a>(&'a [u8]);
impl core::fmt::Debug for CarouselDescriptorLoop<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "CarouselDescriptorLoop(<{} bytes>)", self.0.len())
}
}
impl<'a> IntoIterator for &CarouselDescriptorLoop<'a> {
type Item = crate::error::Result<AnyCarouselDescriptor<'a>>;
type IntoIter = CarouselDescriptorIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> CarouselDescriptorLoop<'a> {
#[must_use]
pub const fn new(raw: &'a [u8]) -> Self {
Self(raw)
}
#[must_use]
pub const fn raw(&self) -> &'a [u8] {
self.0
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn raw_tags(&self) -> impl Iterator<Item = (u8, &'a [u8])> {
let bytes = self.0;
let mut pos = 0usize;
core::iter::from_fn(move || {
let rem = bytes.get(pos..)?;
let (&tag, rem) = rem.split_first()?;
let (&len, rem) = rem.split_first()?;
let body = rem.get(..usize::from(len))?;
pos += 2 + usize::from(len);
Some((tag, body))
})
}
#[must_use]
pub fn iter(&self) -> CarouselDescriptorIter<'a> {
parse_carousel_loop(self.0)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for CarouselDescriptorLoop<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
struct Entry<'a>(crate::error::Result<AnyCarouselDescriptor<'a>>);
impl serde::Serialize for Entry<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match &self.0 {
Ok(d) => d.serialize(s),
Err(e) => {
use serde::ser::SerializeMap;
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("parseError", &alloc::string::ToString::to_string(e))?;
m.end()
}
}
}
}
s.collect_seq(self.iter().map(Entry))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn walk_typed_and_unknown() {
let buf = [
0x09, 0x05, 0x08, 0x00, 0x00, 0x02, 0x34, 0x05, 0x04, 0xDE, 0xAD, 0xBE, 0xEF, 0x71, 0x02, 0x02, 0xFF, ];
let items: Vec<_> = parse_carousel_loop(&buf).collect();
assert_eq!(items.len(), 3);
assert!(matches!(
items[0].as_ref().unwrap(),
AnyCarouselDescriptor::CompressedModule(c) if c.original_size == 0x234
));
assert!(matches!(
items[1].as_ref().unwrap(),
AnyCarouselDescriptor::Crc32(c) if c.crc_32 == 0xDEAD_BEEF
));
assert!(matches!(
items[2].as_ref().unwrap(),
AnyCarouselDescriptor::Unknown { tag: 0x71, body } if *body == [0x02, 0xFF]
));
assert_eq!(items[0].as_ref().unwrap().name(), "COMPRESSED_MODULE");
}
#[test]
fn loop_helpers_and_truncation() {
let buf = [0x06, 0x01, 0x42, 0x07, 0x04, 0x00];
let l = CarouselDescriptorLoop::new(&buf);
assert_eq!(l.len(), 6);
assert!(!l.is_empty());
assert_eq!(l.raw(), &buf);
assert_eq!(l.raw_tags().count(), 1);
let items: Vec<_> = l.iter().collect();
assert_eq!(items.len(), 2);
assert!(items[0].is_ok() && items[1].is_err());
assert!(CarouselDescriptorLoop::default().is_empty());
}
#[test]
fn typed_parse_error_surfaces_and_walk_continues() {
let buf = [0x06, 0x02, 0x01, 0x02, 0x06, 0x01, 0x09];
let items: Vec<_> = parse_carousel_loop(&buf).collect();
assert_eq!(items.len(), 2);
assert!(items[0].is_err());
assert!(matches!(
items[1].as_ref().unwrap(),
AnyCarouselDescriptor::Location(l) if l.location_tag == 9
));
}
#[cfg(feature = "serde")]
#[test]
fn serde_surfaces_parse_errors_instead_of_dropping_them() {
let buf = [0x06, 0xFF];
let json = serde_json::to_string(&CarouselDescriptorLoop::new(&buf)).unwrap();
assert!(json.contains("parseError"), "{json}");
}
#[test]
fn dispatched_tags_covers_all_known() {
assert_eq!(
AnyCarouselDescriptor::DISPATCHED_TAGS,
&[
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B
]
);
}
}