stellar_xdr/cli/generate/
arbitrary.rs1use arbitrary::Unstructured;
2use clap::{Args, ValueEnum};
3use std::{
4 ffi::{OsStr, OsString},
5 fs,
6 io::{stdin, stdout, Read, Write},
7 str::FromStr,
8};
9
10use crate::cli::util;
11
12#[derive(thiserror::Error, Debug)]
13pub enum Error {
14 #[error("unknown type {0}, choose one of {1:?}")]
15 UnknownType(String, &'static [&'static str]),
16 #[error("error reading file: {0}")]
17 ReadFile(#[from] std::io::Error),
18 #[error("error generating XDR: {0}")]
19 WriteXdr(#[from] crate::Error),
20 #[error("error generating JSON: {0}")]
21 GenerateJson(#[from] serde_json::Error),
22 #[error("error generating arbitrary value: {0}")]
23 Arbitrary(#[from] arbitrary::Error),
24 #[error("type doesn't have a text representation, use 'json' as output")]
25 TextUnsupported,
26 #[error("could not generate a value whose JSON contains all of {hints:?} within {attempts} attempts")]
27 HintNotFound { hints: Vec<String>, attempts: u64 },
28}
29
30#[derive(Args, Debug, Clone)]
32#[command()]
33pub struct Cmd {
34 #[arg(long)]
36 pub r#type: String,
37
38 #[arg(long = "output", value_enum, default_value_t)]
40 pub output_format: OutputFormat,
41
42 #[arg(long)]
47 pub hint: Vec<String>,
48
49 #[arg(long, default_value_t = 20_000)]
52 pub hint_attempts: u64,
53
54 #[arg(long, conflicts_with = "hint")]
59 pub entropy: Option<OsString>,
60}
61
62#[derive(Default, Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
63pub enum OutputFormat {
64 Single,
65 #[default]
66 SingleBase64,
67 Json,
71 JsonFormatted,
72 Text,
73}
74
75macro_rules! run_x {
78 ($f:ident) => {
79 fn $f(&self) -> Result<(), Error> {
80 use crate::WriteXdr;
81 let r#type = crate::TypeVariant::from_str(&self.r#type).map_err(|_| {
82 Error::UnknownType(self.r#type.clone(), &crate::TypeVariant::VARIANTS_STR)
83 })?;
84 let (v, hint_json) = self.generate(r#type)?;
85 match self.output_format {
86 OutputFormat::Single => {
87 let l = crate::Limits::none();
88 stdout().write_all(&v.to_xdr(l)?)?
89 }
90 OutputFormat::SingleBase64 => {
91 let l = crate::Limits::none();
92 println!("{}", v.to_xdr_base64(l)?)
93 }
94 OutputFormat::Json => match hint_json {
95 Some(json) => println!("{json}"),
96 None => println!("{}", serde_json::to_string(&v)?),
97 },
98 OutputFormat::JsonFormatted => {
99 println!("{}", serde_json::to_string_pretty(&v)?);
100 }
101 OutputFormat::Text => {
102 let v = serde_json::to_value(v)?;
103 let text = util::serde_json_value_to_text(v).ok_or(Error::TextUnsupported)?;
104 println!("{text}")
105 }
106 }
107 Ok(())
108 }
109 };
110}
111
112impl Cmd {
113 pub fn run(&self) -> Result<(), Error> {
119 self.run_inner()
120 }
121
122 run_x!(run_inner);
123
124 fn generate_one(type_: crate::TypeVariant) -> Result<crate::Type, Error> {
127 let r = rand::random::<[u8; 10_240]>();
128 let mut u = Unstructured::new(&r);
129 Ok(crate::Type::arbitrary(type_, &mut u)?)
130 }
131
132 fn read_entropy(path: &OsStr) -> Result<Vec<u8>, Error> {
135 if path == "-" {
136 let mut bytes = Vec::new();
137 stdin().read_to_end(&mut bytes)?;
138 Ok(bytes)
139 } else {
140 Ok(fs::read(path)?)
141 }
142 }
143
144 fn generate(&self, type_: crate::TypeVariant) -> Result<(crate::Type, Option<String>), Error> {
162 if let Some(path) = &self.entropy {
163 let bytes = Self::read_entropy(path)?;
164 let mut u = Unstructured::new(&bytes);
165 return Ok((crate::Type::arbitrary(type_, &mut u)?, None));
166 }
167 if self.hint.is_empty() {
168 return Ok((Self::generate_one(type_)?, None));
169 }
170 for _ in 0..self.hint_attempts {
171 let Ok(v) = Self::generate_one(type_) else {
172 continue;
173 };
174 let Ok(json) = serde_json::to_string(&v) else {
175 continue;
176 };
177 if self.hint.iter().all(|hint| json.contains(hint)) {
178 return Ok((v, Some(json)));
179 }
180 }
181 Err(Error::HintNotFound {
182 hints: self.hint.clone(),
183 attempts: self.hint_attempts,
184 })
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
195 fn entropy_is_deterministic() {
196 let file = std::env::temp_dir().join("stellar-xdr-arbitrary-entropy");
197 fs::write(&file, (0u8..=255).collect::<Vec<u8>>()).unwrap();
198 let cmd = Cmd {
199 r#type: "TimeBounds".to_string(),
200 output_format: OutputFormat::Json,
201 hint: vec![],
202 hint_attempts: 1,
203 entropy: Some(file.clone().into_os_string()),
204 };
205 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
206 let (first, json) = cmd.generate(type_).unwrap();
207 assert!(json.is_none());
208 let (second, _) = cmd.generate(type_).unwrap();
209 assert_eq!(
210 serde_json::to_string(&first).unwrap(),
211 serde_json::to_string(&second).unwrap(),
212 );
213 fs::remove_file(&file).unwrap();
214 }
215
216 #[test]
217 fn hint_matches() {
218 let cmd = Cmd {
219 r#type: "TimeBounds".to_string(),
220 output_format: OutputFormat::Json,
221 hint: vec!["min_time".into()],
222 hint_attempts: 1000,
223 entropy: None,
224 };
225 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
226 let (v, json) = cmd.generate(type_).unwrap();
227 let json = json.expect("a hinted search returns the computed json");
228 assert!(json.contains("min_time"));
229 assert_eq!(json, serde_json::to_string(&v).unwrap());
230 }
231
232 #[test]
233 fn all_hints_match() {
234 let cmd = Cmd {
235 r#type: "TimeBounds".to_string(),
236 output_format: OutputFormat::Json,
237 hint: vec!["min_time".into(), "max_time".into()],
238 hint_attempts: 1000,
239 entropy: None,
240 };
241 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
242 let (_v, json) = cmd.generate(type_).unwrap();
243 let json = json.expect("a hinted search returns the computed json");
244 assert!(json.contains("min_time"));
245 assert!(json.contains("max_time"));
246 }
247
248 #[test]
249 fn hint_not_found() {
250 let cmd = Cmd {
251 r#type: "TimeBounds".to_string(),
252 output_format: OutputFormat::Json,
253 hint: vec!["zzz_does_not_exist_xyzzy".into()],
254 hint_attempts: 5,
255 entropy: None,
256 };
257 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
258 assert!(matches!(
259 cmd.generate(type_),
260 Err(Error::HintNotFound { .. })
261 ));
262 }
263
264 #[test]
265 fn not_all_hints_found() {
266 let cmd = Cmd {
267 r#type: "TimeBounds".to_string(),
268 output_format: OutputFormat::Json,
269 hint: vec!["min_time".into(), "zzz_does_not_exist_xyzzy".into()],
270 hint_attempts: 5,
271 entropy: None,
272 };
273 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
274 assert!(matches!(
275 cmd.generate(type_),
276 Err(Error::HintNotFound { .. })
277 ));
278 }
279
280 #[test]
281 fn no_hint_generates_once() {
282 let cmd = Cmd {
283 r#type: "TimeBounds".to_string(),
284 output_format: OutputFormat::Json,
285 hint: vec![],
286 hint_attempts: 1,
287 entropy: None,
288 };
289 let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
290 let (_v, json) = cmd.generate(type_).unwrap();
291 assert!(json.is_none());
292 }
293}