Skip to main content

openrtc_file_transfer/
capabilities.rs

1//! Negotiated protocol requirements. Application stores and authorization remain
2//! with the consumer; advertised capabilities must reflect its real receiver.
3use crate::{FileTransferHeader, TransferError};
4use serde::{Deserialize, Serialize};
5
6pub const REQUIRED_CAPABILITIES_METADATA_KEY: &str = "openrtc.required-capabilities.v1";
7pub const MAX_CAPABILITY_BYTES: usize = 4096;
8pub const MAX_SAFE_FILE_BYTES: u64 = 9_007_199_254_740_991;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct RequiredTransferCapabilities {
13    pub version: u8,
14    pub durable_resume: bool,
15    pub completion_receipt: bool,
16    pub extensions: Vec<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20#[serde(rename_all = "camelCase", deny_unknown_fields)]
21pub struct TransferCapabilities {
22    pub version: u8,
23    pub durable_resume: bool,
24    pub completion_receipt: bool,
25    pub extensions: Vec<String>,
26    pub receive_available: bool,
27    pub max_file_bytes: u64,
28}
29
30fn normalize_extensions(extensions: &mut [String]) -> Result<(), TransferError> {
31    if extensions.len() > 16
32        || extensions.iter().any(|extension| {
33            extension.len() > 128
34                || extension
35                    .split(['.', '_', '-'])
36                    .enumerate()
37                    .any(|(index, part)| {
38                        part.is_empty()
39                            || !part
40                                .bytes()
41                                .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
42                            || index == 0 && !part.as_bytes()[0].is_ascii_lowercase()
43                    })
44        })
45    {
46        return Err(TransferError::Protocol(
47            "invalid transfer capability extensions".into(),
48        ));
49    }
50    extensions.sort();
51    if extensions.windows(2).any(|pair| pair[0] == pair[1]) {
52        return Err(TransferError::Protocol(
53            "duplicate transfer capability extension".into(),
54        ));
55    }
56    Ok(())
57}
58impl RequiredTransferCapabilities {
59    pub fn normalized(mut self) -> Result<Self, TransferError> {
60        if self.version != 1 {
61            return Err(TransferError::Protocol(
62                "invalid transfer capability version".into(),
63            ));
64        }
65        normalize_extensions(&mut self.extensions)?;
66        Ok(self)
67    }
68}
69impl TransferCapabilities {
70    pub fn normalized(mut self) -> Result<Self, TransferError> {
71        if self.version != 1 || self.max_file_bytes > MAX_SAFE_FILE_BYTES {
72            return Err(TransferError::Protocol(
73                "invalid transfer capability version or file limit".into(),
74            ));
75        }
76        normalize_extensions(&mut self.extensions)?;
77        Ok(self)
78    }
79
80    /// Call before accepting a new body; durable receipts can be reconciled
81    /// independently of the receiver's current body availability and limits.
82    pub fn require(&self, header: &FileTransferHeader) -> Result<(), TransferError> {
83        let caps = self.clone().normalized()?;
84        let required = header.required_capabilities()?;
85        if !caps.receive_available
86            || header.size > caps.max_file_bytes
87            || required.is_some_and(|r| {
88                r.durable_resume && !caps.durable_resume
89                    || r.completion_receipt && !caps.completion_receipt
90                    || r.extensions
91                        .iter()
92                        .any(|extension| !caps.extensions.contains(extension))
93            })
94        {
95            return Err(TransferError::Protocol("transfer_capability_required: receiver cannot admit the selected size or required transfer features".into()));
96        }
97        Ok(())
98    }
99}
100impl FileTransferHeader {
101    pub fn required_capabilities(
102        &self,
103    ) -> Result<Option<RequiredTransferCapabilities>, TransferError> {
104        let Some(encoded) = self.metadata.get(REQUIRED_CAPABILITIES_METADATA_KEY) else {
105            return Ok(None);
106        };
107        if encoded.len() > MAX_CAPABILITY_BYTES {
108            return Err(TransferError::Limit(
109                "required transfer capability metadata is too large".into(),
110            ));
111        }
112        let required: RequiredTransferCapabilities = serde_json::from_str(encoded)?;
113        Ok(Some(required.normalized()?))
114    }
115
116    pub fn with_required_capabilities(
117        mut self,
118        required: RequiredTransferCapabilities,
119    ) -> Result<Self, TransferError> {
120        let encoded = serde_json::to_string(&required.normalized()?)?;
121        if self
122            .metadata
123            .get(REQUIRED_CAPABILITIES_METADATA_KEY)
124            .is_some_and(|old| old != &encoded)
125        {
126            return Err(TransferError::Protocol("transfer_identity_conflict: required transfer capabilities differ from the accepted header".into()));
127        }
128        self.metadata
129            .insert(REQUIRED_CAPABILITIES_METADATA_KEY.into(), encoded);
130        Ok(self)
131    }
132}