jam-tooling 0.1.26

Various helpful utilities for JAM tooling developers
Documentation
use anyhow::anyhow;
use jam_std_common::hash_raw;
use jam_types::{Memo, ServiceId, MEMO_LEN};
use std::time::Duration;

pub type Blob = Vec<u8>;

#[derive(Clone, Debug)]
#[allow(clippy::len_without_is_empty)]
pub enum DataIdLen {
	HashLen([u8; 32], usize),
	Data(Vec<u8>),
}

impl DataIdLen {
	pub fn hash(&self) -> [u8; 32] {
		match self {
			DataIdLen::HashLen(hash, _) => *hash,
			DataIdLen::Data(data) => hash_raw(data),
		}
	}
	pub fn len(&self) -> usize {
		match self {
			DataIdLen::HashLen(_, l) => *l,
			DataIdLen::Data(data) => data.len(),
		}
	}
	pub fn data(&self) -> Option<&[u8]> {
		match self {
			DataIdLen::HashLen(..) => None,
			DataIdLen::Data(ref data) => Some(data),
		}
	}
	pub fn into_data(self) -> Option<Vec<u8>> {
		match self {
			DataIdLen::HashLen(..) => None,
			DataIdLen::Data(d) => Some(d),
		}
	}
}

pub fn data_id_len(s: &str) -> anyhow::Result<DataIdLen> {
	match s {
		_ if s.starts_with("0x") &&
			s.len() > 67 &&
			s.bytes().skip(2).take(64).all(|x| x.is_ascii_hexdigit()) &&
			s.find(':') == Some(66) &&
			s[67..].parse::<usize>().is_ok() =>
		{
			let mut hash = [0u8; 32];
			for (i, c) in s.as_bytes()[2..66].chunks(2).enumerate() {
				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
			}
			Ok(DataIdLen::HashLen(hash, s[67..].parse::<usize>().expect("see above")))
		},
		_ if s.len() > 65 &&
			s.bytes().take(64).all(|x| x.is_ascii_hexdigit()) &&
			s.find(':') == Some(64) &&
			s[65..].parse::<usize>().is_ok() =>
		{
			let mut hash = [0u8; 32];
			for (i, c) in s.as_bytes()[..64].chunks(2).enumerate() {
				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
			}
			Ok(DataIdLen::HashLen(hash, s[65..].parse::<usize>().expect("see above")))
		},
		_ => Ok(DataIdLen::Data(blob(s)?)),
	}
}

#[allow(dead_code)]
#[derive(Clone, Debug)]
pub enum DataId {
	Hash([u8; 32]),
	Data(Vec<u8>),
}

#[allow(dead_code)]
#[allow(clippy::len_without_is_empty)]
impl DataId {
	pub fn hash(&self) -> [u8; 32] {
		match self {
			DataId::Hash(hash) => *hash,
			DataId::Data(data) => hash_raw(data),
		}
	}
	pub fn len(&self) -> Option<usize> {
		match self {
			DataId::Hash(_) => None,
			DataId::Data(data) => Some(data.len()),
		}
	}
	pub fn data(&self) -> Option<&[u8]> {
		match self {
			DataId::Hash(..) => None,
			DataId::Data(ref data) => Some(data),
		}
	}
}

pub fn data_id(s: &str) -> anyhow::Result<DataId> {
	match s {
		_ if s.len() == 64 && s.bytes().all(|x| x.is_ascii_hexdigit()) => {
			let mut hash = [0u8; 32];
			for (i, c) in s.as_bytes().chunks(2).enumerate() {
				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
			}
			Ok(DataId::Hash(hash))
		},
		_ if s.starts_with("0x") &&
			s.len() == 66 &&
			s.bytes().skip(2).all(|x| x.is_ascii_hexdigit()) =>
		{
			let mut hash = [0u8; 32];
			for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
				hash[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
			}
			Ok(DataId::Hash(hash))
		},
		_ => Ok(DataId::Data(blob(s)?)),
	}
}

pub fn blob(s: &str) -> anyhow::Result<Blob> {
	match s {
		_ if std::path::Path::new(s).exists() => Ok(std::fs::read(s)?),
		_ if s.starts_with("0x") &&
			s.len().is_multiple_of(2) &&
			s.bytes().skip(2).all(|x| x.is_ascii_hexdigit()) =>
		{
			let mut inner = Vec::with_capacity((s.len() - 2) / 2);
			for c in s.as_bytes()[2..].chunks(2) {
				inner.push(u8::from_str_radix(std::str::from_utf8(c)?, 16)?);
			}
			Ok(inner)
		},
		_ => Ok(s.as_bytes().to_vec()),
	}
}

pub fn gas(s: &str) -> anyhow::Result<u64> {
	if s == "max" {
		Ok(u64::MAX)
	} else {
		Ok(s.parse::<u64>()?)
	}
}

pub fn memo(s: &str) -> anyhow::Result<Memo> {
	match s {
		_ if s.len() == MEMO_LEN * 2 && s.bytes().all(|x| x.is_ascii_hexdigit()) => {
			let mut inner = Memo::default();
			for (i, c) in s.as_bytes().chunks(2).enumerate() {
				inner[i] = u8::from_str_radix(std::str::from_utf8(c)?, 16)?;
			}
			Ok(inner)
		},
		_ if s.len() <= MEMO_LEN => {
			let mut inner = Memo::default();
			inner.0[..s.len()].copy_from_slice(s.as_bytes());
			Ok(inner)
		},
		_ => Err(anyhow!("Memo too long")),
	}
}

pub fn service_id(s: &str) -> anyhow::Result<ServiceId> {
	Ok(ServiceId::from_str_radix(s, 16)?)
}

pub fn exact_bytes(s: &str) -> anyhow::Result<u64> {
	let i = s
		.rfind(char::is_numeric)
		.ok_or_else(|| anyhow!("Failed to parse number from {s:?}"))?;
	let (number_str, prefix) = s.split_at(i + 1);
	let prefix = prefix.trim();
	if !matches!(prefix.chars().last(), Some('B' | 'b')) {
		return Err(anyhow!("Failed to parse size in bytes: invalid prefix {prefix:?}"));
	}
	let prefix = &prefix[..prefix.len() - 1];
	let scale = get_iec_scale(prefix)
		.ok_or_else(|| anyhow!("Failed to parse size in bytes: invalid prefix {prefix:?}"))?;
	let number: u64 = number_str.trim().parse()?;
	number
		.checked_mul(scale)
		.ok_or_else(|| anyhow!("The size is too big: {number} * {scale}"))
}

fn get_iec_scale(prefix: &str) -> Option<u64> {
	for (iec_prefix, scale) in IEC_TABLE.iter() {
		if iec_prefix.eq_ignore_ascii_case(prefix) {
			return Some(*scale);
		}
	}
	None
}

pub(crate) const IEC_TABLE: [(&str, u64); 7] = [
	("Ei", 1024_u64.pow(6)),
	("Pi", 1024_u64.pow(5)),
	("Ti", 1024_u64.pow(4)),
	("Gi", 1024_u64.pow(3)),
	("Mi", 1024_u64.pow(2)),
	("Ki", 1024_u64),
	("", 1_u64),
];

pub fn exact_duration(s: &str) -> anyhow::Result<Duration> {
	let (number_str, unit) = match s.find(|ch: char| !ch.is_numeric()) {
		Some(i) => (&s[..i], s[i..].trim()),
		None => (s, ""),
	};
	let scale =
		get_duration_scale(unit).ok_or_else(|| anyhow!("Invalid duration unit {unit:?}"))?;
	let number: u64 = number_str.trim().parse()?;
	let secs = number
		.checked_mul(scale)
		.ok_or_else(|| anyhow!("The duration is too big: {number} * {scale}"))?;
	Ok(Duration::from_secs(secs))
}

fn get_duration_scale(their_unit: &str) -> Option<u64> {
	for (our_unit, scale) in DURATION_TABLE.iter() {
		if our_unit.eq_ignore_ascii_case(their_unit) {
			return Some(*scale);
		}
	}
	None
}

pub(crate) const DURATION_TABLE: [(&str, u64); 6] =
	[("w", 60 * 60 * 24 * 7), ("d", 60 * 60 * 24), ("h", 60 * 60), ("m", 60), ("s", 1), ("", 1)];

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn exact_bytes_works() {
		assert_eq!(1, exact_bytes("1B").unwrap());
		assert_eq!(1024, exact_bytes("1KiB").unwrap());
		assert_eq!(4 * 1024 * 1024, exact_bytes(" 4 mib ").unwrap());
	}

	#[test]
	fn exact_duration_works() {
		assert_eq!(Duration::from_secs(1), exact_duration("1 s").unwrap());
		assert_eq!(Duration::from_secs(60), exact_duration("1m").unwrap());
		assert_eq!(Duration::from_secs(60 * 60), exact_duration("1h").unwrap());
		assert_eq!(Duration::from_secs(60 * 60 * 24), exact_duration("1d").unwrap());
		assert_eq!(Duration::from_secs(60 * 60 * 24 * 7), exact_duration("1w").unwrap());
	}
}