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