use crate::osutils::rand_chars;
use lazy_regex::regex;
use lazy_static::lazy_static;
use regex::bytes::Regex;
use std::time::{SystemTime, UNIX_EPOCH};
lazy_static! {
static ref FILE_ID_CHARS_RE: Regex = Regex::new(r#"[^\w.]"#).unwrap();
static ref REV_ID_CHARS_RE: Regex = Regex::new(r#"[^-\w.+@]"#).unwrap();
static ref GEN_FILE_ID_SUFFIX: String = gen_file_id_suffix();
}
fn gen_file_id_suffix() -> String {
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let random_chars = rand_chars(16);
format!(
"-{}-{}-",
crate::osutils::time::compact_date(current_time),
random_chars
)
}
pub fn next_id_suffix(suffix: Option<&str>) -> Vec<u8> {
static GEN_FILE_ID_SERIAL: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
let serial = GEN_FILE_ID_SERIAL.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"{}{}",
suffix.unwrap_or(GEN_FILE_ID_SUFFIX.as_str()),
serial
)
.into_bytes()
}
pub fn gen_file_id(name: &str) -> Vec<u8> {
let name_bytes = name
.chars()
.filter(|c| c.is_ascii())
.collect::<String>()
.to_ascii_lowercase()
.as_bytes()
.to_vec();
let ascii_word_only = FILE_ID_CHARS_RE
.replace_all(&name_bytes, |_: ®ex::bytes::Captures| b"")
.to_vec();
let without_dots = ascii_word_only
.into_iter()
.skip_while(|c| *c == b'.')
.collect::<Vec<u8>>();
let short = without_dots.iter().take(20).cloned().collect::<Vec<u8>>();
let suffix = next_id_suffix(None);
[short, suffix].concat()
}
pub fn gen_root_id() -> Vec<u8> {
gen_file_id("tree_root")
}
fn get_identifier(s: &str) -> Vec<u8> {
let mut identifier = s.to_string();
if let Some(start) = s.find('<') {
let end = s.rfind('>');
if end.is_some()
&& start < end.unwrap()
&& end.unwrap() == s.len() - 1
&& s[start..].find('@').is_some()
{
identifier = s[start + 1..end.unwrap()].to_string();
}
}
let identifier: String = identifier
.to_ascii_lowercase()
.replace(' ', "_")
.chars()
.filter(|c| c.is_ascii())
.collect();
REV_ID_CHARS_RE
.replace_all(identifier.as_bytes(), |_: ®ex::bytes::Captures| b"")
.to_vec()
}
pub fn gen_revision_id(username: &str, timestamp: Option<u64>) -> Vec<u8> {
let user_or_email = get_identifier(username);
let unique_chunk = crate::osutils::rand_chars(16).as_bytes().to_vec();
let timestamp = timestamp.unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
});
[
user_or_email,
crate::osutils::time::compact_date(timestamp)
.as_bytes()
.to_vec(),
unique_chunk,
]
.join(&b'-')
}
#[cfg(test)]
mod tests {
use super::*;
fn starts_with(id: &[u8], prefix: &[u8]) -> bool {
id.starts_with(prefix)
}
#[test]
fn gen_file_id_preserves_filename_prefix() {
assert!(starts_with(&gen_file_id("bar"), b"bar-"));
}
#[test]
fn gen_file_id_squashes_case_and_strips_non_word_chars() {
assert!(starts_with(&gen_file_id("Mwoo oof\t m"), b"mwoooofm-"));
}
#[test]
fn gen_file_id_strips_leading_dots() {
assert!(starts_with(&gen_file_id("..gam.py"), b"gam.py-"));
assert!(starts_with(&gen_file_id("..Mwoo oof\t m"), b"mwoooofm-"));
}
#[test]
fn gen_file_id_strips_non_ascii_and_avoids_hidden_id() {
assert!(starts_with(&gen_file_id("\u{e5}\u{b5}.txt"), b"txt-"));
}
#[test]
fn gen_file_id_truncates_to_twenty_chars_lowercased() {
let name: String = std::iter::repeat('A').take(50).collect::<String>() + ".txt";
let fid = gen_file_id(&name);
let expected_prefix: Vec<u8> = b"a".repeat(20);
let mut expected = expected_prefix;
expected.push(b'-');
assert!(starts_with(&fid, &expected));
assert!(fid.len() < 60);
}
#[test]
fn gen_file_id_truncation_happens_after_other_steps() {
let fid = gen_file_id("\u{e5}\u{b5}..aBcd\tefGhijKLMnop\tqrstuvwxyz");
assert!(starts_with(&fid, b"abcdefghijklmnopqrst-"));
assert!(fid.len() < 60);
}
#[test]
fn next_id_suffix_increments_serial() {
let ids: Vec<Vec<u8>> = (0..10).map(|_| next_id_suffix(Some("foo-"))).collect();
let ns: Vec<i64> = ids
.iter()
.map(|id| {
let s = std::str::from_utf8(id).unwrap();
s.rsplit('-').next().unwrap().parse().unwrap()
})
.collect();
for i in 1..ns.len() {
assert!(ns[i] > ns[i - 1]);
}
}
#[test]
fn gen_root_id_starts_with_tree_root() {
assert!(starts_with(&gen_root_id(), b"tree_root-"));
}
#[test]
fn gen_revision_id_uses_explicit_timestamp() {
let id = gen_revision_id("user@host", Some(1162500656));
let s = std::str::from_utf8(&id).unwrap();
assert!(s.starts_with("user@host-20061102205056-"));
}
#[test]
fn gen_revision_id_extracts_email_from_angle_brackets() {
for input in [
"user+joe_bar@foo-bar.com",
"<user+joe_bar@foo-bar.com>",
"Joe Bar <user+joe_bar@foo-bar.com>",
"Joe Bar <user+Joe_Bar@Foo-Bar.com>",
"Joe B\u{e5}r <user+Joe_Bar@Foo-Bar.com>",
] {
let id = gen_revision_id(input, Some(0));
let s = std::str::from_utf8(&id).unwrap();
assert!(
s.starts_with("user+joe_bar@foo-bar.com-"),
"expected email prefix for input {:?}, got {:?}",
input,
s
);
}
}
#[test]
fn gen_revision_id_falls_back_to_full_username() {
let id = gen_revision_id("Joe Bar", Some(0));
let s = std::str::from_utf8(&id).unwrap();
assert!(s.starts_with("joe_bar-"));
let id = gen_revision_id("Joe B\u{e5}r", Some(0));
let s = std::str::from_utf8(&id).unwrap();
assert!(s.starts_with("joe_br-"));
}
#[test]
fn gen_revision_id_always_returns_ascii() {
let id = gen_revision_id("Joe Bar <joe@f\u{b6}>", Some(0));
let s = std::str::from_utf8(&id).unwrap();
assert!(s.is_ascii());
assert!(s.starts_with("joe@f-"));
}
}