Skip to main content

harmos_domain/model/storage/files/
codec.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::borrow::Cow;
18use std::num::NonZeroUsize;
19use std::path::PathBuf;
20// Third Party Imports
21
22// Local Imports
23use super::{FileMetadata, FileReference};
24
25use crate::model::storage::StorageCursor;
26use crate::model::storage::position::codec::{decode_bounded, encode_bounded};
27use crate::schema::declaration::{Field, Kind};
28use crate::schema::{Cardinality, Described};
29use crate::{Error, MessageError, Result};
30
31/* === Constants === */
32
33/* === Macros === */
34
35crate::model::storage::codec::product!(super::FileSession {
36    history: crate::model::storage::codec::Nested<crate::model::storage::StoreHistory> => 1,
37    id: crate::model::storage::codec::Id => 2,
38    generation: crate::model::storage::codec::Unsigned => 3,
39});
40
41crate::model::storage::codec::product!(super::StagedFile {
42    history: crate::model::storage::codec::Nested<crate::model::storage::StoreHistory> => 1,
43    id: crate::model::storage::codec::Id => 2,
44});
45crate::model::storage::codec::product!(super::FileProgress {
46    length: crate::model::storage::codec::Unsigned => 1,
47    phase: Phase => 2,
48});
49
50/* === Type Aliases === */
51
52/* === Structs === */
53
54// Wire Values: Zero Producing, One Finished, Two Abandoned.
55struct Phase;
56
57// Native Paths May Be Non-Unicode; Transport Decoding Requires Portable UTF-8.
58pub struct LocalPath;
59pub struct Digest;
60
61/* === Enums === */
62
63/* === Traits === */
64
65/* === Implementations === */
66
67impl crate::model::storage::codec::Value for Digest {
68    const NAME: &'static str = concat!(module_path!(), "::Digest");
69    type Native = [u8; 32];
70    const KIND: Kind = Kind::Bytes;
71    fn postcard_write(value: &[u8; 32], bytes: &mut Vec<u8>) {
72        bytes.push(32);
73        bytes.extend_from_slice(value);
74    }
75    fn postcard_size(_: &[u8; 32]) -> usize {
76        33
77    }
78    fn postcard_memory(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<usize, MessageError> {
79        Self::postcard_read(input)?;
80        Ok(0)
81    }
82    fn postcard_read(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<[u8; 32], MessageError> {
83        if input.value::<usize>()? != 32 {
84            return Err(MessageError::borrowed("SHA-256 requires exactly 32 bytes"));
85        }
86        input.take(32)?.try_into().map_err(|_| MessageError::borrowed("SHA-256 requires exactly 32 bytes"))
87    }
88}
89
90impl crate::model::storage::codec::Value for LocalPath {
91    const NAME: &'static str = concat!(module_path!(), "::LocalPath");
92    type Native = PathBuf;
93    const KIND: Kind = Kind::Bytes;
94
95    fn postcard_write(value: &PathBuf, bytes: &mut Vec<u8>) {
96        let path = value.as_os_str().as_encoded_bytes();
97        crate::__private::PostcardValue::write(&(path.len() as u64), bytes);
98        bytes.extend_from_slice(path);
99    }
100
101    fn postcard_size(value: &PathBuf) -> usize {
102        let length = value.as_os_str().as_encoded_bytes().len();
103        crate::__private::PostcardValue::size(&(length as u64)).saturating_add(length)
104    }
105
106    fn postcard_memory(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<usize, MessageError> {
107        <crate::model::storage::codec::Bytes as crate::model::storage::codec::Value>::postcard_memory(input)
108    }
109
110    fn postcard_read(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<PathBuf, MessageError> {
111        let bytes = <crate::model::storage::codec::Bytes as crate::model::storage::codec::Value>::postcard_read(input)?;
112        let path = String::from_utf8(bytes).map_err(|_| MessageError::borrowed("transported local paths must be UTF-8"))?;
113        Ok(PathBuf::from(path))
114    }
115}
116
117impl crate::model::storage::codec::Value for Phase {
118    const NAME: &'static str = concat!(module_path!(), "::", stringify!(Phase));
119    fn postcard_write(value: &Self::Native, bytes: &mut Vec<u8>) {
120        crate::__private::PostcardValue::write(
121            &match value {
122                super::FilePhase::Producing => 0u64,
123                super::FilePhase::Finished => 1u64,
124                super::FilePhase::Abandoned => 2u64,
125            },
126            bytes,
127        );
128    }
129
130    fn postcard_size(_: &Self::Native) -> usize {
131        1
132    }
133
134    fn postcard_memory(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<usize, MessageError> {
135        Self::postcard_read(input)?;
136        Ok(0)
137    }
138
139    fn postcard_read(input: &mut crate::__private::PostcardReader<'_>) -> std::result::Result<Self::Native, MessageError> {
140        match input.value::<u64>()? {
141            0 => Ok(super::FilePhase::Producing),
142            1 => Ok(super::FilePhase::Finished),
143            2 => Ok(super::FilePhase::Abandoned),
144            _ => Err(MessageError::borrowed("invalid storage selector")),
145        }
146    }
147
148    type Native = super::FilePhase;
149    const KIND: Kind = Kind::U64;
150}
151
152impl FileMetadata {
153    pub fn encode_bounded(&self, max_bytes: NonZeroUsize) -> Result<Vec<u8>> {
154        encode_bounded(self, max_bytes)
155    }
156
157    pub fn decode_bounded(bytes: &[u8], max_bytes: NonZeroUsize) -> Result<Self> {
158        decode_bounded(bytes, max_bytes)
159    }
160}
161
162impl FileReference {
163    pub fn encode_bounded(&self, max_bytes: NonZeroUsize) -> Result<Vec<u8>> {
164        self.check_native().map_err(|_| Error::InvalidInput {
165            field: "storage file codec",
166            reason: "invalid file reference",
167        })?;
168        encode_bounded(self, max_bytes)
169    }
170
171    pub fn decode_bounded(bytes: &[u8], max_bytes: NonZeroUsize) -> Result<Self> {
172        decode_bounded(bytes, max_bytes)
173    }
174
175    pub fn check_native(&self) -> std::result::Result<(), MessageError> {
176        if self.position.sequence == 0
177            || self.metadata.expected_length.is_some_and(|length| length != self.length)
178            || self.metadata.expected_sha256.is_some_and(|hash| hash != self.sha256)
179        {
180            return Err(MessageError::borrowed("file reference conflicts with its position or integrity requirements"));
181        }
182        Ok(())
183    }
184}
185
186crate::postcard_product!(FileMetadata {
187    id: crate::Identifier,
188    scope: Option<crate::Identifier>,
189    media_type: String,
190    expected_length: Option<u64>,
191    expected_sha256: Option<[u8; 32]>,
192});
193
194crate::postcard_product!(
195    FileReference {
196        position: StorageCursor,
197        metadata: FileMetadata,
198        length: u64,
199        sha256: [u8; 32]
200    },
201    validate = FileReference::check_native
202);
203
204impl Described for FileMetadata {
205    const NAME: &'static str = concat!(module_path!(), "::", stringify!(FileMetadata));
206    const FIELDS: &'static [Field] = &[
207        Field {
208            name: Cow::Borrowed("id"),
209            tag: 1,
210            kind: Kind::String,
211            cardinality: Cardinality::Required,
212        },
213        Field {
214            name: Cow::Borrowed("scope"),
215            tag: 2,
216            kind: Kind::String,
217            cardinality: Cardinality::Optional,
218        },
219        Field {
220            name: Cow::Borrowed("media_type"),
221            tag: 3,
222            kind: Kind::String,
223            cardinality: Cardinality::Required,
224        },
225        Field {
226            name: Cow::Borrowed("expected_length"),
227            tag: 4,
228            kind: Kind::U64,
229            cardinality: Cardinality::Optional,
230        },
231        Field {
232            name: Cow::Borrowed("expected_sha256"),
233            tag: 5,
234            kind: Kind::Bytes,
235            cardinality: Cardinality::Optional,
236        },
237    ];
238
239    const MARKER: &'static [u8] = &crate::schema::declaration::encode::<
240        { crate::schema::declaration::encoded_len(Self::NAME, Self::FIELDS) },
241    >(Self::NAME, Self::FIELDS);
242}
243
244impl Described for FileReference {
245    const NAME: &'static str = concat!(module_path!(), "::", stringify!(FileReference));
246    const FIELDS: &'static [Field] = &[
247        Field {
248            name: Cow::Borrowed("position"),
249            tag: 1,
250            kind: Kind::Message { definition: StorageCursor::DEFINITION },
251            cardinality: Cardinality::Required,
252        },
253        Field {
254            name: Cow::Borrowed("metadata"),
255            tag: 2,
256            kind: Kind::Message { definition: FileMetadata::DEFINITION },
257            cardinality: Cardinality::Required,
258        },
259        Field {
260            name: Cow::Borrowed("length"),
261            tag: 3,
262            kind: Kind::U64,
263            cardinality: Cardinality::Required,
264        },
265        Field {
266            name: Cow::Borrowed("sha256"),
267            tag: 4,
268            kind: Kind::Bytes,
269            cardinality: Cardinality::Required,
270        },
271    ];
272
273    const MARKER: &'static [u8] = &crate::schema::declaration::encode::<
274        { crate::schema::declaration::encoded_len(Self::NAME, Self::FIELDS) },
275    >(Self::NAME, Self::FIELDS);
276}
277
278/* === Public Functions === */
279
280/* === Private Functions === */
281
282/* === Tests === */