use std::fs::File;
use std::io::Read;
pub const RUN_ID_HEX_LEN: usize = 32;
pub const SHORT_RUN_ID_LEN: usize = 8;
pub const MIN_PREFIX_LEN: usize = 4;
pub trait RunIdSource: Send + Sync {
fn mint(&self) -> Result<String, String>;
}
#[derive(Debug, Default)]
pub struct RandomRunIds;
impl RunIdSource for RandomRunIds {
fn mint(&self) -> Result<String, String> {
let mut bytes = [0_u8; RUN_ID_HEX_LEN / 2];
random_bytes(&mut bytes)?;
Ok(hex(&bytes))
}
}
#[derive(Debug)]
pub struct FixedRunIds {
remaining: std::sync::Mutex<std::collections::VecDeque<String>>,
}
impl FixedRunIds {
pub fn new(ids: impl IntoIterator<Item = String>) -> Self {
Self {
remaining: std::sync::Mutex::new(ids.into_iter().collect()),
}
}
}
impl RunIdSource for FixedRunIds {
fn mint(&self) -> Result<String, String> {
self.remaining
.lock()
.expect("the fixed id queue is not poisoned")
.pop_front()
.ok_or_else(|| "the fixed run id source is exhausted".into())
}
}
fn random_bytes(buffer: &mut [u8]) -> Result<(), String> {
#[cfg(target_os = "linux")]
let filled = unsafe { libc::getrandom(buffer.as_mut_ptr().cast(), buffer.len(), 0) }
== buffer.len() as libc::ssize_t;
#[cfg(not(target_os = "linux"))]
let filled = unsafe { libc::getentropy(buffer.as_mut_ptr().cast(), buffer.len()) } == 0;
if filled {
return Ok(());
}
File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(buffer))
.map_err(|source| format!("cannot read random bytes for a run id: {source}"))
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}
pub fn short(id: &str) -> &str {
id.get(..SHORT_RUN_ID_LEN).unwrap_or(id)
}
pub fn alias(ticket_id: &str, attempt: i64) -> String {
format!("{ticket_id}-r{attempt}")
}
pub fn parse_alias(reference: &str) -> Option<(&str, i64)> {
let (ticket_id, attempt) = reference.rsplit_once("-r")?;
if ticket_id.is_empty() || attempt.is_empty() {
return None;
}
if !attempt.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
attempt
.parse()
.ok()
.filter(|attempt| *attempt > 0)
.map(|attempt| (ticket_id, attempt))
}
pub fn as_id_prefix(reference: &str) -> Option<String> {
let long_enough = (MIN_PREFIX_LEN..=RUN_ID_HEX_LEN).contains(&reference.len());
let hexadecimal = reference.bytes().all(|byte| byte.is_ascii_hexdigit());
(long_enough && hexadecimal).then(|| reference.to_ascii_lowercase())
}
pub const ACCEPTED_RUN_REFERENCES: &str = "an alias like `TICK-20-r1`, a ticket id or name for \
its latest run, or at least 4 characters of a run id";
#[cfg(test)]
mod tests {
use super::{
FixedRunIds, RUN_ID_HEX_LEN, RandomRunIds, RunIdSource, alias, as_id_prefix, parse_alias,
short,
};
use std::collections::HashSet;
#[test]
fn minted_ids_are_full_width_lowercase_hex_and_do_not_repeat() {
let source = RandomRunIds;
let mut seen = HashSet::new();
for _ in 0..512 {
let id = source.mint().expect("mint a run id");
assert_eq!(id.len(), RUN_ID_HEX_LEN, "{id}");
assert!(
id.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
"{id}"
);
assert!(seen.insert(id.clone()), "minted `{id}` twice");
}
}
#[test]
fn an_injected_source_decides_the_identities_and_reports_its_own_exhaustion() {
let source = FixedRunIds::new(["aaaa1111".into(), "bbbb2222".into()]);
assert_eq!(source.mint().unwrap(), "aaaa1111");
assert_eq!(source.mint().unwrap(), "bbbb2222");
assert!(source.mint().is_err());
}
#[test]
fn display_shortens_minted_ids_and_leaves_legacy_ids_whole() {
assert_eq!(short("3f2a9c1b7d4e5061a2b3c4d5e6f70819"), "3f2a9c1b");
assert_eq!(short("R14"), "R14");
}
#[test]
fn aliases_round_trip_through_parsing() {
let alias = alias("TICK-20", 3);
assert_eq!(alias, "TICK-20-r3");
assert_eq!(parse_alias(&alias), Some(("TICK-20", 3)));
}
#[test]
fn alias_parsing_rejects_bare_tickets_and_malformed_attempts() {
for reference in [
"TICK-20",
"TICK-20-r",
"TICK-20-r0",
"TICK-20-rx",
"-r1",
"TICK-20-r+1",
] {
assert_eq!(parse_alias(reference), None, "{reference}");
}
}
#[test]
fn prefixes_are_hexadecimal_of_a_useful_length_and_never_ticket_ids() {
assert_eq!(as_id_prefix("3F2a"), Some("3f2a".into()));
for reference in ["3f2", "TICK-20", "TICK-20-r1", "zzzz"] {
assert_eq!(as_id_prefix(reference), None, "{reference}");
}
}
}