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::{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            _ => unreachable!(),
118        }
119    }
120}
121
122impl DelegateCode<'static> {
123    fn load_versioned(
124        mut contract_data: Cursor<Vec<u8>>,
125    ) -> Result<(Self, APIVersion), std::io::Error> {
126        // Get contract version
127        let version = contract_data
128            .read_u64::<BigEndian>()
129            .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to read version"))?;
130        let version = APIVersion::from_u64(version).map_err(|e| {
131            std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Version error: {}", e))
132        })?;
133
134        if version == APIVersion::Version0_0_1 {
135            let mut code_hash = [0u8; 32];
136            contract_data.read_exact(&mut code_hash)?;
137        }
138
139        // Get contract code
140        let mut code_data: Vec<u8> = vec![];
141        contract_data
142            .read_to_end(&mut code_data)
143            .map_err(|_| std::io::ErrorKind::InvalidData)?;
144        Ok((DelegateCode::from(code_data), version))
145    }
146
147    /// Loads contract code which has been versioned from the fs.
148    pub fn load_versioned_from_path(path: &Path) -> Result<(Self, APIVersion), std::io::Error> {
149        let contract_data = Cursor::new(Self::load_bytes(path)?);
150        Self::load_versioned(contract_data)
151    }
152
153    /// Loads contract code which has been versioned from the fs.
154    pub fn load_versioned_from_bytes(
155        versioned_code: Vec<u8>,
156    ) -> Result<(Self, APIVersion), std::io::Error> {
157        let contract_data = Cursor::new(versioned_code);
158        Self::load_versioned(contract_data)
159    }
160}
161
162impl DelegateCode<'_> {
163    pub fn to_bytes_versioned(
164        &self,
165        version: APIVersion,
166    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
167        match version {
168            APIVersion::Version0_0_1 => {
169                let output_size =
170                    std::mem::size_of::<u64>() + self.data().len() + self.hash().0.len();
171                let mut output: Vec<u8> = Vec::with_capacity(output_size);
172                output.write_u64::<BigEndian>(APIVersion::Version0_0_1.into_u64())?;
173                output.extend(self.hash().0.iter());
174                output.extend(self.data());
175                Ok(output)
176            }
177        }
178    }
179}
180
181/// Contains the different versions available for WASM contracts.
182#[non_exhaustive]
183#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
184
185pub enum ContractWasmAPIVersion {
186    V1(WrappedContract),
187}
188
189impl From<ContractWasmAPIVersion> for ContractContainer {
190    fn from(value: ContractWasmAPIVersion) -> Self {
191        ContractContainer::Wasm(value)
192    }
193}
194
195impl Display for ContractWasmAPIVersion {
196    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
197        match self {
198            ContractWasmAPIVersion::V1(contract_v1) => {
199                write!(f, "[api=0.0.1]({contract_v1})")
200            }
201        }
202    }
203}
204
205/// Wrapper that allows contract versioning. This enum maintains the types of contracts that are
206/// allowed and their corresponding version.
207#[non_exhaustive]
208#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
209pub enum ContractContainer {
210    Wasm(ContractWasmAPIVersion),
211}
212
213impl From<ContractContainer> for APIVersion {
214    fn from(contract: ContractContainer) -> APIVersion {
215        match contract {
216            ContractContainer::Wasm(ContractWasmAPIVersion::V1(_)) => APIVersion::Version0_0_1,
217        }
218    }
219}
220
221impl ContractContainer {
222    /// Return the `ContractKey` from the specific contract version.
223    pub fn key(&self) -> ContractKey {
224        match self {
225            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => *contract_v1.key(),
226        }
227    }
228
229    /// Return the `ContractInstanceId` from the specific contract version.
230    pub fn id(&self) -> &ContractInstanceId {
231        match self {
232            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.key().id(),
233        }
234    }
235
236    /// Return the `Parameters` from the specific contract version.
237    pub fn params(&self) -> Parameters<'static> {
238        match self {
239            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.params().clone(),
240        }
241    }
242
243    /// Return the contract code from the specific contract version as `Vec<u8>`.
244    pub fn data(&self) -> &[u8] {
245        match self {
246            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1.data.data(),
247        }
248    }
249
250    pub fn unwrap_v1(self) -> WrappedContract {
251        match self {
252            Self::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => contract_v1,
253        }
254    }
255}
256
257impl Display for ContractContainer {
258    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
259        match self {
260            ContractContainer::Wasm(wasm_version) => {
261                write!(f, "WasmContainer({wasm_version})")
262            }
263        }
264    }
265}
266
267impl<'a> TryFrom<(&'a Path, Parameters<'static>)> for ContractContainer {
268    type Error = std::io::Error;
269
270    fn try_from((path, params): (&'a Path, Parameters<'static>)) -> Result<Self, Self::Error> {
271        let (contract_code, version) = ContractCode::load_versioned_from_path(path)?;
272
273        match version {
274            APIVersion::Version0_0_1 => Ok(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
275                WrappedContract::new(Arc::new(contract_code), params),
276            ))),
277        }
278    }
279}
280
281impl<'a, P> TryFrom<(Vec<u8>, P)> for ContractContainer
282where
283    P: std::ops::Deref<Target = Parameters<'a>>,
284{
285    type Error = std::io::Error;
286
287    fn try_from((versioned_contract_bytes, params): (Vec<u8>, P)) -> Result<Self, Self::Error> {
288        let params = params.deref().clone().into_owned();
289
290        let (contract_code, version) =
291            ContractCode::load_versioned_from_bytes(versioned_contract_bytes)?;
292
293        match version {
294            APIVersion::Version0_0_1 => Ok(ContractContainer::Wasm(ContractWasmAPIVersion::V1(
295                WrappedContract::new(Arc::new(contract_code), params),
296            ))),
297        }
298    }
299}
300
301impl ContractCode<'static> {
302    fn load_versioned(
303        mut contract_data: Cursor<Vec<u8>>,
304    ) -> Result<(Self, APIVersion), std::io::Error> {
305        // Get contract version
306        let version = contract_data.read_u64::<BigEndian>().map_err(|_| {
307            std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to read version")
308        })?;
309        let version = APIVersion::from_u64(version).map_err(|e| {
310            std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Version error: {}", e))
311        })?;
312
313        if version == APIVersion::Version0_0_1 {
314            let mut code_hash = [0u8; 32];
315            contract_data.read_exact(&mut code_hash)?;
316        }
317
318        // Get contract code
319        let mut code_data: Vec<u8> = vec![];
320        contract_data
321            .read_to_end(&mut code_data)
322            .map_err(|_| std::io::ErrorKind::InvalidData)?;
323        Ok((ContractCode::from(code_data), version))
324    }
325
326    /// Loads contract code which has been versioned from the fs.
327    pub fn load_versioned_from_path(path: &Path) -> Result<(Self, APIVersion), std::io::Error> {
328        let contract_data = Cursor::new(Self::load_bytes(path)?);
329        Self::load_versioned(contract_data)
330    }
331
332    /// Loads contract code which has been versioned from the fs.
333    pub fn load_versioned_from_bytes(
334        versioned_code: Vec<u8>,
335    ) -> Result<(Self, APIVersion), std::io::Error> {
336        let contract_data = Cursor::new(versioned_code);
337        Self::load_versioned(contract_data)
338    }
339}
340
341#[derive(Debug, Error)]
342pub enum VersionError {
343    #[error("unsupported incremental API version: {0}")]
344    UnsupportedVersion(u64),
345    #[error("failed to read version: {0}")]
346    IoError(#[from] std::io::Error),
347}
348
349#[derive(PartialEq, Eq, Clone, Copy)]
350pub enum APIVersion {
351    Version0_0_1,
352}
353
354impl APIVersion {
355    fn from_u64(version: u64) -> Result<Self, VersionError> {
356        match version {
357            0 => Ok(Self::Version0_0_1),
358            v => Err(VersionError::UnsupportedVersion(v)),
359        }
360    }
361
362    fn into_u64(self) -> u64 {
363        match self {
364            Self::Version0_0_1 => 0,
365        }
366    }
367}
368
369impl Display for APIVersion {
370    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
371        match self {
372            APIVersion::Version0_0_1 => write!(f, "0.0.1"),
373        }
374    }
375}
376
377impl<'a> TryFrom<&'a semver::Version> for APIVersion {
378    type Error = Box<dyn std::error::Error + Send + Sync>;
379    fn try_from(value: &'a semver::Version) -> Result<Self, Self::Error> {
380        match value {
381            ver if ver == &semver::Version::new(0, 0, 1) => Ok(APIVersion::Version0_0_1),
382            other => Err(format!("{other} version not supported").into()),
383        }
384    }
385}
386
387impl ContractCode<'_> {
388    pub fn to_bytes_versioned(
389        &self,
390        version: APIVersion,
391    ) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync + 'static>> {
392        match version {
393            APIVersion::Version0_0_1 => {
394                let output_size =
395                    std::mem::size_of::<u64>() + self.data().len() + self.hash().0.len();
396                let mut output: Vec<u8> = Vec::with_capacity(output_size);
397                output.write_u64::<BigEndian>(APIVersion::Version0_0_1.into_u64())?;
398                output.extend(self.hash().0.iter());
399                output.extend(self.data());
400                Ok(output)
401            }
402        }
403    }
404}
405
406impl<'a> TryFromFbs<&FbsContractContainer<'a>> for ContractContainer {
407    fn try_decode_fbs(value: &FbsContractContainer<'a>) -> Result<Self, WsApiError> {
408        match value.contract_type() {
409            ContractType::WasmContractV1 => {
410                let contract = value.contract_as_wasm_contract_v1().unwrap();
411                let data = Arc::new(ContractCode::from(contract.data().data().bytes().to_vec()));
412                let params = Parameters::from(contract.parameters().bytes().to_vec());
413                let key = ContractKey::from_params_and_code(&params, &*data);
414                Ok(ContractContainer::from(V1(WrappedContract {
415                    data,
416                    params,
417                    key,
418                })))
419            }
420            _ => unreachable!(),
421        }
422    }
423}