Skip to main content

stellar_xdr/cli/generate/
arbitrary.rs

1use arbitrary::Unstructured;
2use clap::{Args, ValueEnum};
3use std::{
4    io::{stdout, Write},
5    str::FromStr,
6};
7
8use crate::cli::util;
9
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12    #[error("unknown type {0}, choose one of {1:?}")]
13    UnknownType(String, &'static [&'static str]),
14    #[error("error reading file: {0}")]
15    ReadFile(#[from] std::io::Error),
16    #[error("error generating XDR: {0}")]
17    WriteXdr(#[from] crate::Error),
18    #[error("error generating JSON: {0}")]
19    GenerateJson(#[from] serde_json::Error),
20    #[error("error generating arbitrary value: {0}")]
21    Arbitrary(#[from] arbitrary::Error),
22    #[error("type doesn't have a text representation, use 'json' as output")]
23    TextUnsupported,
24    #[error("could not generate a value whose JSON contains all of {hints:?} within {attempts} attempts")]
25    HintNotFound { hints: Vec<String>, attempts: u64 },
26}
27
28/// Generate arbitrary XDR values
29#[derive(Args, Debug, Clone)]
30#[command()]
31pub struct Cmd {
32    /// XDR type to generate
33    #[arg(long)]
34    pub r#type: String,
35
36    // Output format to encode to
37    #[arg(long = "output", value_enum, default_value_t)]
38    pub output_format: OutputFormat,
39
40    /// Keep generating values until the value's JSON representation contains
41    /// this string. Useful for hinting toward a particular union/enum variant
42    /// or sub-value. Repeat the flag to require several substrings; all of
43    /// them must be present in the same value.
44    #[arg(long)]
45    pub hint: Vec<String>,
46
47    /// Maximum number of generation attempts when --hint is set before giving
48    /// up.
49    #[arg(long, default_value_t = 20_000)]
50    pub hint_attempts: u64,
51}
52
53#[derive(Default, Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
54pub enum OutputFormat {
55    Single,
56    #[default]
57    SingleBase64,
58    // TODO: Stream,
59    // TODO: StreamBase64,
60    // TODO: StreamFramed,
61    Json,
62    JsonFormatted,
63    Text,
64}
65
66// TODO: Remove run_x macro, it exists only to reduce the diff from when curr/next
67// channels existed and each had their own run_curr/run_next invocation.
68macro_rules! run_x {
69    ($f:ident) => {
70        fn $f(&self) -> Result<(), Error> {
71            use crate::WriteXdr;
72            let r#type = crate::TypeVariant::from_str(&self.r#type).map_err(|_| {
73                Error::UnknownType(self.r#type.clone(), &crate::TypeVariant::VARIANTS_STR)
74            })?;
75            let (v, hint_json) = self.generate(r#type)?;
76            match self.output_format {
77                OutputFormat::Single => {
78                    let l = crate::Limits::none();
79                    stdout().write_all(&v.to_xdr(l)?)?
80                }
81                OutputFormat::SingleBase64 => {
82                    let l = crate::Limits::none();
83                    println!("{}", v.to_xdr_base64(l)?)
84                }
85                OutputFormat::Json => match hint_json {
86                    Some(json) => println!("{json}"),
87                    None => println!("{}", serde_json::to_string(&v)?),
88                },
89                OutputFormat::JsonFormatted => {
90                    println!("{}", serde_json::to_string_pretty(&v)?);
91                }
92                OutputFormat::Text => {
93                    let v = serde_json::to_value(v)?;
94                    let text = util::serde_json_value_to_text(v).ok_or(Error::TextUnsupported)?;
95                    println!("{text}")
96                }
97            }
98            Ok(())
99        }
100    };
101}
102
103impl Cmd {
104    /// Run the CLIs generate arbitrary command.
105    ///
106    /// ## Errors
107    ///
108    /// If the command is configured with state that is invalid.
109    pub fn run(&self) -> Result<(), Error> {
110        self.run_inner()
111    }
112
113    run_x!(run_inner);
114
115    /// Generate a single arbitrary value of the given type from fresh random
116    /// bytes.
117    fn generate_one(type_: crate::TypeVariant) -> Result<crate::Type, Error> {
118        let r = rand::random::<[u8; 10_240]>();
119        let mut u = Unstructured::new(&r);
120        Ok(crate::Type::arbitrary(type_, &mut u)?)
121    }
122
123    /// Generate an arbitrary value of the given type.
124    ///
125    /// If any `hint`s are configured, values are generated repeatedly (up to
126    /// `hint_attempts` times) until one whose JSON representation contains all
127    /// of the hints is found. In that case the matching value's compact JSON,
128    /// already computed to test the hints, is returned alongside the value so
129    /// callers can reuse it instead of serializing again. When no hints are
130    /// configured no JSON is computed and `None` is returned.
131    ///
132    /// An attempt that fails to generate or serialize is treated as unlucky
133    /// rather than fatal: it is skipped and the search continues with fresh
134    /// randomness, so a single bad draw doesn't abort the whole search.
135    fn generate(&self, type_: crate::TypeVariant) -> Result<(crate::Type, Option<String>), Error> {
136        if self.hint.is_empty() {
137            return Ok((Self::generate_one(type_)?, None));
138        }
139        for _ in 0..self.hint_attempts {
140            let Ok(v) = Self::generate_one(type_) else {
141                continue;
142            };
143            let Ok(json) = serde_json::to_string(&v) else {
144                continue;
145            };
146            if self.hint.iter().all(|hint| json.contains(hint)) {
147                return Ok((v, Some(json)));
148            }
149        }
150        Err(Error::HintNotFound {
151            hints: self.hint.clone(),
152            attempts: self.hint_attempts,
153        })
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn hint_matches() {
163        let cmd = Cmd {
164            r#type: "TimeBounds".to_string(),
165            output_format: OutputFormat::Json,
166            hint: vec!["min_time".into()],
167            hint_attempts: 1000,
168        };
169        let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
170        let (v, json) = cmd.generate(type_).unwrap();
171        let json = json.expect("a hinted search returns the computed json");
172        assert!(json.contains("min_time"));
173        assert_eq!(json, serde_json::to_string(&v).unwrap());
174    }
175
176    #[test]
177    fn all_hints_match() {
178        let cmd = Cmd {
179            r#type: "TimeBounds".to_string(),
180            output_format: OutputFormat::Json,
181            hint: vec!["min_time".into(), "max_time".into()],
182            hint_attempts: 1000,
183        };
184        let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
185        let (_v, json) = cmd.generate(type_).unwrap();
186        let json = json.expect("a hinted search returns the computed json");
187        assert!(json.contains("min_time"));
188        assert!(json.contains("max_time"));
189    }
190
191    #[test]
192    fn hint_not_found() {
193        let cmd = Cmd {
194            r#type: "TimeBounds".to_string(),
195            output_format: OutputFormat::Json,
196            hint: vec!["zzz_does_not_exist_xyzzy".into()],
197            hint_attempts: 5,
198        };
199        let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
200        assert!(matches!(
201            cmd.generate(type_),
202            Err(Error::HintNotFound { .. })
203        ));
204    }
205
206    #[test]
207    fn not_all_hints_found() {
208        let cmd = Cmd {
209            r#type: "TimeBounds".to_string(),
210            output_format: OutputFormat::Json,
211            hint: vec!["min_time".into(), "zzz_does_not_exist_xyzzy".into()],
212            hint_attempts: 5,
213        };
214        let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
215        assert!(matches!(
216            cmd.generate(type_),
217            Err(Error::HintNotFound { .. })
218        ));
219    }
220
221    #[test]
222    fn no_hint_generates_once() {
223        let cmd = Cmd {
224            r#type: "TimeBounds".to_string(),
225            output_format: OutputFormat::Json,
226            hint: vec![],
227            hint_attempts: 1,
228        };
229        let type_ = crate::TypeVariant::from_str("TimeBounds").unwrap();
230        let (_v, json) = cmd.generate(type_).unwrap();
231        assert!(json.is_none());
232    }
233}