homestar_invocation/task/instruction/
nonce.rs1use 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#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub enum IncomingTyp {
29 String,
31 Bytes,
33}
34
35#[derive(Clone, Debug, PartialEq, EnumAsInner, Serialize, Deserialize)]
37pub enum Nonce {
38 Nonce96(Nonce96, IncomingTyp),
40 Nonce128(Nonce128, IncomingTyp),
42 Empty,
44}
45
46impl Nonce {
47 pub fn generate() -> Self {
50 Nonce::Nonce96(
51 *GenericArray::from_slice(xid::new().as_bytes()),
52 IncomingTyp::Bytes,
53 )
54 }
55
56 pub fn generate_128() -> Self {
58 Nonce::Nonce128(
59 *GenericArray::from_slice(Uuid::new_v4().as_bytes()),
60 IncomingTyp::Bytes,
61 )
62 }
63
64 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}