solana_clap_utils/
input_validators.rs

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