use std::fmt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
pub struct Error(pub String);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for Error {}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error(e.to_string())
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error(format!("json: {e}"))
}
}
impl From<toml::de::Error> for Error {
fn from(e: toml::de::Error) -> Self {
Error(format!("toml: {e}"))
}
}
pub fn err<T>(msg: impl Into<String>) -> Result<T> {
Err(Error(msg.into()))
}
pub fn now_iso() -> String {
let since_epoch = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
format_rfc3339_utc(since_epoch.as_secs(), since_epoch.subsec_nanos())
}
fn format_rfc3339_utc(unix_secs: u64, nanos: u32) -> String {
let days = (unix_secs / 86_400) as i64;
let rem = unix_secs % 86_400;
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = yoe + era * 400 + i64::from(month <= 2);
let mut out = format!(
"{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}",
rem / 3_600,
rem % 3_600 / 60,
rem % 60
);
if nanos != 0 {
let frac = format!("{nanos:09}");
out.push('.');
out.push_str(frac.trim_end_matches('0'));
}
out.push('Z');
out
}
pub fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
pub fn sha256_hex(bytes: &[u8]) -> String {
rightkit_fs::digest::sha256_bytes(bytes)
}
pub fn new_id() -> String {
let mut b = [0u8; 16];
getrandom::fill(&mut b).expect("operating system random source unavailable");
b[6] = (b[6] & 0x0f) | 0x40;
b[8] = (b[8] & 0x3f) | 0x80;
let h = |r: std::ops::Range<usize>| b[r].iter().map(|x| format!("{x:02x}")).collect::<String>();
format!(
"{}-{}-{}-{}-{}",
h(0..4),
h(4..6),
h(6..8),
h(8..10),
h(10..16)
)
}
pub fn sleep_ms(ms: u64) {
std::thread::sleep(Duration::from_millis(ms));
}
pub fn tail(s: &str, max: usize) -> String {
if s.len() <= max {
return s.to_string();
}
let mut start = s.len() - max;
while !s.is_char_boundary(start) {
start += 1;
}
s[start..].to_string()
}
pub fn require_text(value: &str, name: &str) -> Result<String> {
let v = value.trim();
if v.is_empty() {
return err(format!("{name} is required"));
}
Ok(v.to_string())
}
pub fn platform() -> Result<&'static str> {
if cfg!(target_os = "macos") {
Ok("darwin")
} else if cfg!(target_os = "windows") {
Ok("windows")
} else {
err(format!(
"rightkit-qa supports macOS and Windows, received {}",
std::env::consts::OS
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc3339_known_instants() {
assert_eq!(format_rfc3339_utc(0, 0), "1970-01-01T00:00:00Z");
assert_eq!(format_rfc3339_utc(951_782_400, 0), "2000-02-29T00:00:00Z");
assert_eq!(
format_rfc3339_utc(1_791_635_696, 789_000_000),
"2026-10-10T12:34:56.789Z"
);
assert_eq!(
format_rfc3339_utc(86_399, 123_456_789),
"1970-01-01T23:59:59.123456789Z"
);
let now = now_iso();
assert!(now.ends_with('Z') && now.len() >= 20, "{now}");
}
#[test]
fn new_id_is_a_version_4_uuid() {
let id = new_id();
assert_eq!(id.len(), 36);
let parts: Vec<&str> = id.split('-').collect();
assert_eq!(
parts.iter().map(|p| p.len()).collect::<Vec<_>>(),
vec![8, 4, 4, 4, 12]
);
assert!(parts[2].starts_with('4'));
assert!(matches!(parts[3].as_bytes()[0], b'8' | b'9' | b'a' | b'b'));
assert!(id.bytes().all(|c| c == b'-' || c.is_ascii_hexdigit()));
assert_ne!(id, new_id());
}
}
pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
let mut out = Vec::with_capacity(s.len() * 3 / 4);
let (mut acc, mut bits) = (0u32, 0u32);
for c in s.bytes() {
let v = match c {
b'A'..=b'Z' => c - b'A',
b'a'..=b'z' => c - b'a' + 26,
b'0'..=b'9' => c - b'0' + 52,
b'+' => 62,
b'/' => 63,
b'=' | b'\n' | b'\r' | b' ' => continue,
_ => return None,
} as u32;
acc = (acc << 6) | v;
bits += 6;
if bits >= 8 {
bits -= 8;
out.push((acc >> bits) as u8);
acc &= (1 << bits) - 1;
}
}
Some(out)
}
#[cfg(test)]
mod base64_tests {
use super::base64_decode;
#[test]
fn decodes_rfc4648_vectors_and_rejects_junk() {
for (i, o) in [
("", ""),
("Zg==", "f"),
("Zm8=", "fo"),
("Zm9v", "foo"),
("Zm9vYg==", "foob"),
("Zm9vYmFy", "foobar"),
] {
assert_eq!(base64_decode(i).unwrap(), o.as_bytes());
}
assert!(base64_decode("Zm9v!").is_none());
}
}