1use std::fmt;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5pub type Result<T> = std::result::Result<T, Error>;
6
7#[derive(Debug, Clone)]
9pub struct Error(pub String);
10
11impl fmt::Display for Error {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 f.write_str(&self.0)
14 }
15}
16impl std::error::Error for Error {}
17impl From<std::io::Error> for Error {
18 fn from(e: std::io::Error) -> Self {
19 Error(e.to_string())
20 }
21}
22impl From<serde_json::Error> for Error {
23 fn from(e: serde_json::Error) -> Self {
24 Error(format!("json: {e}"))
25 }
26}
27impl From<toml::de::Error> for Error {
28 fn from(e: toml::de::Error) -> Self {
29 Error(format!("toml: {e}"))
30 }
31}
32
33pub fn err<T>(msg: impl Into<String>) -> Result<T> {
34 Err(Error(msg.into()))
35}
36
37pub fn now_iso() -> String {
40 let since_epoch = SystemTime::now()
41 .duration_since(UNIX_EPOCH)
42 .unwrap_or_default();
43 format_rfc3339_utc(since_epoch.as_secs(), since_epoch.subsec_nanos())
44}
45
46fn format_rfc3339_utc(unix_secs: u64, nanos: u32) -> String {
47 let days = (unix_secs / 86_400) as i64;
48 let rem = unix_secs % 86_400;
49 let z = days + 719_468;
51 let era = z.div_euclid(146_097);
52 let doe = z.rem_euclid(146_097);
53 let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
54 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
55 let mp = (5 * doy + 2) / 153;
56 let day = doy - (153 * mp + 2) / 5 + 1;
57 let month = if mp < 10 { mp + 3 } else { mp - 9 };
58 let year = yoe + era * 400 + i64::from(month <= 2);
59 let mut out = format!(
60 "{year:04}-{month:02}-{day:02}T{:02}:{:02}:{:02}",
61 rem / 3_600,
62 rem % 3_600 / 60,
63 rem % 60
64 );
65 if nanos != 0 {
66 let frac = format!("{nanos:09}");
67 out.push('.');
68 out.push_str(frac.trim_end_matches('0'));
69 }
70 out.push('Z');
71 out
72}
73
74pub fn now_ms() -> u128 {
75 SystemTime::now()
76 .duration_since(UNIX_EPOCH)
77 .map(|d| d.as_millis())
78 .unwrap_or(0)
79}
80
81pub fn sha256_hex(bytes: &[u8]) -> String {
82 rightkit_fs::digest::sha256_bytes(bytes)
83}
84
85pub fn new_id() -> String {
87 let mut b = [0u8; 16];
88 getrandom::fill(&mut b).expect("operating system random source unavailable");
89 b[6] = (b[6] & 0x0f) | 0x40;
90 b[8] = (b[8] & 0x3f) | 0x80;
91 let h = |r: std::ops::Range<usize>| b[r].iter().map(|x| format!("{x:02x}")).collect::<String>();
92 format!(
93 "{}-{}-{}-{}-{}",
94 h(0..4),
95 h(4..6),
96 h(6..8),
97 h(8..10),
98 h(10..16)
99 )
100}
101
102pub fn sleep_ms(ms: u64) {
103 std::thread::sleep(Duration::from_millis(ms));
104}
105
106pub fn tail(s: &str, max: usize) -> String {
108 if s.len() <= max {
109 return s.to_string();
110 }
111 let mut start = s.len() - max;
112 while !s.is_char_boundary(start) {
113 start += 1;
114 }
115 s[start..].to_string()
116}
117
118pub fn require_text(value: &str, name: &str) -> Result<String> {
119 let v = value.trim();
120 if v.is_empty() {
121 return err(format!("{name} is required"));
122 }
123 Ok(v.to_string())
124}
125
126pub fn platform() -> Result<&'static str> {
128 if cfg!(target_os = "macos") {
129 Ok("darwin")
130 } else if cfg!(target_os = "windows") {
131 Ok("windows")
132 } else {
133 err(format!(
134 "rightkit-qa supports macOS and Windows, received {}",
135 std::env::consts::OS
136 ))
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn rfc3339_known_instants() {
146 assert_eq!(format_rfc3339_utc(0, 0), "1970-01-01T00:00:00Z");
147 assert_eq!(format_rfc3339_utc(951_782_400, 0), "2000-02-29T00:00:00Z");
148 assert_eq!(
149 format_rfc3339_utc(1_791_635_696, 789_000_000),
150 "2026-10-10T12:34:56.789Z"
151 );
152 assert_eq!(
153 format_rfc3339_utc(86_399, 123_456_789),
154 "1970-01-01T23:59:59.123456789Z"
155 );
156 let now = now_iso();
157 assert!(now.ends_with('Z') && now.len() >= 20, "{now}");
158 }
159
160 #[test]
161 fn new_id_is_a_version_4_uuid() {
162 let id = new_id();
163 assert_eq!(id.len(), 36);
164 let parts: Vec<&str> = id.split('-').collect();
165 assert_eq!(
166 parts.iter().map(|p| p.len()).collect::<Vec<_>>(),
167 vec![8, 4, 4, 4, 12]
168 );
169 assert!(parts[2].starts_with('4'));
170 assert!(matches!(parts[3].as_bytes()[0], b'8' | b'9' | b'a' | b'b'));
171 assert!(id.bytes().all(|c| c == b'-' || c.is_ascii_hexdigit()));
172 assert_ne!(id, new_id());
173 }
174}
175
176pub fn base64_decode(s: &str) -> Option<Vec<u8>> {
178 let mut out = Vec::with_capacity(s.len() * 3 / 4);
179 let (mut acc, mut bits) = (0u32, 0u32);
180 for c in s.bytes() {
181 let v = match c {
182 b'A'..=b'Z' => c - b'A',
183 b'a'..=b'z' => c - b'a' + 26,
184 b'0'..=b'9' => c - b'0' + 52,
185 b'+' => 62,
186 b'/' => 63,
187 b'=' | b'\n' | b'\r' | b' ' => continue,
188 _ => return None,
189 } as u32;
190 acc = (acc << 6) | v;
191 bits += 6;
192 if bits >= 8 {
193 bits -= 8;
194 out.push((acc >> bits) as u8);
195 acc &= (1 << bits) - 1;
196 }
197 }
198 Some(out)
199}
200
201#[cfg(test)]
202mod base64_tests {
203 use super::base64_decode;
204 #[test]
205 fn decodes_rfc4648_vectors_and_rejects_junk() {
206 for (i, o) in [
207 ("", ""),
208 ("Zg==", "f"),
209 ("Zm8=", "fo"),
210 ("Zm9v", "foo"),
211 ("Zm9vYg==", "foob"),
212 ("Zm9vYmFy", "foobar"),
213 ] {
214 assert_eq!(base64_decode(i).unwrap(), o.as_bytes());
215 }
216 assert!(base64_decode("Zm9v!").is_none());
217 }
218}