Skip to main content

freenet_stdlib/
versioning.rs

1use std::fmt;
2use std::fmt::{Display, Formatter};
3use std::io::{Cursor, Read};
4use std::path::Path;
5use std::sync::Arc;
6
7use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use crate::{
12    client_api::{unknown_union_discriminant, TryFromFbs, WsApiError},
13    common_generated::common::{ContractContainer as FbsContractContainer, ContractType},
14    contract_interface::{ContractInstanceId, ContractKey},
15    generated::client_request::{DelegateContainer as FbsDelegateContainer, DelegateType},
16    parameters::Parameters,
17    prelude::{
18        CodeHash, ContractCode, ContractWasmAPIVersion::V1, Delegate, DelegateCode, DelegateKey,
19        WrappedContract,
20    },
21};
22
23/// Contains the different versions available for WASM delegates.
24#[non_exhaustive]
25#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
26pub enum DelegateWasmAPIVersion {
27    V1(#[serde(deserialize_with = "Delegate::deserialize_delegate")] Delegate<'static>),
28}
29
30impl DelegateWasmAPIVersion {}
31
32#[non_exhaustive]
33#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
34pub enum DelegateContainer {
35    Wasm(DelegateWasmAPIVersion),
36}
37
38impl From<DelegateContainer> for APIVersion {
39    fn from(delegate: DelegateContainer) -> APIVersion {
40        match delegate {
41            DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(_)) => APIVersion::Version0_0_1,
42        }
43    }
44}
45
46impl DelegateContainer {
47    pub fn key(&self) -> &DelegateKey {
48        match self {
49            Self::Wasm(DelegateWasmAPIVersion::V1(delegate_v1)) => delegate_v1.key(),
50        }
51    }
52
53    pub fn code(&self) -> &DelegateCode<'_> {
54        match self {
55            Self::Wasm(DelegateWasmAPIVersion::V1(delegate_v1)) => delegate_v1.code(),
56        }
57    }
58
59    pub fn code_hash(&self) -> &CodeHash {
60        match self {
61            Self::Wasm(DelegateWasmAPIVersion::V1(delegate_v1)) => delegate_v1.code_hash(),
62        }
63    }
64}
65
66impl<'a> TryFrom<(&'a Path, Parameters<'static>)> for DelegateContainer {
67    type Error = std::io::Error;
68
69    fn try_from((path, params): (&'a Path, Parameters<'static>)) -> Result<Self, Self::Error> {
70        let (contract_code, version) = DelegateCode::load_versioned_from_path(path)?;
71
72        match version {
73            APIVersion::Version0_0_1 => {
74                let delegate = Delegate::from((&contract_code, &params));
75                Ok(DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(
76                    delegate,
77                )))
78            }
79        }
80    }
81}
82
83impl<'a, P> TryFrom<(Vec<u8>, P)> for DelegateContainer
84where
85    P: std::ops::Deref<Target = Parameters<'a>>,
86{
87    type Error = std::io::Error;
88
89    fn try_from((versioned_contract_bytes, params): (Vec<u8>, P)) -> Result<Self, Self::Error> {
90        let params = params.deref().clone().into_owned();
91
92        let (contract_code, version) =
93            DelegateCode::load_versioned_from_bytes(versioned_contract_bytes)?;
94
95        match version {
96            APIVersion::Version0_0_1 => {
97                let delegate = Delegate::from((&contract_code, &params));
98                Ok(DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(
99                    delegate,
100                )))
101            }
102        }
103    }
104}
105
106impl<'a> TryFromFbs<&FbsDelegateContainer<'a>> for DelegateContainer {
107    fn try_decode_fbs(container: &FbsDelegateContainer<'a>) -> Result<Self, WsApiError> {
108        match container.delegate_type() {
109            DelegateType::WasmDelegateV1 => {
110                let delegate = container.delegate_as_wasm_delegate_v1().unwrap();
111                let data = DelegateCode::from(delegate.data().data().bytes().to_vec());
112                let params = Parameters::from(delegate.parameters().bytes().to_vec());
113                Ok(DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(
114                    Delegate::from((&data, &params)),
115                )))
116            }
117            // Reachable, not `unreachable!()`: the generated verifier for this
118            // union ends in `_ => Ok(())`, and the TypeScript SDK's
119            // `DelegateContainer` constructor takes `DelegateType.NONE` as its
120            // default argument, so NONE is a value first-party client code can
121            // realistically send. See `unknown_union_discriminant`.
122            other => Err(unknown_union_discriminant("DelegateType", other.0)),
123        }
124    }
125}
126
127impl DelegateCode<'static> {
128    fn load_versioned(
129        mut contract_data: Cursor<Vec<u8>>,
130    ) -> Result<(Self, APIVersion), std::io::Error> {
131        // Get contract version
132        let version = contract_data.read_u64::<BigEndian>().map_err(|_| {
133            std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to read version")
134        })?;
135        let version = APIVersion::from_u64(version).map_err(|e| {
136            std::io::Error::new(
137                std::io::ErrorKind::InvalidData,
138                format!("Version error: {}", e),
139            )
140        })?;
141
142        if version == APIVersion::Version0_0_1 {
143            let mut code_hash = [0u8; 32];
144            contract_data.read_exact(&mut code_hash)?;
145        }
146
147        // Get contract code
148        let mut code_data: Vec<u8> = vec![];
149        contract_data
150            .read_to_end(&mut code_data)
151            .map_err(|_| std::io::ErrorKind::InvalidData)?;
152        Ok((DelegateCode::from(code_data), version))
153    }
154
155    /// Loads contract code which has been versioned from the fs.
156    pub fn load_versioned_from_path(path: &Path) -> Result<(Self, APIVersion), std::io::Error> {
157        let contract_data = Cursor::new(Self::load_bytes(path)?);
158        Self::load_versioned(contract_data)
159    }
160
161    /// Loads contract code which has been versioned from the fs.
162    pub fn load_versioned_from_bytes(
163        versioned_code: Vec<u8>,
164    ) -> Result<(Self, APIVersion), std::io::Error> {
165        let contract_data = Cursor::new(versioned_code);
166        Self::load_versioned(contract_data)
167    }
168}
169
170impl DelegateCode<'_> {
171    pub fn to_bytes_versioned(
172        &self,
173        version: APIVersion,
174    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
175        match version {
176            APIVersion::Version0_0_1 => {
177                let output_size =
178                    std::mem::size_of::<u64>() + self.data().len() + self.hash().0.len();
179                let mut output: Vec<u8> = Vec::with_capacity(output_size);
180                output.write_u64::<BigEndian>(APIVersion::Version0_0_1.into_u64())?;
181                output.extend(self.hash().0.iter());
182                output.extend(self.data());
183                Ok(output)
184            }
185        }
186    }
187}
188
189/// Contains the different versions available for WASM contracts.
190#[non_exhaustive]
191#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
192
193pub enum ContractWasmAPIVersion {
194    V1(WrappedContract),
195}
196
197impl From<ContractWasmAPIVersion> for ContractContainer {
198    fn from(value: ContractWasmAPIVersion) -> Self {
199        ContractContainer::Wasm(value)
200    }
201}
202
203impl Display for ContractWasmAPIVersion {
204    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
205        match self {
206            ContractWasmAPIVersion::V1(contract_v1) => {
207                write!(f, "[api=0.0.1]({contract_v1})")
208            }
209        }
210    }
211}
212
213/// Wrapper that allows contract versioning. This enum maintains the types of contracts that are
214/// allowed and their corresponding version.
215#[non_exhaustive]
216#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
217pub enum ContractContainer {
218    Wasm(ContractWasmAPIVersion),
219}
220
221impl From<ContractContainer> for APIVersion {
222    fn from(contract: ContractContainer) -> APIVersion {
223        match contract {
224            ContractContainer::Wasm(ContractWasmAPIVersion::V1(_)) => APIVersion::Version0_0_1,
225        }
226    }
227}
228
229impl ContractContainer {
230    /// Return the `ContractKey` from the specific contract version.
231    pub fn key(&self) -> ContractKey {
232        match self {
233            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => *contract_v1.key(),
234        }
235    }
236
237    /// Return the `ContractInstanceId` from the specific contract version.
238    pub fn id(&self) -> &ContractInstanceId {
239        match self {
240            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.key().id(),
241        }
242    }
243
244    /// Return the `Parameters` from the specific contract version.
245    pub fn params(&self) -> Parameters<'static> {
246        match self {
247            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.params().clone(),
248        }
249    }
250
251    /// Return the contract code from the specific contract version as `Vec<u8>`.
252    pub fn data(&self) -> &[u8] {
253        match self {
254            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.data.data(),
255        }
256    }
257
258    pub fn unwrap_v1(self) -> WrappedContract {
259        match self {
260            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1,
261        }
262    }
263}
264
265impl Display for ContractContainer {
266    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
267        match self {
268            ContractContainer::Wasm(wasm_version) => {
269                write!(f, "WasmContainer({wasm_version})")
270            }
271        }
272    }
273}
274
275impl<'a> TryFrom<(&'a Path, Parameters<'static>)> for ContractContainer {
276    type Error = std::io::Error;
277
278    fn try_from((path, params): (&'a Path, Parameters<'static>)) -> Result<Self, Self::Error> {
279        let (contract_code, version) = ContractCode::load_versioned_from_path(path)?;
280
281        match version {
282            APIVersion::Version0_0_1 => Ok(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
283                WrappedContract::new(Arc::new(contract_code), params),
284            ))),
285        }
286    }
287}
288
289impl<'a, P> TryFrom<(Vec<u8>, P)> for ContractContainer
290where
291    P: std::ops::Deref<Target = Parameters<'a>>,
292{
293    type Error = std::io::Error;
294
295    fn try_from((versioned_contract_bytes, params): (Vec<u8>, P)) -> Result<Self, Self::Error> {
296        let params = params.deref().clone().into_owned();
297
298        let (contract_code, version) =
299            ContractCode::load_versioned_from_bytes(versioned_contract_bytes)?;
300
301        match version {
302            APIVersion::Version0_0_1 => Ok(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
303                WrappedContract::new(Arc::new(contract_code), params),
304            ))),
305        }
306    }
307}
308
309impl ContractCode<'static> {
310    fn load_versioned(
311        mut contract_data: Cursor<Vec<u8>>,
312    ) -> Result<(Self, APIVersion), std::io::Error> {
313        // Get contract version
314        let version = contract_data.read_u64::<BigEndian>().map_err(|_| {
315            std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to read version")
316        })?;
317        let version = APIVersion::from_u64(version).map_err(|e| {
318            std::io::Error::new(
319                std::io::ErrorKind::InvalidData,
320                format!("Version error: {}", e),
321            )
322        })?;
323
324        if version == APIVersion::Version0_0_1 {
325            let mut code_hash = [0u8; 32];
326            contract_data.read_exact(&mut code_hash)?;
327        }
328
329        // Get contract code
330        let mut code_data: Vec<u8> = vec![];
331        contract_data
332            .read_to_end(&mut code_data)
333            .map_err(|_| std::io::ErrorKind::InvalidData)?;
334        Ok((ContractCode::from(code_data), version))
335    }
336
337    /// Loads contract code which has been versioned from the fs.
338    pub fn load_versioned_from_path(path: &Path) -> Result<(Self, APIVersion), std::io::Error> {
339        let contract_data = Cursor::new(Self::load_bytes(path)?);
340        Self::load_versioned(contract_data)
341    }
342
343    /// Loads contract code which has been versioned from the fs.
344    pub fn load_versioned_from_bytes(
345        versioned_code: Vec<u8>,
346    ) -> Result<(Self, APIVersion), std::io::Error> {
347        let contract_data = Cursor::new(versioned_code);
348        Self::load_versioned(contract_data)
349    }
350}
351
352#[derive(Debug, Error)]
353pub enum VersionError {
354    #[error("unsupported incremental API version: {0}")]
355    UnsupportedVersion(u64),
356    #[error("failed to read version: {0}")]
357    IoError(#[from] std::io::Error),
358}
359
360/// Marked `#[non_exhaustive]` so future protocol versions can be added
361/// without a source-level break. Downstream `match` sites must include a
362/// wildcard arm. Historical note: an earlier release had `APIVersion::from_u64`
363/// panic on unknown versions; both the panic fix (0.1.8) and this attribute
364/// reduce the blast radius of version evolution.
365#[non_exhaustive]
366#[derive(PartialEq, Eq, Clone, Copy)]
367pub enum APIVersion {
368    Version0_0_1,
369}
370
371impl APIVersion {
372    fn from_u64(version: u64) -> Result<Self, VersionError> {
373        match version {
374            0 => Ok(Self::Version0_0_1),
375            v => Err(VersionError::UnsupportedVersion(v)),
376        }
377    }
378
379    fn into_u64(self) -> u64 {
380        match self {
381            Self::Version0_0_1 => 0,
382        }
383    }
384}
385
386impl Display for APIVersion {
387    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
388        match self {
389            APIVersion::Version0_0_1 => write!(f, "0.0.1"),
390        }
391    }
392}
393
394impl<'a> TryFrom<&'a semver::Version> for APIVersion {
395    type Error = Box<dyn std::error::Error + Send + Sync>;
396    fn try_from(value: &'a semver::Version) -> Result<Self, Self::Error> {
397        match value {
398            ver if ver == &semver::Version::new(0, 0, 1) => Ok(APIVersion::Version0_0_1),
399            other => Err(format!("{other} version not supported").into()),
400        }
401    }
402}
403
404impl ContractCode<'_> {
405    pub fn to_bytes_versioned(
406        &self,
407        version: APIVersion,
408    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
409        match version {
410            APIVersion::Version0_0_1 => {
411                let output_size =
412                    std::mem::size_of::<u64>() + self.data().len() + self.hash().0.len();
413                let mut output: Vec<u8> = Vec::with_capacity(output_size);
414                output.write_u64::<BigEndian>(APIVersion::Version0_0_1.into_u64())?;
415                output.extend(self.hash().0.iter());
416                output.extend(self.data());
417                Ok(output)
418            }
419        }
420    }
421}
422
423impl<'a> TryFromFbs<&FbsContractContainer<'a>> for ContractContainer {
424    fn try_decode_fbs(value: &FbsContractContainer<'a>) -> Result<Self, WsApiError> {
425        match value.contract_type() {
426            ContractType::WasmContractV1 => {
427                let contract = value.contract_as_wasm_contract_v1().unwrap();
428                let data = Arc::new(ContractCode::from(contract.data().data().bytes().to_vec()));
429                let params = Parameters::from(contract.parameters().bytes().to_vec());
430                let key = ContractKey::from_params_and_code(&params, &*data);
431                Ok(ContractContainer::from(V1(WrappedContract {
432                    data,
433                    params,
434                    key,
435                })))
436            }
437            // Reachable, not `unreachable!()`: the generated verifier for this
438            // union ends in `_ => Ok(())`, and the TypeScript SDK's
439            // `ContractContainer` constructor takes `ContractType.NONE` as its
440            // default argument, so NONE is a value first-party client code can
441            // realistically send on a PUT. See `unknown_union_discriminant`.
442            other => Err(unknown_union_discriminant("ContractType", other.0)),
443        }
444    }
445}