jam_tooling/
parsing.rs

1use anyhow::anyhow;
2use jam_std_common::hash_raw;
3use jam_types::{Memo, ServiceId, MEMO_LEN};
4use std::time::Duration;
5
6pub type Blob = Vec<u8>;
7
8#[derive(Clone, Debug)]
9#[allow(clippy::len_without_is_empty)]
10pub enum DataIdLen {
11	HashLen([u8; 32], usize),
12	Data(Vec<u8>),
13}
14
15impl DataIdLen {
16	pub fn hash(&self) -> [u8; 32] {
17		match self {
18			DataIdLen::HashLen(hash, _) => *hash,
19			DataIdLen::Data(data) => hash_raw(data),
20		}
21	}
22	pub fn len(&self) -> usize {
23		match self {
24			DataIdLen::HashLen(_, l) => *l,
25			DataIdLen::Data(data) => data.len(),
26		}
27	}
28	pub fn data(&self) -> Option<&[u8]> {
29		match self {
30			DataIdLen::HashLen(..) => None,
31			DataIdLen::Data(ref data) => Some(data),
32		}
33	}
34	pub fn into_data(self) -> Option<Vec<u8>> {
35		match self {
36			DataIdLen::HashLen(..) => None,
37			DataIdLen::Data(d) => Some(d),
38		}
39	}
40}
41
42pub fn data_id_len(s: &str) -> anyhow::Result<DataIdLen> {
43	match s {
44		_ if s.starts_with("0x") &&
45			s.len() > 67 &&
46			s.bytes().skip(2).take(64).all(|x| x.is_ascii_hexdigit()) &&
47			s.find(':') == Some(66) &&
48			s[67..].parse::<usize>().is_ok() =>
49		{
50			let mut hash = [0u8; 32];
51			for (i, c) in s.as_bytes()[2..66].chunks(2).enumerate() {
52				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
53			}
54			Ok(DataIdLen::HashLen(hash, s[67..].parse::<usize>().expect("see above")))
55		},
56		_ if s.len() > 65 &&
57			s.bytes().take(64).all(|x| x.is_ascii_hexdigit()) &&
58			s.find(':') == Some(64) &&
59			s[65..].parse::<usize>().is_ok() =>
60		{
61			let mut hash = [0u8; 32];
62			for (i, c) in s.as_bytes()[..64].chunks(2).enumerate() {
63				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
64			}
65			Ok(DataIdLen::HashLen(hash, s[65..].parse::<usize>().expect("see above")))
66		},
67		_ => Ok(DataIdLen::Data(blob(s)?)),
68	}
69}
70
71#[allow(dead_code)]
72#[derive(Clone, Debug)]
73pub enum DataId {
74	Hash([u8; 32]),
75	Data(Vec<u8>),
76}
77
78#[allow(dead_code)]
79#[allow(clippy::len_without_is_empty)]
80impl DataId {
81	pub fn hash(&self) -> [u8; 32] {
82		match self {
83			DataId::Hash(hash) => *hash,
84			DataId::Data(data) => hash_raw(data),
85		}
86	}
87	pub fn len(&self) -> Option<usize> {
88		match self {
89			DataId::Hash(_) => None,
90			DataId::Data(data) => Some(data.len()),
91		}
92	}
93	pub fn data(&self) -> Option<&[u8]> {
94		match self {
95			DataId::Hash(..) => None,
96			DataId::Data(ref data) => Some(data),
97		}
98	}
99}
100
101pub fn data_id(s: &str) -> anyhow::Result<DataId> {
102	match s {
103		_ if s.len() == 64 && s.bytes().all(|x| x.is_ascii_hexdigit()) => {
104			let mut hash = [0u8; 32];
105			for (i, c) in s.as_bytes().chunks(2).enumerate() {
106				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
107			}
108			Ok(DataId::Hash(hash))
109		},
110		_ if s.starts_with("0x") &&
111			s.len() == 66 &&
112			s.bytes().skip(2).all(|x| x.is_ascii_hexdigit()) =>
113		{
114			let mut hash = [0u8; 32];
115			for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
116				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
117			}
118			Ok(DataId::Hash(hash))
119		},
120		_ => Ok(DataId::Data(blob(s)?)),
121	}
122}
123
124pub fn blob(s: &str) -> anyhow::Result<Blob> {
125	match s {
126		_ if std::path::Path::new(s).exists() => Ok(std::fs::read(s)?),
127		_ if s.starts_with("0x") &&
128			s.len().is_multiple_of(2) &&
129			s.bytes().skip(2).all(|x| x.is_ascii_hexdigit()) =>
130		{
131			let mut inner = Vec::with_capacity((s.len() - 2) / 2);
132			for c in s.as_bytes()[2..].chunks(2) {
133				inner.push(u8::from_str_radix(std::str::from_utf8(c)?, 16)?);
134			}
135			Ok(inner)
136		},
137		_ => Ok(s.as_bytes().to_vec()),
138	}
139}
140
141pub fn gas(s: &str) -> anyhow::Result<u64> {
142	if s == "max" {
143		Ok(u64::MAX)
144	} else {
145		Ok(s.parse::<u64>()?)
146	}
147}
148
149pub fn memo(s: &str) -> anyhow::Result<Memo> {
150	match s {
151		_ if s.len() == MEMO_LEN * 2 && s.bytes().all(|x| x.is_ascii_hexdigit()) => {
152			let mut inner = Memo::default();
153			for (i, c) in s.as_bytes().chunks(2).enumerate() {
154				inner[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
155			}
156			Ok(inner)
157		},
158		_ if s.len() <= MEMO_LEN => {
159			let mut inner = Memo::default();
160			inner.0[..s.len()].copy_from_slice(s.as_bytes());
161			Ok(inner)
162		},
163		_ => Err(anyhow!("Memo too long")),
164	}
165}
166
167pub fn service_id(s: &str) -> anyhow::Result<ServiceId> {
168	Ok(ServiceId::from_str_radix(s, 16)?)
169}
170
171pub fn exact_bytes(s: &str) -> anyhow::Result<u64> {
172	let i = s
173		.rfind(char::is_numeric)
174		.ok_or_else(|| anyhow!("Failed to parse number from {s:?}"))?;
175	let (number_str, prefix) = s.split_at(i + 1);
176	let prefix = prefix.trim();
177	if !matches!(prefix.chars().last(), Some('B' | 'b')) {
178		return Err(anyhow!("Failed to parse size in bytes: invalid prefix {prefix:?}"));
179	}
180	let prefix = &prefix[..prefix.len() - 1];
181	let scale = get_iec_scale(prefix)
182		.ok_or_else(|| anyhow!("Failed to parse size in bytes: invalid prefix {prefix:?}"))?;
183	let number: u64 = number_str.trim().parse()?;
184	number
185		.checked_mul(scale)
186		.ok_or_else(|| anyhow!("The size is too big: {number} * {scale}"))
187}
188
189fn get_iec_scale(prefix: &str) -> Option<u64> {
190	for (iec_prefix, scale) in IEC_TABLE.iter() {
191		if iec_prefix.eq_ignore_ascii_case(prefix) {
192			return Some(*scale);
193		}
194	}
195	None
196}
197
198pub(crate) const IEC_TABLE: [(&str, u64); 7] = [
199	("Ei", 1024_u64.pow(6)),
200	("Pi", 1024_u64.pow(5)),
201	("Ti", 1024_u64.pow(4)),
202	("Gi", 1024_u64.pow(3)),
203	("Mi", 1024_u64.pow(2)),
204	("Ki", 1024_u64),
205	("", 1_u64),
206];
207
208pub fn exact_duration(s: &str) -> anyhow::Result<Duration> {
209	let (number_str, unit) = match s.find(|ch: char| !ch.is_numeric()) {
210		Some(i) => (&s[..i], s[i..].trim()),
211		None => (s, ""),
212	};
213	let scale =
214		get_duration_scale(unit).ok_or_else(|| anyhow!("Invalid duration unit {unit:?}"))?;
215	let number: u64 = number_str.trim().parse()?;
216	let secs = number
217		.checked_mul(scale)
218		.ok_or_else(|| anyhow!("The duration is too big: {number} * {scale}"))?;
219	Ok(Duration::from_secs(secs))
220}
221
222fn get_duration_scale(their_unit: &str) -> Option<u64> {
223	for (our_unit, scale) in DURATION_TABLE.iter() {
224		if our_unit.eq_ignore_ascii_case(their_unit) {
225			return Some(*scale);
226		}
227	}
228	None
229}
230
231pub(crate) const DURATION_TABLE: [(&str, u64); 6] =
232	[("w", 60 * 60 * 24 * 7), ("d", 60 * 60 * 24), ("h", 60 * 60), ("m", 60), ("s", 1), ("", 1)];
233
234#[cfg(test)]
235mod tests {
236	use super::*;
237
238	#[test]
239	fn exact_bytes_works() {
240		assert_eq!(1, exact_bytes("1B").unwrap());
241		assert_eq!(1024, exact_bytes("1KiB").unwrap());
242		assert_eq!(4 * 1024 * 1024, exact_bytes(" 4 mib ").unwrap());
243	}
244
245	#[test]
246	fn exact_duration_works() {
247		assert_eq!(Duration::from_secs(1), exact_duration("1 s").unwrap());
248		assert_eq!(Duration::from_secs(60), exact_duration("1m").unwrap());
249		assert_eq!(Duration::from_secs(60 * 60), exact_duration("1h").unwrap());
250		assert_eq!(Duration::from_secs(60 * 60 * 24), exact_duration("1d").unwrap());
251		assert_eq!(Duration::from_secs(60 * 60 * 24 * 7), exact_duration("1w").unwrap());
252	}
253}