homestar_invocation/task/instruction/
nonce.rs

1//! [Instruction] nonce parameter.
2//!
3//! [Instruction]: super::Instruction
4
5use crate::{ipld::schema, Error, Unit};
6use const_format::formatcp;
7use enum_as_inner::EnumAsInner;
8use generic_array::{
9    typenum::consts::{U12, U16},
10    GenericArray,
11};
12use libipld::{multibase::Base::Base32HexLower, Ipld};
13use schemars::{
14    gen::SchemaGenerator,
15    schema::{InstanceType, Metadata, Schema, SchemaObject, SingleOrVec, StringValidation},
16    JsonSchema,
17};
18use serde::{Deserialize, Serialize};
19use serde_json::json;
20use std::{borrow::Cow, fmt, module_path};
21use uuid::Uuid;
22
23type Nonce96 = GenericArray<u8, U12>;
24type Nonce128 = GenericArray<u8, U16>;
25
26/// Incoming type for nonce conversion.
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub enum IncomingTyp {
29    /// Nonce incoming as a string.
30    String,
31    /// Nonce incoming as bytes.
32    Bytes,
33}
34
35/// Enumeration over allowed `nonce` types.
36#[derive(Clone, Debug, PartialEq, EnumAsInner, Serialize, Deserialize)]
37pub enum Nonce {
38    /// 96-bit, 12-byte nonce, e.g. [xid].
39    Nonce96(Nonce96, IncomingTyp),
40    /// 128-bit, 16-byte nonce.
41    Nonce128(Nonce128, IncomingTyp),
42    /// No Nonce attributed.
43    Empty,
44}
45
46impl Nonce {
47    /// Default generator, outputting a [xid] nonce, which is a 96-bit, 12-byte
48    /// nonce.
49    pub fn generate() -> Self {
50        Nonce::Nonce96(
51            *GenericArray::from_slice(xid::new().as_bytes()),
52            IncomingTyp::Bytes,
53        )
54    }
55
56    /// Generate a default, 128-bit, 16-byte nonce via [Uuid::new_v4()].
57    pub fn generate_128() -> Self {
58        Nonce::Nonce128(
59            *GenericArray::from_slice(Uuid::new_v4().as_bytes()),
60            IncomingTyp::Bytes,
61        )
62    }
63
64    /// Convert the nonce to a byte vector.
65    pub fn to_vec(&self) -> Vec<u8> {
66        match self {
67            Nonce::Nonce96(nonce, _) => nonce.to_vec(),
68            Nonce::Nonce128(nonce, _) => nonce.to_vec(),
69            Nonce::Empty => vec![],
70        }
71    }
72}
73
74impl fmt::Display for Nonce {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        match self {
77            Nonce::Nonce96(nonce, _) => {
78                write!(f, "{}", Base32HexLower.encode(nonce.as_slice()))
79            }
80            Nonce::Nonce128(nonce, _) => {
81                write!(f, "{}", Base32HexLower.encode(nonce.as_slice()))
82            }
83            Nonce::Empty => write!(f, ""),
84        }
85    }
86}
87
88impl From<Nonce> for Ipld {
89    fn from(nonce: Nonce) -> Self {
90        match nonce {
91            Nonce::Nonce96(nonce, typ) => {
92                if let IncomingTyp::Bytes = typ {
93                    Ipld::Bytes(nonce.to_vec())
94                } else {
95                    Ipld::String(Base32HexLower.encode(nonce.as_slice()))
96                }
97            }
98            Nonce::Nonce128(nonce, typ) => {
99                if let IncomingTyp::Bytes = typ {
100                    Ipld::Bytes(nonce.to_vec())
101                } else {
102                    Ipld::String(Base32HexLower.encode(nonce.as_slice()))
103                }
104            }
105            Nonce::Empty => Ipld::String("".to_string()),
106        }
107    }
108}
109
110impl TryFrom<Ipld> for Nonce {
111    type Error = Error<Unit>;
112
113    fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
114        match ipld {
115            Ipld::String(s) if s.is_empty() => Ok(Nonce::Empty),
116            Ipld::String(s) => {
117                let bytes = Base32HexLower.decode(s)?;
118                match bytes.len() {
119                    12 => Ok(Nonce::Nonce96(
120                        *GenericArray::from_slice(&bytes),
121                        IncomingTyp::String,
122                    )),
123                    16 => Ok(Nonce::Nonce128(
124                        *GenericArray::from_slice(&bytes),
125                        IncomingTyp::String,
126                    )),
127                    other => Err(Error::unexpected_ipld(other.to_owned().into())),
128                }
129            }
130            Ipld::Bytes(v) => match v.len() {
131                12 => Ok(Nonce::Nonce96(
132                    *GenericArray::from_slice(&v),
133                    IncomingTyp::Bytes,
134                )),
135                16 => Ok(Nonce::Nonce128(
136                    *GenericArray::from_slice(&v),
137                    IncomingTyp::Bytes,
138                )),
139                other_ipld => {
140                    println!("other_ipld: {:?}", v.len());
141                    Err(Error::unexpected_ipld(other_ipld.to_owned().into()))
142                }
143            },
144            _ => Ok(Nonce::Empty),
145        }
146    }
147}
148
149impl TryFrom<&Ipld> for Nonce {
150    type Error = Error<Unit>;
151
152    fn try_from(ipld: &Ipld) -> Result<Self, Self::Error> {
153        TryFrom::try_from(ipld.to_owned())
154    }
155}
156
157impl JsonSchema for Nonce {
158    fn schema_name() -> String {
159        "nonce".to_owned()
160    }
161
162    fn schema_id() -> Cow<'static, str> {
163        Cow::Borrowed(formatcp!("{}::Nonce", module_path!()))
164    }
165
166    fn json_schema(gen: &mut SchemaGenerator) -> Schema {
167        let mut schema = SchemaObject {
168            instance_type: None,
169            metadata: Some(Box::new(Metadata {
170                description: Some(
171                    "A 12-byte or 16-byte nonce encoded as IPLD bytes. Use empty string for no nonce.".to_string(),
172                ),
173                ..Default::default()
174            })),
175            ..Default::default()
176        };
177
178        let empty_string = SchemaObject {
179            instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
180            const_value: Some(json!("")),
181            ..Default::default()
182        };
183
184        let non_empty_string = SchemaObject {
185            instance_type: Some(SingleOrVec::Single(InstanceType::String.into())),
186            metadata: Some(Box::new(Metadata {
187                description: Some("A 12-byte or 16-byte nonce encoded as a string, which expects to be decoded with Base32hex lower".to_string()),
188                ..Default::default()
189            })),
190            string: Some(Box::new(StringValidation {
191                min_length: Some(1),
192                ..Default::default()
193            })),
194            ..Default::default()
195        };
196
197        schema.subschemas().one_of = Some(vec![
198            gen.subschema_for::<schema::IpldBytesStub>(),
199            Schema::Object(empty_string),
200            Schema::Object(non_empty_string),
201        ]);
202
203        schema.into()
204    }
205}
206
207#[cfg(test)]
208mod test {
209    use super::*;
210    use libipld::{json::DagJsonCodec, multibase::Base, prelude::Codec};
211
212    #[test]
213    fn ipld_roundtrip_12() {
214        let gen = Nonce::generate();
215        let ipld = Ipld::from(gen.clone());
216
217        let inner = if let Nonce::Nonce96(nonce, _) = gen {
218            Ipld::Bytes(nonce.to_vec())
219        } else {
220            panic!("No conversion!")
221        };
222
223        assert_eq!(ipld, inner);
224        assert_eq!(gen, ipld.try_into().unwrap());
225    }
226
227    #[test]
228    fn ipld_roundtrip_16() {
229        let gen = Nonce::generate_128();
230        let ipld = Ipld::from(gen.clone());
231
232        let inner = if let Nonce::Nonce128(nonce, _) = gen {
233            Ipld::Bytes(nonce.to_vec())
234        } else {
235            panic!("No conversion!")
236        };
237
238        assert_eq!(ipld, inner);
239        assert_eq!(gen, ipld.try_into().unwrap());
240    }
241
242    #[test]
243    fn ser_de() {
244        let gen = Nonce::generate_128();
245        let ser = serde_json::to_string(&gen).unwrap();
246        let de = serde_json::from_str(&ser).unwrap();
247
248        assert_eq!(gen, de);
249    }
250
251    #[test]
252    fn json_nonce_roundtrip() {
253        let b = b"LSS3Ftv+Gtq9965M";
254        let bytes = Base::Base64.encode(b);
255        let json = json!({
256            "/": {"bytes": format!("{}", bytes)}
257        });
258
259        let ipld: Ipld = DagJsonCodec.decode(json.to_string().as_bytes()).unwrap();
260        let nonce: Nonce = Nonce::try_from(ipld.clone()).unwrap();
261
262        let Ipld::Bytes(bytes) = ipld.clone() else {
263            panic!("IPLD is not bytes");
264        };
265
266        assert_eq!(bytes, b);
267        assert_eq!(ipld, Ipld::Bytes(b.to_vec()));
268        assert_eq!(
269            nonce,
270            Nonce::Nonce128(*GenericArray::from_slice(b), IncomingTyp::Bytes)
271        );
272        assert_eq!(nonce, Nonce::try_from(ipld.clone()).unwrap());
273
274        let nonce: Nonce = ipld.clone().try_into().unwrap();
275        let ipld = Ipld::from(nonce.clone());
276        assert_eq!(ipld, Ipld::Bytes(b.to_vec()));
277    }
278
279    #[test]
280    fn nonce_as_string_roundtrip() {
281        let nonce = Nonce::generate();
282        let string = nonce.to_string();
283        let from_string = Nonce::try_from(Ipld::String(string.clone())).unwrap();
284
285        assert_eq!(nonce.to_vec(), from_string.to_vec());
286        assert_eq!(string, nonce.to_string());
287    }
288
289    #[test]
290    fn json_nonce_string_roundtrip() {
291        let in_nnc = "1sod60ml6g26mfhsrsa0";
292        let json = json!({
293            "nnc": in_nnc
294        });
295
296        let ipld: Ipld = DagJsonCodec.decode(json.to_string().as_bytes()).unwrap();
297        let Ipld::Map(map) = ipld else {
298            panic!("IPLD is not a map");
299        };
300        let nnc = map.get("nnc").unwrap();
301        let nnc: Nonce = Nonce::try_from(nnc.clone()).unwrap();
302        assert_eq!(nnc.to_string(), in_nnc);
303        let nonce = Nonce::Nonce96(
304            *GenericArray::from_slice(Base32HexLower.decode(in_nnc).unwrap().as_slice()),
305            IncomingTyp::String,
306        );
307        assert_eq!(nnc, nonce);
308    }
309}