Skip to main content

jam_tooling/
parsing.rs

1use anyhow::anyhow;
2use jam_std_common::hash_raw;
3use jam_types::{CoreIndex, Memo, ServiceId, VecSet, 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 nanos = number
217		.checked_mul(scale)
218		.ok_or_else(|| anyhow!("The duration is too big: {number} * {scale}"))?;
219	Ok(Duration::from_nanos(nanos))
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); 9] = [
232	("w", 60 * 60 * 24 * 7 * 1_000_000_000),
233	("d", 60 * 60 * 24 * 1_000_000_000),
234	("h", 60 * 60 * 1_000_000_000),
235	("m", 60 * 1_000_000_000),
236	("s", 1_000_000_000),
237	("", 1_000_000_000),
238	("ms", 1_000_000),
239	("µs", 1_000),
240	("ns", 1),
241];
242
243/// Hex-encoded 32-byte Ed25519 public key.
244pub fn ed25519_public_key(s: &str) -> anyhow::Result<[u8; 32]> {
245	let s = s.trim();
246	hex::FromHex::from_hex(s).map_err(|e| anyhow!("Invalid Ed25519 public key {s:?}: {e}"))
247}
248
249/// Parse comma-separated list of cores or core ranges: "1,2,4-6,10". Ranges are inclusive.
250///
251/// NOTE This function doesn't check if core indices are correct since the protocol parameters might
252/// haven't been initialized at the time of command line arguments parsing.
253pub fn cores(s: &str) -> anyhow::Result<VecSet<CoreIndex>> {
254	let error = || {
255		anyhow!(
256			"Expects comma-separated list of cores or core ranges, e.g. \"1,2,4-6,10\". \
257            Ranges are inclusive."
258		)
259	};
260	let mut cores = VecSet::new();
261	for core_spec in s.split(',') {
262		let range = if core_spec.contains('-') {
263			let mut iter = core_spec.splitn(2, '-');
264			let min_core: CoreIndex =
265				iter.next().ok_or_else(error)?.parse().map_err(|_| error())?;
266			let max_core: CoreIndex =
267				iter.next().ok_or_else(error)?.parse().map_err(|_| error())?;
268			min_core..=max_core
269		} else {
270			let core: CoreIndex = core_spec.parse()?;
271			core..=core
272		};
273		for core in range {
274			cores.insert(core);
275		}
276	}
277	Ok(cores)
278}
279
280pub fn import_spec(input: &str) -> anyhow::Result<jam_types::ImportSpec> {
281	let mut parts = input.splitn(3, ':');
282	let malformed = anyhow!("import must be of form \"target_prefix:hash:index\"");
283	let Some(prefix) = parts.next() else {
284		return Err(malformed);
285	};
286	let is_segment_root = if prefix == "wp" {
287		false
288	} else if prefix == "sr" {
289		true
290	} else {
291		return Err(anyhow!(
292			"invalid prefix for imports, must be `sr` (segment root) or `wp` (work package)"
293		))
294	};
295	let Some(hash_str) = parts.next() else {
296		return Err(malformed);
297	};
298	let hash: [u8; 32] = decode_hex(hash_str)?;
299	let Some(index_str) = parts.next() else {
300		return Err(malformed);
301	};
302	let index = index_str.parse::<u16>()?;
303	Ok(if is_segment_root {
304		jam_types::ImportSpec {
305			root: jam_types::RootIdentifier::Direct(jam_types::SegmentTreeRoot(hash)),
306			index,
307		}
308	} else {
309		jam_types::ImportSpec {
310			root: jam_types::RootIdentifier::Indirect(jam_types::WorkPackageHash(hash)),
311			index,
312		}
313	})
314}
315
316/// Decode a hex string to a fixed-size byte array, accepting the length parameter
317fn decode_hex<const N: usize>(s: &str) -> anyhow::Result<[u8; N]> {
318	let s = s.strip_prefix("0x").unwrap_or(s);
319	if s.len() != N * 2 {
320		return Err(anyhow!("Invalid hex length: expected {} characters, got {}", N * 2, s.len()))
321	}
322	let mut result = [0u8; N];
323	for i in 0..N {
324		let byte_str = &s[i * 2..(i + 1) * 2];
325		result[i] = u8::from_str_radix(byte_str, 16)
326			.map_err(|_| anyhow!("Invalid hex character at position {}", i * 2))?;
327	}
328	Ok(result)
329}
330
331#[cfg(test)]
332mod tests {
333	use super::*;
334
335	#[test]
336	fn exact_bytes_works() {
337		assert_eq!(1, exact_bytes("1B").unwrap());
338		assert_eq!(1024, exact_bytes("1KiB").unwrap());
339		assert_eq!(4 * 1024 * 1024, exact_bytes(" 4 mib ").unwrap());
340	}
341
342	#[test]
343	fn exact_duration_works() {
344		assert_eq!(Duration::from_secs(1), exact_duration("1 s").unwrap());
345		assert_eq!(Duration::from_secs(60), exact_duration("1m").unwrap());
346		assert_eq!(Duration::from_secs(60 * 60), exact_duration("1h").unwrap());
347		assert_eq!(Duration::from_secs(60 * 60 * 24), exact_duration("1d").unwrap());
348		assert_eq!(Duration::from_secs(60 * 60 * 24 * 7), exact_duration("1w").unwrap());
349	}
350
351	#[test]
352	fn cores_work() {
353		assert_eq!(&[1, 2, 4, 5, 6, 10], cores("1,2,4-6,10").unwrap().as_ref());
354		assert_eq!(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], cores("1-4,2-10").unwrap().as_ref());
355	}
356
357	#[test]
358	fn ed25519_public_key_works() {
359		assert_eq!(
360			[0x11_u8; 32],
361			ed25519_public_key("1111111111111111111111111111111111111111111111111111111111111111")
362				.unwrap()
363		);
364	}
365}