qbt-clean 0.3.0

Automated rules-based cleaning of qBittorrent torrents.
use anyhow::Context as _;

pub fn parse_bytes(s: &str) -> anyhow::Result<u64> {
	let i = s
		.find(|c: char| !matches!(c, '0'..='9' | ',' | '.'))
		.unwrap_or(s.len());
	let (num, unit) = s.split_at(i);

	let last = unit.as_bytes().last().unwrap_or(&0);
	let unit_scale = match last {
		b'B' => 1,
		b'b' => 8,
		other => anyhow::bail!(
			"Invalid unit {:?} for byte count, expected \"B\".",
			other)
	};

	let unit = unit.trim();
	let prefix = &unit[..(unit.len() - 1)];

	let (base, prefix) = match prefix.strip_suffix('i') {
		Some(p) => (1024u64, p),
		None => (1000, prefix),
	};

	if prefix.len() > 1 {
		anyhow::bail!("Invalid unit {:?} for byte count, expected \"B\".", unit)
	}

	let pow = match prefix.as_bytes().first() {
		Some(&c) => crate::SI_PREFIXES.bytes()
			.position(|b| b == c)
			.map(|i| i + 1)
			.ok_or_else(|| anyhow::format_err!(
				"Invalid SI prefix {:?}.",
				prefix))?
			.try_into()
			.context("SI prefix too large")?,
		None => 0,
	};
	let scale = base.checked_pow(pow)
		.context("SI prefix to long")?;

	let (int, frac) = match num.find('.') {
		Some(i) => (&num[..i], &num[(i+1)..]),
		None => (num, ""),
	};

	let int = int.parse::<u64>()?
		.checked_mul(scale)
		.context("SI scale too large")?;

	let frac_int = if frac.is_empty() {
		0
	} else {
		frac.parse::<u128>()
			.context("fractional component")?
	};
	let frac_int = frac_int.checked_mul(scale.into())
		.context("fractional component")?;
	let frac = frac_int / 10u128.pow(frac.len().try_into().unwrap_or(u32::MAX));
	let frac = frac.try_into()
		.context("fractional component")?;

	let combined = int.checked_add(frac)
		.context("number too large")?;

	Ok(combined / unit_scale)
}