Skip to main content

dash_runtime/address/
template.rs

1//! `SegmentTemplate` identifier scanning and substitution (ISO/IEC 23009-1
2//! 5.3.9.4.4, Table 21).
3//!
4//! The scan is strict: a `$` that does not enclose a known identifier makes
5//! the whole template invalid, which the spec resolves by dropping the
6//! containing Representation (5.3.9.4.4 step 2). Format tags are `%0<width>d`
7//! only. Substitution itself is `transmux::SegmentTemplate::resolve`, so the
8//! two crates cannot disagree about widths or `$$`.
9
10use broadcast_common::impl_spec_display;
11use transmux::dash_parse::MAX_FORMAT_WIDTH;
12
13const IDENT_RESERVED: &str = "RepresentationID";
14const IDENT_NUMBER: &str = "Number";
15const IDENT_TIME: &str = "Time";
16const IDENT_BANDWIDTH: &str = "Bandwidth";
17const IDENT_SUB_NUMBER: &str = "SubNumber";
18const TAG_ZERO_FLAG: char = '0';
19const TAG_CONVERSION: char = 'd';
20const DELIMITER: char = '$';
21const TAG_START: char = '%';
22
23/// Which identifiers a template contains.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25#[non_exhaustive]
26pub struct TemplateUse {
27    /// `$RepresentationID$`.
28    pub representation_id: bool,
29    /// `$Number$`.
30    pub number: bool,
31    /// `$Time$`.
32    pub time: bool,
33    /// `$Bandwidth$`.
34    pub bandwidth: bool,
35    /// `$SubNumber$` (segment sequences; unsupported).
36    pub sub_number: bool,
37}
38
39/// Why a template is invalid.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum TemplateFault {
43    /// A `$` with no closing `$`.
44    UnterminatedIdentifier,
45    /// `$name$` where `name` is not in Table 21.
46    UnknownIdentifier,
47    /// A format tag on `$RepresentationID$`, which takes none.
48    FormatOnRepresentationId,
49    /// A format tag that is not `%0<width>d`.
50    BadFormatTag,
51    /// A width above the supported maximum.
52    WidthTooLarge,
53    /// `$Number$` and `$Time$` in one template.
54    NumberAndTime,
55    /// A media template with neither `$Number$` nor `$Time$`.
56    MissingNumberOrTime,
57    /// An `@initialization` template containing `$Number$` or `$Time$`.
58    DynamicIdentifierInInitialization,
59}
60
61impl TemplateFault {
62    /// Stable snake_case label.
63    #[must_use]
64    pub fn name(&self) -> &'static str {
65        match self {
66            Self::UnterminatedIdentifier => "unterminated_identifier",
67            Self::UnknownIdentifier => "unknown_identifier",
68            Self::FormatOnRepresentationId => "format_on_representation_id",
69            Self::BadFormatTag => "bad_format_tag",
70            Self::WidthTooLarge => "width_too_large",
71            Self::NumberAndTime => "number_and_time",
72            Self::MissingNumberOrTime => "missing_number_or_time",
73            Self::DynamicIdentifierInInitialization => "dynamic_identifier_in_initialization",
74        }
75    }
76}
77impl_spec_display!(TemplateFault);
78
79fn check_tag(tag: &str) -> Result<(), TemplateFault> {
80    let inner = tag
81        .strip_suffix(TAG_CONVERSION)
82        .ok_or(TemplateFault::BadFormatTag)?;
83    let digits = inner
84        .strip_prefix(TAG_ZERO_FLAG)
85        .ok_or(TemplateFault::BadFormatTag)?;
86    if !digits.bytes().all(|b| b.is_ascii_digit()) {
87        return Err(TemplateFault::BadFormatTag);
88    }
89    if !digits.is_empty() {
90        let width: usize = digits.parse().map_err(|_| TemplateFault::WidthTooLarge)?;
91        if width > MAX_FORMAT_WIDTH {
92            return Err(TemplateFault::WidthTooLarge);
93        }
94    }
95    Ok(())
96}
97
98/// Scan `template` and report the identifiers it uses.
99///
100/// # Errors
101/// [`TemplateFault`] for anything 5.3.9.4.4 leaves undefined.
102pub fn scan(template: &str) -> Result<TemplateUse, TemplateFault> {
103    let mut used = TemplateUse::default();
104    let mut rest = template;
105    while let Some(at) = rest.find(DELIMITER) {
106        let tail = &rest[at + 1..];
107        if let Some(after) = tail.strip_prefix(DELIMITER) {
108            rest = after;
109            continue;
110        }
111        let end = tail
112            .find(DELIMITER)
113            .ok_or(TemplateFault::UnterminatedIdentifier)?;
114        let ident = &tail[..end];
115        let (name, tag) = match ident.split_once(TAG_START) {
116            Some((n, t)) => (n, Some(format!("{TAG_START}{t}"))),
117            None => (ident, None),
118        };
119        match name {
120            IDENT_RESERVED => {
121                if tag.is_some() {
122                    return Err(TemplateFault::FormatOnRepresentationId);
123                }
124                used.representation_id = true;
125            }
126            IDENT_NUMBER | IDENT_TIME | IDENT_BANDWIDTH | IDENT_SUB_NUMBER => {
127                if let Some(t) = &tag {
128                    check_tag(&t[TAG_START.len_utf8()..])?;
129                }
130                match name {
131                    IDENT_NUMBER => used.number = true,
132                    IDENT_TIME => used.time = true,
133                    IDENT_BANDWIDTH => used.bandwidth = true,
134                    _ => used.sub_number = true,
135                }
136            }
137            _ => return Err(TemplateFault::UnknownIdentifier),
138        }
139        rest = &tail[end + 1..];
140    }
141    Ok(used)
142}
143
144/// Validate a media/index template: exactly one of `$Number$`/`$Time$`.
145///
146/// # Errors
147/// [`TemplateFault`].
148pub fn scan_media(template: &str) -> Result<TemplateUse, TemplateFault> {
149    let used = scan(template)?;
150    if used.number && used.time {
151        return Err(TemplateFault::NumberAndTime);
152    }
153    if !used.number && !used.time {
154        return Err(TemplateFault::MissingNumberOrTime);
155    }
156    Ok(used)
157}
158
159/// Validate an initialization template: neither `$Number$` nor `$Time$`.
160///
161/// # Errors
162/// [`TemplateFault`].
163pub fn scan_initialization(template: &str) -> Result<TemplateUse, TemplateFault> {
164    let used = scan(template)?;
165    if used.number || used.time {
166        return Err(TemplateFault::DynamicIdentifierInInitialization);
167    }
168    Ok(used)
169}
170
171/// Substitute the identifiers of an already scanned template.
172#[must_use]
173pub fn substitute(
174    template: &str,
175    representation_id: &str,
176    number: Option<u64>,
177    time: Option<u64>,
178    bandwidth: u64,
179) -> String {
180    transmux::SegmentTemplate::resolve(template, representation_id, number, time, Some(bandwidth))
181}