Skip to main content

icydb_schema/
codec.rs

1//! Canonical, bounded IC Wire schema authoring artifacts.
2//!
3//! Framing and schema validation belong to IcyDB; serialization and allocation
4//! admission belong to IC Wire. These identities are a pre-1.0 hard cut: earlier
5//! authoring artifacts and their derived digests must not be reinterpreted.
6
7use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
8
9use crate::{
10    Blob, EntityFragment, EntityMigration, EntitySourceKey, FieldFragment, FieldSourceKey,
11    MAX_PROPOSAL_LITERAL_BYTES, MAX_SCHEMA_FRAGMENT_BYTES, MAX_SCHEMA_MIGRATION_PLAN_BYTES,
12    MAX_SCHEMA_PROPOSAL_BYTES, NamedTypeFragment, SchemaContractError, SchemaFragment,
13    SchemaMigrationPlan, SchemaProposal,
14};
15
16const FRAGMENT_HEADER: [u8; 5] = *b"ICWF\x01";
17const PROPOSAL_HEADER: [u8; 5] = *b"ICWP\x01";
18const MIGRATION_HEADER: [u8; 5] = *b"ICWM\x01";
19const SOURCE_MEANING_HEADER: [u8; 5] = *b"ICWS\x01";
20
21/// Encode one canonical schema fragment.
22///
23/// # Errors
24///
25/// Returns a typed validation, size or encoding error.
26pub fn encode_schema_fragment(fragment: &SchemaFragment) -> Result<Vec<u8>, SchemaContractError> {
27    fragment.validate()?;
28    encode(FRAGMENT_HEADER, fragment, MAX_SCHEMA_FRAGMENT_BYTES)
29}
30
31/// Decode and validate one complete canonical schema fragment.
32///
33/// # Errors
34///
35/// Returns a typed size, decoding, canonicalization or schema-contract error.
36pub fn decode_schema_fragment(bytes: &[u8]) -> Result<SchemaFragment, SchemaContractError> {
37    let fragment: SchemaFragment = decode(FRAGMENT_HEADER, bytes, MAX_SCHEMA_FRAGMENT_BYTES)?;
38    fragment.validate()?;
39    ensure_canonical(bytes, encode_schema_fragment(&fragment)?)?;
40    Ok(fragment)
41}
42
43/// Encode one canonical database-scoped schema proposal.
44///
45/// # Errors
46///
47/// Returns a typed validation, size or encoding error.
48pub fn encode_schema_proposal(proposal: &SchemaProposal) -> Result<Vec<u8>, SchemaContractError> {
49    proposal.validate_current()?;
50    encode(PROPOSAL_HEADER, proposal, MAX_SCHEMA_PROPOSAL_BYTES)
51}
52
53/// Decode and validate one complete canonical schema proposal.
54///
55/// # Errors
56///
57/// Returns a typed size, decoding, version, canonicalization or contract error.
58pub fn decode_schema_proposal(bytes: &[u8]) -> Result<SchemaProposal, SchemaContractError> {
59    let proposal: SchemaProposal = decode(PROPOSAL_HEADER, bytes, MAX_SCHEMA_PROPOSAL_BYTES)?;
60    proposal.validate_current()?;
61    ensure_canonical(bytes, encode_schema_proposal(&proposal)?)?;
62    Ok(proposal)
63}
64
65/// Encode one canonical coordinated migration plan.
66///
67/// # Errors
68///
69/// Returns a typed validation, size or encoding error.
70pub fn encode_schema_migration_plan(
71    plan: &SchemaMigrationPlan,
72) -> Result<Vec<u8>, SchemaContractError> {
73    plan.validate()?;
74    encode(MIGRATION_HEADER, plan, MAX_SCHEMA_MIGRATION_PLAN_BYTES)
75}
76
77/// Decode and validate one complete canonical migration plan.
78///
79/// # Errors
80///
81/// Returns a typed size, decoding, version, canonicalization or contract error.
82pub fn decode_schema_migration_plan(
83    bytes: &[u8],
84) -> Result<SchemaMigrationPlan, SchemaContractError> {
85    let plan: SchemaMigrationPlan =
86        decode(MIGRATION_HEADER, bytes, MAX_SCHEMA_MIGRATION_PLAN_BYTES)?;
87    plan.validate()?;
88    ensure_canonical(bytes, encode_schema_migration_plan(&plan)?)?;
89    Ok(plan)
90}
91
92pub(crate) fn encode_migration_transitions_for_digest(
93    transitions: &[EntityMigration],
94) -> Result<Vec<u8>, SchemaContractError> {
95    // Include the current artifact identity in digest material as well as the
96    // separate digest domain, so layouts cannot share an authority accidentally.
97    encode(
98        MIGRATION_HEADER,
99        &(
100            crate::migration::MIGRATION_PROGRAM_VERSION_CURRENT,
101            transitions,
102        ),
103        MAX_SCHEMA_MIGRATION_PLAN_BYTES,
104    )
105}
106
107pub(crate) fn encode_entity_source_meaning(
108    entity: &EntityFragment,
109    relation_targets: &[(EntitySourceKey, Vec<(FieldSourceKey, FieldFragment)>)],
110    reachable_types: &[NamedTypeFragment],
111) -> Result<Vec<u8>, SchemaContractError> {
112    encode(
113        SOURCE_MEANING_HEADER,
114        &(entity, relation_targets, reachable_types),
115        MAX_SCHEMA_PROPOSAL_BYTES,
116    )
117}
118
119fn encode<T: Serialize + ?Sized>(
120    header: [u8; 5],
121    value: &T,
122    max: usize,
123) -> Result<Vec<u8>, SchemaContractError> {
124    let framed = (header, value);
125    let len = ic_wire::native::encoded_len(&framed, crate::wire::sizing_limits())
126        .map_err(|_| SchemaContractError::Encode)?;
127    ensure_input_bound(len, max)?;
128    ic_wire::native::encode(&framed, crate::wire::limits(max))
129        .map_err(|_| SchemaContractError::Encode)
130}
131
132fn decode<T: DeserializeOwned>(
133    header: [u8; 5],
134    bytes: &[u8],
135    max: usize,
136) -> Result<T, SchemaContractError> {
137    ensure_input_bound(bytes.len(), max)?;
138    if !bytes.starts_with(&header) {
139        return Err(SchemaContractError::Decode);
140    }
141    let (_, value): ([u8; 5], T) = ic_wire::native::decode(bytes, crate::wire::limits(max))
142        .map_err(|_| SchemaContractError::Decode)?;
143    Ok(value)
144}
145
146const fn ensure_input_bound(len: usize, max: usize) -> Result<(), SchemaContractError> {
147    if len > max {
148        Err(SchemaContractError::EncodedTooLarge { len, max })
149    } else {
150        Ok(())
151    }
152}
153
154fn ensure_canonical(bytes: &[u8], canonical: Vec<u8>) -> Result<(), SchemaContractError> {
155    if canonical == bytes {
156        Ok(())
157    } else {
158        Err(SchemaContractError::NonCanonical)
159    }
160}
161
162// These adapters retain the existing domain Vec/String/Blob APIs. Wire owns
163// length admission and fallible allocation; IcyDB selects each field's bound.
164pub(crate) fn deserialize_list<'de, D, T, const N: usize>(
165    deserializer: D,
166) -> Result<Vec<T>, D::Error>
167where
168    D: Deserializer<'de>,
169    T: Deserialize<'de>,
170{
171    ic_wire::List::<T, N>::deserialize(deserializer).map(ic_wire::List::into_vec)
172}
173
174pub(crate) fn deserialize_literal_text<'de, D: Deserializer<'de>>(
175    deserializer: D,
176) -> Result<String, D::Error> {
177    if deserializer.is_human_readable() {
178        let value = String::deserialize(deserializer)?;
179        if value.len() > MAX_PROPOSAL_LITERAL_BYTES {
180            return Err(serde::de::Error::custom("proposal text limit"));
181        }
182        Ok(value)
183    } else {
184        ic_wire::Text::<MAX_PROPOSAL_LITERAL_BYTES>::deserialize(deserializer)
185            .map(ic_wire::Text::into_string)
186    }
187}
188
189pub(crate) fn deserialize_literal_blob<'de, D: Deserializer<'de>>(
190    deserializer: D,
191) -> Result<Blob, D::Error> {
192    if deserializer.is_human_readable() {
193        let value = Blob::deserialize(deserializer)?;
194        if value.len() > MAX_PROPOSAL_LITERAL_BYTES {
195            return Err(serde::de::Error::custom("proposal blob limit"));
196        }
197        Ok(value)
198    } else {
199        ic_wire::Bytes::<MAX_PROPOSAL_LITERAL_BYTES>::deserialize(deserializer)
200            .map(|bytes| Blob::from(bytes.into_vec()))
201    }
202}
203
204// Bound artifact key bytes through Wire, then reuse the key's existing Serde
205// constructor. Public keys retain their string hook for RPC/schema reflection.
206pub(crate) fn deserialize_key<'de, D, T, const N: usize>(d: D) -> Result<T, D::Error>
207where
208    D: Deserializer<'de>,
209    T: DeserializeOwned,
210{
211    if d.is_human_readable() {
212        return T::deserialize(d);
213    }
214    let text = ic_wire::Text::<N>::deserialize(d)?.into_string();
215    T::deserialize(serde::de::value::StringDeserializer::<D::Error>::new(text))
216}
217
218pub(crate) fn deserialize_optional_key<'de, D, T, const N: usize>(
219    d: D,
220) -> Result<Option<T>, D::Error>
221where
222    D: Deserializer<'de>,
223    T: DeserializeOwned,
224{
225    if d.is_human_readable() {
226        return Option::<T>::deserialize(d);
227    }
228    Option::<ic_wire::Text<N>>::deserialize(d)?
229        .map(|text| {
230            T::deserialize(serde::de::value::StringDeserializer::<D::Error>::new(
231                text.into_string(),
232            ))
233        })
234        .transpose()
235}
236
237pub(crate) fn deserialize_keys<'de, D, T, const N: usize>(d: D) -> Result<Vec<T>, D::Error>
238where
239    D: Deserializer<'de>,
240    T: DeserializeOwned,
241{
242    if d.is_human_readable() {
243        return deserialize_list::<D, T, N>(d);
244    }
245    let keys = ic_wire::List::<ic_wire::Text<{ crate::MAX_SOURCE_KEY_BYTES }>, N>::deserialize(d)?;
246    let mut values = Vec::new();
247    values
248        .try_reserve_exact(keys.as_slice().len())
249        .map_err(serde::de::Error::custom)?;
250    for key in keys.into_vec() {
251        values.push(T::deserialize(serde::de::value::StringDeserializer::<
252            D::Error,
253        >::new(key.into_string()))?);
254    }
255    Ok(values)
256}
257
258// Decimal's canonical text has a finite intrinsic bound. Do not let a generic
259// String visitor allocate an artifact-sized body before numeric admission.
260pub(crate) fn deserialize_literal_decimal<'de, D: Deserializer<'de>>(
261    deserializer: D,
262) -> Result<crate::Decimal, D::Error> {
263    const MAX_BYTES: usize = 1
264        + crate::decimal::DECIMAL_DIGIT_BUFFER_LEN
265        + 1
266        + crate::decimal::MAX_SUPPORTED_SCALE as usize;
267    let text = if deserializer.is_human_readable() {
268        let text = String::deserialize(deserializer)?;
269        if text.len() > MAX_BYTES {
270            return Err(serde::de::Error::custom("decimal text limit"));
271        }
272        text
273    } else {
274        ic_wire::Text::<MAX_BYTES>::deserialize(deserializer)?.into_string()
275    };
276    text.parse().map_err(serde::de::Error::custom)
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn artifacts_refuse_noncanonical_wire_and_truncation() {
285        let fragment = SchemaFragment::try_new(Vec::new(), Vec::new()).unwrap();
286        let canonical = encode_schema_fragment(&fragment).unwrap();
287        assert_eq!(&canonical[..5], b"ICWF\x01");
288        let mut alternate = canonical.clone();
289        alternate.splice(5..6, [0x80, 0]);
290        assert_eq!(
291            decode_schema_fragment(&alternate),
292            Err(SchemaContractError::Decode)
293        );
294        for end in 0..canonical.len() {
295            assert!(decode_schema_fragment(&canonical[..end]).is_err());
296        }
297    }
298
299    #[test]
300    fn artifact_decimal_admission_is_bounded_before_numeric_parsing() {
301        // ScalarLiteral's Decimal variant carries text, matching the shared
302        // atom's Serde form, but storage artifacts admit only its finite domain.
303        let wide = crate::wire::encode_one((4_u32, "0".repeat(1_024))).unwrap();
304        assert!(crate::wire::decode_one::<crate::ScalarLiteral>(&wide).is_err());
305        let invalid = crate::ScalarLiteral::Decimal(crate::Decimal::new_unchecked(
306            1,
307            crate::decimal::MAX_SUPPORTED_SCALE + 1,
308        ));
309        assert_eq!(invalid.validate(), Err(SchemaContractError::InvalidLiteral));
310    }
311
312    #[test]
313    fn artifact_relation_keys_bound_each_name_and_the_complete_list() {
314        for count in [crate::MAX_FRAGMENT_FIELDS, crate::MAX_FRAGMENT_FIELDS + 1] {
315            let bytes = crate::wire::encode_one((0_u32, vec!["id"; count])).unwrap();
316            assert_eq!(
317                crate::wire::decode_one::<crate::RelationSourceFragment>(&bytes).is_ok(),
318                count == crate::MAX_FRAGMENT_FIELDS
319            );
320        }
321        let bytes =
322            crate::wire::encode_one((0_u32, vec!["x".repeat(crate::MAX_SOURCE_KEY_BYTES + 1)]))
323                .unwrap();
324        assert!(crate::wire::decode_one::<crate::RelationSourceFragment>(&bytes).is_err());
325    }
326
327    #[test]
328    fn artifacts_admit_only_bounded_collections_and_valid_scalar_tags() {
329        let excessive = vec![(); crate::MAX_FRAGMENT_ENTITIES + 1];
330        let bytes =
331            crate::wire::encode_one((FRAGMENT_HEADER, (&excessive, Vec::<u8>::new()))).unwrap();
332        assert_eq!(
333            decode_schema_fragment(&bytes),
334            Err(SchemaContractError::Decode)
335        );
336        let invalid = crate::wire::encode_one(u32::MAX).unwrap();
337        assert!(
338            ic_wire::native::decode::<crate::ScalarLiteral>(&invalid, crate::wire::limits(64))
339                .is_err()
340        );
341        let wide = crate::wire::encode_one("x".repeat(crate::MAX_SOURCE_KEY_BYTES + 1)).unwrap();
342        assert!(crate::wire::decode_one::<crate::SchemaName>(&wide).is_err());
343    }
344}