Skip to main content

icydb_schema/
codec.rs

1//! Bounded current-form schema authoring transport.
2//!
3//! These bytes are internal build/application artifacts, not Candid endpoint
4//! payloads. Each top-level form has one tagged V1 representation. Pre-1.0
5//! obsolete representations are rejected rather than translated.
6
7mod fragment;
8mod migration;
9mod proposal;
10mod value;
11mod wire;
12
13use crate::{
14    EntityFragment, EntitySourceKey, FieldFragment, FieldSourceKey, MAX_SCHEMA_FRAGMENT_BYTES,
15    MAX_SCHEMA_MIGRATION_PLAN_BYTES, MAX_SCHEMA_PROPOSAL_BYTES, NamedTypeFragment,
16    SchemaContractError, SchemaFragment, SchemaMigrationPlan, SchemaProposal,
17};
18
19use self::{
20    fragment::{
21        decode_fragment_payload, encode_entity, encode_field, encode_fragment_payload,
22        encode_named_type,
23    },
24    migration::{decode_migration_payload, encode_migration_payload},
25    proposal::{decode_proposal_payload, encode_proposal_payload},
26    wire::{WireReader, WireWriter},
27};
28
29const FRAGMENT_HEADER: &[u8; 5] = b"ICYF\x01";
30const PROPOSAL_HEADER: &[u8; 5] = b"ICYP\x01";
31const MIGRATION_HEADER: &[u8; 5] = b"ICYM\x01";
32const SOURCE_MEANING_HEADER: &[u8; 5] = b"ICYS\x01";
33
34/// Encode one canonical schema fragment.
35///
36/// # Errors
37///
38/// Returns a typed contract or size error when the fragment is malformed or
39/// exceeds the frozen transport bound.
40pub fn encode_schema_fragment(fragment: &SchemaFragment) -> Result<Vec<u8>, SchemaContractError> {
41    fragment.validate()?;
42    let mut writer = WireWriter::new(MAX_SCHEMA_FRAGMENT_BYTES);
43    writer.push_raw(FRAGMENT_HEADER)?;
44    encode_fragment_payload(&mut writer, fragment)?;
45    Ok(writer.finish())
46}
47
48/// Decode one bounded current-form schema fragment.
49///
50/// # Errors
51///
52/// Returns a typed size, decoding, canonicalization, or nested-contract error.
53pub fn decode_schema_fragment(bytes: &[u8]) -> Result<SchemaFragment, SchemaContractError> {
54    ensure_input_bound(bytes, MAX_SCHEMA_FRAGMENT_BYTES)?;
55    let mut reader = WireReader::new(bytes);
56    reader.expect_raw(FRAGMENT_HEADER)?;
57    let fragment = decode_fragment_payload(&mut reader)?;
58    reader.finish()?;
59    fragment.validate()?;
60    ensure_canonical(bytes, encode_schema_fragment(&fragment)?)?;
61    Ok(fragment)
62}
63
64/// Encode one canonical database-scoped schema proposal.
65///
66/// # Errors
67///
68/// Returns a typed contract or size error when the proposal is malformed or
69/// exceeds the frozen transport bound.
70pub fn encode_schema_proposal(proposal: &SchemaProposal) -> Result<Vec<u8>, SchemaContractError> {
71    proposal.validate_current()?;
72    let mut writer = WireWriter::new(MAX_SCHEMA_PROPOSAL_BYTES);
73    writer.push_raw(PROPOSAL_HEADER)?;
74    encode_proposal_payload(&mut writer, proposal)?;
75    Ok(writer.finish())
76}
77
78/// Decode one bounded current-form database-scoped proposal.
79///
80/// # Errors
81///
82/// Returns a typed size, decoding, version, canonicalization, or nested
83/// contract error. Obsolete forms are never translated.
84pub fn decode_schema_proposal(bytes: &[u8]) -> Result<SchemaProposal, SchemaContractError> {
85    ensure_input_bound(bytes, MAX_SCHEMA_PROPOSAL_BYTES)?;
86    let mut reader = WireReader::new(bytes);
87    reader.expect_raw(PROPOSAL_HEADER)?;
88    let proposal = decode_proposal_payload(&mut reader)?;
89    reader.finish()?;
90    proposal.validate_current()?;
91    ensure_canonical(bytes, encode_schema_proposal(&proposal)?)?;
92    Ok(proposal)
93}
94
95/// Encode one bounded canonical migration plan.
96///
97/// # Errors
98///
99/// Returns a typed validation, encoding, or size error.
100pub fn encode_schema_migration_plan(
101    plan: &SchemaMigrationPlan,
102) -> Result<Vec<u8>, SchemaContractError> {
103    plan.validate()?;
104    let mut writer = WireWriter::new(MAX_SCHEMA_MIGRATION_PLAN_BYTES);
105    writer.push_raw(MIGRATION_HEADER)?;
106    encode_migration_payload(&mut writer, plan)?;
107    Ok(writer.finish())
108}
109
110/// Decode one bounded canonical migration plan.
111///
112/// # Errors
113///
114/// Returns a typed size, decoding, version, validation, or canonicalization
115/// error. Obsolete forms are never translated.
116pub fn decode_schema_migration_plan(
117    bytes: &[u8],
118) -> Result<SchemaMigrationPlan, SchemaContractError> {
119    ensure_input_bound(bytes, MAX_SCHEMA_MIGRATION_PLAN_BYTES)?;
120    let mut reader = WireReader::new(bytes);
121    reader.expect_raw(MIGRATION_HEADER)?;
122    let plan = decode_migration_payload(&mut reader)?;
123    reader.finish()?;
124    plan.validate()?;
125    ensure_canonical(bytes, encode_schema_migration_plan(&plan)?)?;
126    Ok(plan)
127}
128
129pub(crate) use migration::encode_migration_transitions_for_digest;
130
131pub(crate) fn encode_entity_source_meaning(
132    entity: &EntityFragment,
133    relation_targets: &[(EntitySourceKey, Vec<(FieldSourceKey, FieldFragment)>)],
134    reachable_types: &[NamedTypeFragment],
135) -> Result<Vec<u8>, SchemaContractError> {
136    let mut writer = WireWriter::new(MAX_SCHEMA_PROPOSAL_BYTES);
137    writer.push_raw(SOURCE_MEANING_HEADER)?;
138    encode_entity(&mut writer, entity)?;
139    writer.push_len(relation_targets.len())?;
140    for (entity, fields) in relation_targets {
141        value::encode_source_key(&mut writer, entity.as_str())?;
142        writer.push_len(fields.len())?;
143        for (_, field) in fields {
144            encode_field(&mut writer, field)?;
145        }
146    }
147    writer.push_len(reachable_types.len())?;
148    for definition in reachable_types {
149        encode_named_type(&mut writer, definition)?;
150    }
151    Ok(writer.finish())
152}
153
154const fn ensure_input_bound(bytes: &[u8], max: usize) -> Result<(), SchemaContractError> {
155    if bytes.len() > max {
156        return Err(SchemaContractError::EncodedTooLarge {
157            len: bytes.len(),
158            max,
159        });
160    }
161    Ok(())
162}
163
164fn ensure_canonical(bytes: &[u8], canonical: Vec<u8>) -> Result<(), SchemaContractError> {
165    if canonical == bytes {
166        Ok(())
167    } else {
168        Err(SchemaContractError::NonCanonical)
169    }
170}