Skip to main content

lace_sans_io/records/
spec.rs

1//! Application-facing generic record construction.
2
3use std::fmt;
4
5use super::blob::append_blob;
6use super::plex::append_plex;
7use super::seal::finish_seal;
8use super::util::{
9    MAX_BLOB_SIZE, MAX_PLEX_FIELDS, RECORD_GLYPH_LINE_LEN, SEAL_OVERHEAD, blob_record_len,
10    is_reserved_application_field_name, validate_field_name_value,
11};
12use super::{BySecret, Record};
13
14/// Error returned by application-facing record construction.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum RecordCreateError {
17    /// The supplied intent does not map to a valid record kind.
18    Inference(String),
19    /// An application field used a reserved structural or synthetic name.
20    ReservedField(String),
21    /// An application field was malformed.
22    Field(String),
23    /// Data exceeded the Blob limit.
24    Data(String),
25    /// Marking failed.
26    Marking(String),
27}
28
29impl fmt::Display for RecordCreateError {
30    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::Inference(message) => write!(formatter, "record inference error: {message}"),
33            Self::ReservedField(name) => write!(formatter, "reserved field name: {name}"),
34            Self::Field(message) => write!(formatter, "field error: {message}"),
35            Self::Data(message) => write!(formatter, "data error: {message}"),
36            Self::Marking(message) => write!(formatter, "marking error: {message}"),
37        }
38    }
39}
40
41impl std::error::Error for RecordCreateError {}
42
43/// Values accepted by [`RecordSpec::data`] and [`RecordSpec::from_data`].
44pub trait IntoRecordData {
45    fn into_record_data(self) -> Vec<u8>;
46}
47
48impl IntoRecordData for Vec<u8> {
49    fn into_record_data(self) -> Vec<u8> {
50        self
51    }
52}
53impl IntoRecordData for &Vec<u8> {
54    fn into_record_data(self) -> Vec<u8> {
55        self.clone()
56    }
57}
58impl IntoRecordData for &[u8] {
59    fn into_record_data(self) -> Vec<u8> {
60        self.to_vec()
61    }
62}
63impl<const N: usize> IntoRecordData for [u8; N] {
64    fn into_record_data(self) -> Vec<u8> {
65        self.to_vec()
66    }
67}
68impl<const N: usize> IntoRecordData for &[u8; N] {
69    fn into_record_data(self) -> Vec<u8> {
70        self.to_vec()
71    }
72}
73impl IntoRecordData for String {
74    fn into_record_data(self) -> Vec<u8> {
75        self.into_bytes()
76    }
77}
78impl IntoRecordData for &String {
79    fn into_record_data(self) -> Vec<u8> {
80        self.as_bytes().to_vec()
81    }
82}
83impl IntoRecordData for &str {
84    fn into_record_data(self) -> Vec<u8> {
85        self.as_bytes().to_vec()
86    }
87}
88
89/// Application-facing record construction intent.
90#[derive(Clone, Default, PartialEq, Eq)]
91pub struct RecordSpec {
92    fields: Vec<(String, String)>,
93    data: Vec<u8>,
94    by_secret: Option<BySecret>,
95}
96
97impl fmt::Debug for RecordSpec {
98    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
99        formatter
100            .debug_struct("RecordSpec")
101            .field("fields", &self.fields)
102            .field("data_len", &self.data.len())
103            .field("by_secret", &self.by_secret.as_ref().map(|_| "<redacted>"))
104            .finish()
105    }
106}
107
108impl RecordSpec {
109    /// Start with an empty specification, which constructs an empty Blob.
110    pub fn new() -> Self {
111        Self::default()
112    }
113
114    /// Start with Blob data.
115    pub fn from_data(data: impl IntoRecordData) -> Self {
116        Self::new().data(data)
117    }
118
119    pub fn by_secret(mut self, by_secret: impl Into<BySecret>) -> Self {
120        self.by_secret = Some(by_secret.into());
121        self
122    }
123
124    pub fn field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
125        self.fields.push((name.into(), value.into()));
126        self
127    }
128
129    pub fn fields<I, N, V>(mut self, fields: I) -> Self
130    where
131        I: IntoIterator<Item = (N, V)>,
132        N: Into<String>,
133        V: Into<String>,
134    {
135        self.fields.extend(
136            fields
137                .into_iter()
138                .map(|(name, value)| (name.into(), value.into())),
139        );
140        self
141    }
142
143    pub fn data(mut self, data: impl IntoRecordData) -> Self {
144        self.data = data.into_record_data();
145        self
146    }
147}
148
149/// Construct and validate the one record kind implied by caller intent.
150pub fn new_record(spec: RecordSpec) -> Result<Record, RecordCreateError> {
151    new_record_impl(&spec.data, &spec.fields, spec.by_secret.as_ref(), None)
152}
153
154/// Exact-auxiliary construction for protocol hosts and canonical vectors.
155#[cfg(feature = "internal")]
156#[doc(hidden)]
157pub fn new_record_with_aux_rand32(
158    spec: RecordSpec,
159    aux_rand32: [u8; 32],
160) -> Result<Record, RecordCreateError> {
161    new_record_impl(
162        &spec.data,
163        &spec.fields,
164        spec.by_secret.as_ref(),
165        Some(aux_rand32),
166    )
167}
168
169pub(crate) fn new_record_impl(
170    data: &[u8],
171    fields: &[(String, String)],
172    by_secret: Option<&BySecret>,
173    aux_rand32: Option<[u8; 32]>,
174) -> Result<Record, RecordCreateError> {
175    CanonicalRecord::build(data, fields, by_secret, aux_rand32)
176        .map(|record| Record::from_constructed(record.0, record.1))
177}
178
179/// Construction proof: only this module can install bytes in this owner,
180/// together with their data offset. Field
181/// validation and bounded assembly establish the same invariant as full parsing,
182/// without accepting caller-supplied record bytes or retaining mutable aliases.
183struct CanonicalRecord(Vec<u8>, usize);
184
185impl CanonicalRecord {
186    fn build(
187        data: &[u8],
188        fields: &[(String, String)],
189        by_secret: Option<&BySecret>,
190        aux_rand32: Option<[u8; 32]>,
191    ) -> Result<Self, RecordCreateError> {
192        if data.len() > MAX_BLOB_SIZE {
193            return Err(RecordCreateError::Data(format!(
194                "Blob data exceeds max size: {} bytes",
195                data.len()
196            )));
197        }
198        if aux_rand32.is_some() && by_secret.is_none() {
199            return Err(RecordCreateError::Marking(
200                "exact auxiliary input requires a By-secret value".into(),
201            ));
202        }
203        if fields.is_empty() && by_secret.is_some() {
204            return Err(RecordCreateError::Inference(
205                "a By-secret requires at least one application field".into(),
206            ));
207        }
208        if fields.len() > MAX_PLEX_FIELDS {
209            return Err(RecordCreateError::Field(
210                "too many application fields".into(),
211            ));
212        }
213        let mut field_bytes = 0;
214        for (name, value) in fields {
215            if is_reserved_application_field_name(name) {
216                return Err(RecordCreateError::ReservedField(name.clone()));
217            }
218            validate_field_name_value(name, value).map_err(RecordCreateError::Field)?;
219            field_bytes += name.len() + 2 + value.len() + 1;
220        }
221
222        // The checked 32 MiB data and 512 x 129-byte fields bound all arithmetic.
223        // Reserve the complete form, then fill inner-to-outer glyphs in place.
224        let seal_bytes = if by_secret.is_some() {
225            SEAL_OVERHEAD
226        } else {
227            0
228        };
229        let plex_bytes = if fields.is_empty() {
230            0
231        } else {
232            RECORD_GLYPH_LINE_LEN + field_bytes
233        };
234        let mut bytes = Vec::with_capacity(seal_bytes + plex_bytes + blob_record_len(data.len()));
235        bytes.resize(seal_bytes, 0);
236        if fields.is_empty() {
237            append_blob(&mut bytes, data);
238        } else {
239            let plex_digest = append_plex(&mut bytes, fields, data);
240            if let Some(secret) = by_secret {
241                finish_seal(&mut bytes, secret, plex_digest, aux_rand32)
242                    .map_err(RecordCreateError::Marking)?;
243            }
244        }
245        let data_offset = bytes.len() - data.len();
246        Ok(Self(bytes, data_offset))
247    }
248}