flute 1.11.1

File Delivery over Unidirectional Transport (FLUTE)
Documentation
use serde::Serialize;

use crate::tools::error::{FluteError, Result};

use super::fdtinstance::{CacheControlChoice, FdtInstance, File};

const XMLNS: &str = "urn:3GPP:metadata:2022:FLUTE:FDT";

#[derive(Serialize)]
pub(crate) struct FdtInstanceL6<'a> {
    #[serde(rename = "@xmlns")]
    xmlns: &'static str,
    #[serde(rename = "@Expires")]
    expires: &'a str,
    #[serde(rename = "@Complete", skip_serializing_if = "Option::is_none")]
    complete: Option<bool>,
    #[serde(rename = "@Content-Type", skip_serializing_if = "Option::is_none")]
    content_type: Option<&'a str>,
    #[serde(rename = "@Content-Encoding", skip_serializing_if = "Option::is_none")]
    content_encoding: Option<&'a str>,
    #[serde(
        rename = "@FEC-OTI-FEC-Encoding-ID",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_fec_encoding_id: Option<u8>,
    #[serde(
        rename = "@FEC-OTI-FEC-Instance-ID",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_fec_instance_id: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Maximum-Source-Block-Length",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_maximum_source_block_length: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Encoding-Symbol-Length",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_encoding_symbol_length: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Max-Number-of-Encoding-Symbols",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_max_number_of_encoding_symbols: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Scheme-Specific-Info",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_scheme_specific_info: Option<&'a str>,
    #[serde(rename = "File")]
    file: Vec<FileL6<'a>>,
    #[serde(rename = "schemaVersion")]
    schema_version: u32,
}

#[derive(Serialize)]
struct FileL6<'a> {
    #[serde(rename = "@Content-Location")]
    content_location: &'a str,
    #[serde(rename = "@TOI")]
    toi: &'a str,
    #[serde(rename = "@Content-Length", skip_serializing_if = "Option::is_none")]
    content_length: Option<u64>,
    #[serde(rename = "@Transfer-Length", skip_serializing_if = "Option::is_none")]
    transfer_length: Option<u64>,
    #[serde(rename = "@Content-Type", skip_serializing_if = "Option::is_none")]
    content_type: Option<&'a str>,
    #[serde(rename = "@Content-Encoding", skip_serializing_if = "Option::is_none")]
    content_encoding: Option<&'a str>,
    #[serde(rename = "@Content-MD5", skip_serializing_if = "Option::is_none")]
    content_md5: Option<&'a str>,
    #[serde(
        rename = "@FEC-OTI-FEC-Encoding-ID",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_fec_encoding_id: Option<u8>,
    #[serde(
        rename = "@FEC-OTI-FEC-Instance-ID",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_fec_instance_id: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Maximum-Source-Block-Length",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_maximum_source_block_length: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Encoding-Symbol-Length",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_encoding_symbol_length: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Max-Number-of-Encoding-Symbols",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_max_number_of_encoding_symbols: Option<u64>,
    #[serde(
        rename = "@FEC-OTI-Scheme-Specific-Info",
        skip_serializing_if = "Option::is_none"
    )]
    fec_oti_scheme_specific_info: Option<&'a str>,
    #[serde(
        rename = "@FEC-Redundancy-Level",
        skip_serializing_if = "Option::is_none"
    )]
    fec_redundancy_level: Option<u32>,
    #[serde(rename = "@File-ETag", skip_serializing_if = "Option::is_none")]
    file_etag: Option<&'a str>,
    #[serde(
        rename = "@X-Optel-Propagator",
        skip_serializing_if = "Option::is_none"
    )]
    optel_propagator: Option<&'a str>,
    #[serde(rename = "Cache-Control", skip_serializing_if = "Option::is_none")]
    cache_control: Option<CacheControlL6>,
}

#[derive(Serialize)]
struct CacheControlL6 {
    #[serde(rename = "$value")]
    value: CacheControlChoiceL6,
}

#[derive(Serialize)]
enum CacheControlChoiceL6 {
    #[serde(rename = "no-cache")]
    NoCache(bool),
    #[serde(rename = "max-stale")]
    MaxStale(bool),
    #[serde(rename = "Expires")]
    Expires(u32),
}

impl<'a> FdtInstanceL6<'a> {
    pub(crate) fn try_from_fdt(instance: &'a FdtInstance) -> Result<Self> {
        let schema_version = instance
            .schema_version
            .ok_or_else(|| FluteError::new("L.6 FDT requires schemaVersion"))?;
        let files = instance
            .file
            .as_ref()
            .filter(|files| !files.is_empty())
            .ok_or_else(|| FluteError::new("L.6 FDT requires at least one File"))?;

        let file = files
            .iter()
            .map(FileL6::try_from_file)
            .collect::<Result<Vec<_>>>()?;

        log_omitted("FullFDT", instance.full_fdt.is_some());
        log_omitted("Base-URL-1", instance.base_url_1.is_some());
        log_omitted("Base-URL-2", instance.base_url_2.is_some());
        log_omitted("delimiter", instance.delimiter.is_some());
        log_omitted("Group", instance.group.is_some());
        log_omitted(
            "MBMS-Session-Identity-Expiry",
            instance.mbms_session_identity_expiry.is_some(),
        );

        Ok(Self {
            xmlns: XMLNS,
            expires: &instance.expires,
            complete: instance.complete,
            content_type: instance.content_type.as_deref(),
            content_encoding: instance.content_encoding.as_deref(),
            fec_oti_fec_encoding_id: instance.fec_oti_fec_encoding_id,
            fec_oti_fec_instance_id: instance.fec_oti_fec_instance_id,
            fec_oti_maximum_source_block_length: instance.fec_oti_maximum_source_block_length,
            fec_oti_encoding_symbol_length: instance.fec_oti_encoding_symbol_length,
            fec_oti_max_number_of_encoding_symbols: instance.fec_oti_max_number_of_encoding_symbols,
            fec_oti_scheme_specific_info: instance.fec_oti_scheme_specific_info.as_deref(),
            file,
            schema_version,
        })
    }
}

impl<'a> FileL6<'a> {
    fn try_from_file(file: &'a File) -> Result<Self> {
        if !matches!(file.toi.parse::<u128>(), Ok(toi) if toi > 0) {
            return Err(FluteError::new("L.6 File TOI must be a positive integer"));
        }

        let fec_redundancy_level = file
            .fec_redundancy_level
            .as_deref()
            .map(str::parse::<u32>)
            .transpose()
            .map_err(|_| FluteError::new("L.6 FEC-Redundancy-Level must be an unsigned integer"))?;

        let cache_control = file
            .cache_control
            .as_ref()
            .map(|cache_control| {
                let value = match cache_control.value {
                    CacheControlChoice::NoCache(Some(false))
                    | CacheControlChoice::MaxStale(Some(false)) => {
                        return Err(FluteError::new(
                            "L.6 no-cache and max-stale values must be true",
                        ))
                    }
                    CacheControlChoice::NoCache(_) => CacheControlChoiceL6::NoCache(true),
                    CacheControlChoice::MaxStale(_) => CacheControlChoiceL6::MaxStale(true),
                    CacheControlChoice::Expires(value) => CacheControlChoiceL6::Expires(value),
                };
                Ok(CacheControlL6 { value })
            })
            .transpose()?;

        log_omitted("Decryption-KEY-URI", file.decryption_key_uri.is_some());
        log_omitted(
            "IndependentUnitPositions",
            file.independent_unit_positions.is_some(),
        );
        log_omitted("delimiter", file.delimiter.is_some());
        log_omitted(
            "Alternate-Content-Location-1",
            file.alternate_content_location_1.is_some(),
        );
        log_omitted(
            "Alternate-Content-Location-2",
            file.alternate_content_location_2.is_some(),
        );
        log_omitted("delimiter", file.delimiter2.is_some());
        log_omitted("Group", file.group.is_some());
        log_omitted(
            "MBMS-Session-Identity",
            file.mbms_session_identity.is_some(),
        );

        Ok(Self {
            content_location: &file.content_location,
            toi: &file.toi,
            content_length: file.content_length,
            transfer_length: file.transfer_length,
            content_type: file.content_type.as_deref(),
            content_encoding: file.content_encoding.as_deref(),
            content_md5: file.content_md5.as_deref(),
            fec_oti_fec_encoding_id: file.fec_oti_fec_encoding_id,
            fec_oti_fec_instance_id: file.fec_oti_fec_instance_id,
            fec_oti_maximum_source_block_length: file.fec_oti_maximum_source_block_length,
            fec_oti_encoding_symbol_length: file.fec_oti_encoding_symbol_length,
            fec_oti_max_number_of_encoding_symbols: file.fec_oti_max_number_of_encoding_symbols,
            fec_oti_scheme_specific_info: file.fec_oti_scheme_specific_info.as_deref(),
            fec_redundancy_level,
            file_etag: file.file_etag.as_deref(),
            optel_propagator: file.optel_propagator.as_deref(),
            cache_control,
        })
    }
}

fn log_omitted(field: &str, present: bool) {
    if present {
        log::error!("Omitting {} because it is not part of the L.6 FDT schema", field);
    }
}