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
28pub 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
38pub 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
58pub fn is_pubkey<T>(string: T) -> Result<(), String>
60where
61 T: AsRef<str> + Display,
62{
63 is_parsable_generic::<Pubkey, _>(string)
64}
65
66pub fn is_hash<T>(string: T) -> Result<(), String>
68where
69 T: AsRef<str> + Display,
70{
71 is_parsable_generic::<Hash, _>(string)
72}
73
74pub 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
84pub 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
97pub 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
116pub 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
124pub 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
139pub fn is_valid_signer<T>(string: T) -> Result<(), String>
148where
149 T: AsRef<str> + Display,
150{
151 is_valid_pubkey(string)
152}
153
154pub 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
179pub 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}