solana_clap_utils/
input_validators.rs

1use {
2    crate::{
3        input_parsers::parse_cpu_ranges,
4        keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD},
5    },
6    chrono::DateTime,
7    solana_clock::{Epoch, Slot},
8    solana_hash::Hash,
9    solana_keypair::read_keypair_file,
10    solana_pubkey::{Pubkey, MAX_SEED_LEN},
11    solana_signature::Signature,
12    std::{fmt::Display, ops::RangeBounds, str::FromStr},
13};
14
15fn is_parsable_generic<U, T>(string: T) -> Result<(), String>
16where
17    T: AsRef<str> + Display,
18    U: FromStr,
19    U::Err: Display,
20{
21    string
22        .as_ref()
23        .parse::<U>()
24        .map(|_| ())
25        .map_err(|err| format!("error parsing '{string}': {err}"))
26}
27
28// Return an error if string cannot be parsed as type T.
29// Takes a String to avoid second type parameter when used as a clap validator
30pub fn is_parsable<T>(string: String) -> Result<(), String>
31where
32    T: FromStr,
33    T::Err: Display,
34{
35    is_parsable_generic::<T, String>(string)
36}
37
38// Return an error if string cannot be parsed as numeric type T, and value not within specified
39// range
40pub fn is_within_range<T, R>(string: String, range: R) -> Result<(), String>
41where
42    T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add<Output = T> + From<usize>,
43    T::Err: Display,
44    R: RangeBounds<T> + std::fmt::Debug,
45{
46    match string.parse::<T>() {
47        Ok(input) => {
48            if !range.contains(&input) {
49                Err(format!("input '{input:?}' out of range {range:?}"))
50            } else {
51                Ok(())
52            }
53        }
54        Err(err) => Err(format!("error parsing '{string}': {err}")),
55    }
56}
57
58// Return an error if a pubkey cannot be parsed.
59pub fn is_pubkey<T>(string: T) -> Result<(), String>
60where
61    T: AsRef<str> + Display,
62{
63    is_parsable_generic::<Pubkey, _>(string)
64}
65
66// Return an error if a hash cannot be parsed.
67pub fn is_hash<T>(string: T) -> Result<(), String>
68where
69    T: AsRef<str> + Display,
70{
71    is_parsable_generic::<Hash, _>(string)
72}
73
74// Return an error if a keypair file cannot be parsed.
75pub fn is_keypair<T>(string: T) -> Result<(), String>
76where
77    T: AsRef<str> + Display,
78{
79    read_keypair_file(string.as_ref())
80        .map(|_| ())
81        .map_err(|err| format!("{err}"))
82}
83
84// Return an error if a keypair file cannot be parsed
85pub fn is_keypair_or_ask_keyword<T>(string: T) -> Result<(), String>
86where
87    T: AsRef<str> + Display,
88{
89    if string.as_ref() == ASK_KEYWORD {
90        return Ok(());
91    }
92    read_keypair_file(string.as_ref())
93        .map(|_| ())
94        .map_err(|err| format!("{err}"))
95}
96
97// Return an error if a `SignerSourceKind::Prompt` cannot be parsed
98pub fn is_prompt_signer_source<T>(string: T) -> Result<(), String>
99where
100    T: AsRef<str> + Display,
101{
102    if string.as_ref() == ASK_KEYWORD {
103        return Ok(());
104    }
105    match parse_signer_source(string.as_ref())
106        .map_err(|err| format!("{err}"))?
107        .kind
108    {
109        SignerSourceKind::Prompt => Ok(()),
110        _ => Err(format!(
111            "Unable to parse input as `prompt:` URI scheme or `ASK` keyword: {string}"
112        )),
113    }
114}
115
116// Return an error if string cannot be parsed as pubkey string or keypair file location
117pub fn is_pubkey_or_keypair<T>(string: T) -> Result<(), String>
118where
119    T: AsRef<str> + Display,
120{
121    is_pubkey(string.as_ref()).or_else(|_| is_keypair(string))
122}
123
124// Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can
125// produce a pubkey()
126pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
127where
128    T: AsRef<str> + Display,
129{
130    match parse_signer_source(string.as_ref())
131        .map_err(|err| format!("{err}"))?
132        .kind
133    {
134        SignerSourceKind::Filepath(path) => is_keypair(path),
135        _ => Ok(()),
136    }
137}
138
139// Return an error if string cannot be parsed as a valid Signer. This is an alias of
140// `is_valid_pubkey`, and does accept pubkey strings, even though a Pubkey is not by itself
141// sufficient to sign a transaction.
142//
143// In the current offline-signing implementation, a pubkey is the valid input for a signer field
144// when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature).
145// Clap validators can't check multiple fields at once, so the verification that a `--signer` is
146// also provided and correct happens in parsing, not in validation.
147pub fn is_valid_signer<T>(string: T) -> Result<(), String>
148where
149    T: AsRef<str> + Display,
150{
151    is_valid_pubkey(string)
152}
153
154// Return an error if string cannot be parsed as pubkey=signature string
155pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
156where
157    T: AsRef<str> + Display,
158{
159    let mut signer = string.as_ref().split('=');
160    match Pubkey::from_str(
161        signer
162            .next()
163            .ok_or_else(|| "Malformed signer string".to_string())?,
164    ) {
165        Ok(_) => {
166            match Signature::from_str(
167                signer
168                    .next()
169                    .ok_or_else(|| "Malformed signer string".to_string())?,
170            ) {
171                Ok(_) => Ok(()),
172                Err(err) => Err(format!("{err}")),
173            }
174        }
175        Err(err) => Err(format!("{err}")),
176    }
177}
178
179// Return an error if a url cannot be parsed.
180pub fn is_url<T>(string: T) -> Result<(), String>
181where
182    T: AsRef<str> + Display,
183{
184    match url::Url::parse(string.as_ref()) {
185        Ok(url) => {
186            if url.has_host() {
187                Ok(())
188            } else {
189                Err("no host provided".to_string())
190            }
191        }
192        Err(err) => Err(format!("{err}")),
193    }
194}
195
196pub fn is_url_or_moniker<T>(string: T) -> Result<(), String>
197where
198    T: AsRef<str> + Display,
199{
200    match url::Url::parse(&normalize_to_url_if_moniker(string.as_ref())) {
201        Ok(url) => {
202            if url.has_host() {
203                Ok(())
204            } else {
205                Err("no host provided".to_string())
206            }
207        }
208        Err(err) => Err(format!("{err}")),
209    }
210}
211
212pub fn normalize_to_url_if_moniker<T: AsRef<str>>(url_or_moniker: T) -> String {
213    match url_or_moniker.as_ref() {
214        "m" | "mainnet-beta" => "https://api.mainnet-beta.solana.com",
215        "t" | "testnet" => "https://api.testnet.solana.com",
216        "d" | "devnet" => "https://api.devnet.solana.com",
217        "l" | "localhost" => "http://localhost:8899",
218        url => url,
219    }
220    .to_string()
221}
222
223pub fn is_epoch<T>(epoch: T) -> Result<(), String>
224where
225    T: AsRef<str> + Display,
226{
227    is_parsable_generic::<Epoch, _>(epoch)
228}
229
230pub fn is_slot<T>(slot: T) -> Result<(), String>
231where
232    T: AsRef<str> + Display,
233{
234    is_parsable_generic::<Slot, _>(slot)
235}
236
237pub fn is_pow2<T>(bins: T) -> Result<(), String>
238where
239    T: AsRef<str> + Display,
240{
241    bins.as_ref()
242        .parse::<usize>()
243        .map_err(|e| format!("Unable to parse, provided: {bins}, err: {e}"))
244        .and_then(|v| {
245            if !v.is_power_of_two() {
246                Err(format!("Must be a power of 2: {v}"))
247            } else {
248                Ok(())
249            }
250        })
251}
252
253pub fn is_port<T>(port: T) -> Result<(), String>
254where
255    T: AsRef<str> + Display,
256{
257    is_parsable_generic::<u16, _>(port)
258}
259
260pub fn is_valid_percentage<T>(percentage: T) -> Result<(), String>
261where
262    T: AsRef<str> + Display,
263{
264    percentage
265        .as_ref()
266        .parse::<u8>()
267        .map_err(|e| format!("Unable to parse input percentage, provided: {percentage}, err: {e}"))
268        .and_then(|v| {
269            if v > 100 {
270                Err(format!(
271                    "Percentage must be in range of 0 to 100, provided: {v}"
272                ))
273            } else {
274                Ok(())
275            }
276        })
277}
278
279pub fn is_amount<T>(amount: T) -> Result<(), String>
280where
281    T: AsRef<str> + Display,
282{
283    if amount.as_ref().parse::<u64>().is_ok() || amount.as_ref().parse::<f64>().is_ok() {
284        Ok(())
285    } else {
286        Err(format!(
287            "Unable to parse input amount as integer or float, provided: {amount}"
288        ))
289    }
290}
291
292pub fn is_amount_or_all<T>(amount: T) -> Result<(), String>
293where
294    T: AsRef<str> + Display,
295{
296    if amount.as_ref().parse::<u64>().is_ok()
297        || amount.as_ref().parse::<f64>().is_ok()
298        || amount.as_ref() == "ALL"
299    {
300        Ok(())
301    } else {
302        Err(format!(
303            "Unable to parse input amount as integer or float, provided: {amount}"
304        ))
305    }
306}
307
308pub fn is_amount_or_all_or_available<T>(amount: T) -> Result<(), String>
309where
310    T: AsRef<str> + Display,
311{
312    if amount.as_ref().parse::<u64>().is_ok()
313        || amount.as_ref().parse::<f64>().is_ok()
314        || amount.as_ref() == "ALL"
315        || amount.as_ref() == "AVAILABLE"
316    {
317        Ok(())
318    } else {
319        Err(format!(
320            "Unable to parse input amount as integer or float, provided: {amount}"
321        ))
322    }
323}
324
325pub fn is_rfc3339_datetime<T>(value: T) -> Result<(), String>
326where
327    T: AsRef<str> + Display,
328{
329    DateTime::parse_from_rfc3339(value.as_ref())
330        .map(|_| ())
331        .map_err(|e| format!("{e}"))
332}
333
334pub fn is_derivation<T>(value: T) -> Result<(), String>
335where
336    T: AsRef<str> + Display,
337{
338    let value = value.as_ref().replace('\'', "");
339    let mut parts = value.split('/');
340    let account = parts.next().unwrap();
341    account
342        .parse::<u32>()
343        .map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}"))
344        .and_then(|_| {
345            if let Some(change) = parts.next() {
346                change.parse::<u32>().map_err(|e| {
347                    format!("Unable to parse derivation, provided: {change}, err: {e}")
348                })
349            } else {
350                Ok(0)
351            }
352        })
353        .map(|_| ())
354}
355
356pub fn is_structured_seed<T>(value: T) -> Result<(), String>
357where
358    T: AsRef<str> + Display,
359{
360    let (prefix, value) = value
361        .as_ref()
362        .split_once(':')
363        .ok_or("Seed must contain ':' as delimiter")
364        .unwrap();
365    if prefix.is_empty() || value.is_empty() {
366        Err(String::from("Seed prefix or value is empty"))
367    } else {
368        match prefix {
369            "string" | "pubkey" | "hex" | "u8" => Ok(()),
370            _ => {
371                let len = prefix.len();
372                if len != 5 && len != 6 {
373                    Err(format!("Wrong prefix length {len} {prefix}:{value}"))
374                } else {
375                    let sign = &prefix[0..1];
376                    let type_size = &prefix[1..len.saturating_sub(2)];
377                    let byte_order = &prefix[len.saturating_sub(2)..len];
378                    if sign != "u" && sign != "i" {
379                        Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
380                    } else if type_size != "16"
381                        && type_size != "32"
382                        && type_size != "64"
383                        && type_size != "128"
384                    {
385                        Err(format!(
386                            "Wrong prefix type size {type_size} {prefix}:{value}"
387                        ))
388                    } else if byte_order != "le" && byte_order != "be" {
389                        Err(format!(
390                            "Wrong prefix byte order {byte_order} {prefix}:{value}"
391                        ))
392                    } else {
393                        Ok(())
394                    }
395                }
396            }
397        }
398    }
399}
400
401pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
402where
403    T: AsRef<str> + Display,
404{
405    let value = value.as_ref();
406    if value.len() > MAX_SEED_LEN {
407        Err(format!(
408            "Address seed must not be longer than {MAX_SEED_LEN} bytes"
409        ))
410    } else {
411        Ok(())
412    }
413}
414
415pub fn validate_maximum_full_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
416where
417    T: AsRef<str> + Display,
418{
419    let value = value.as_ref();
420    if value.eq("0") {
421        Err(String::from(
422            "--maximum-full-snapshot-archives-to-retain cannot be zero",
423        ))
424    } else {
425        Ok(())
426    }
427}
428
429pub fn validate_maximum_incremental_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
430where
431    T: AsRef<str> + Display,
432{
433    let value = value.as_ref();
434    if value.eq("0") {
435        Err(String::from(
436            "--maximum-incremental-snapshot-archives-to-retain cannot be zero",
437        ))
438    } else {
439        Ok(())
440    }
441}
442
443pub fn validate_cpu_ranges<T>(value: T, err_prefix: &str) -> Result<(), String>
444where
445    T: AsRef<str> + Display,
446{
447    parse_cpu_ranges(value.as_ref())
448        .map(|_| ())
449        .map_err(|e| format!("{err_prefix} {e}"))
450}
451
452pub fn is_non_zero(value: impl AsRef<str>) -> Result<(), String> {
453    let value = value.as_ref();
454    if value.eq("0") {
455        Err(String::from("cannot be zero"))
456    } else {
457        Ok(())
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_is_derivation() {
467        assert_eq!(is_derivation("2"), Ok(()));
468        assert_eq!(is_derivation("0"), Ok(()));
469        assert_eq!(is_derivation("65537"), Ok(()));
470        assert_eq!(is_derivation("0/2"), Ok(()));
471        assert_eq!(is_derivation("0'/2'"), Ok(()));
472        assert!(is_derivation("a").is_err());
473        assert!(is_derivation("4294967296").is_err());
474        assert!(is_derivation("a/b").is_err());
475        assert!(is_derivation("0/4294967296").is_err());
476    }
477}