use indicatrix_vault::model::design_key::{catalogue_design_uuid, normalize_design_uuid};
use std::{
path::Path,
sync::atomic::{AtomicU64, Ordering},
time::{SystemTime, UNIX_EPOCH},
};
pub(in crate::gui::editor) fn fresh_design_uuid() -> String {
use std::{
fmt::Write as _,
hash::{BuildHasher, Hasher, RandomState},
};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_nanos() as u64);
let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
let mut bytes = [0u8; 16];
for (half, chunk) in bytes.chunks_mut(8).enumerate() {
let mut hasher = RandomState::new().build_hasher();
hasher.write_u64(nanos);
hasher.write_u32(std::process::id());
hasher.write_u64(counter);
hasher.write_usize(half);
chunk.copy_from_slice(&hasher.finish().to_be_bytes());
}
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
let hex = bytes.iter().fold(String::new(), |mut acc, b| {
let _ = write!(acc, "{b:02x}");
acc
});
format!(
"{}-{}-{}-{}-{}",
&hex[0..8],
&hex[8..12],
&hex[12..16],
&hex[16..20],
&hex[20..32]
)
}
pub(in crate::gui::editor) fn resolve_design_uuid(
stored_id: &str,
catalogue_url: Option<&str>,
fresh: impl FnOnce() -> String,
) -> String {
if normalize_design_uuid(stored_id).is_some() {
return stored_id.to_string();
}
catalogue_url
.filter(|url| !url.trim().is_empty())
.map_or_else(fresh, catalogue_design_uuid)
}
pub(in crate::gui::editor) fn resolve_file_design_uuid(
stored_id: &str,
path: &Path,
fresh: impl FnOnce() -> String,
) -> String {
if normalize_design_uuid(stored_id).is_some() {
return stored_id.to_string();
}
resolve_design_uuid(stored_id, Some(&file_design_url(path)), fresh)
}
pub(in crate::gui::editor) fn file_design_url(path: &Path) -> String {
let resolved = std::fs::canonicalize(path)
.or_else(|_| std::path::absolute(path))
.unwrap_or_else(|_| path.to_path_buf());
file_url_from_path_text(&resolved.to_string_lossy())
}
fn file_url_from_path_text(text: &str) -> String {
use std::fmt::Write as _;
let slashed = text.replace('\\', "/");
let plain = slashed.strip_prefix("//?/UNC/").map_or_else(
|| slashed.strip_prefix("//?/").unwrap_or(&slashed).to_string(),
|rest| format!("//{rest}"),
);
let mut encoded = String::with_capacity(plain.len() + 8);
for byte in plain.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/' | b':') {
encoded.push(char::from(byte));
} else {
let _ = write!(encoded, "%{byte:02X}");
}
}
if encoded.starts_with("//") {
format!("file:{encoded}")
} else if encoded.starts_with('/') {
format!("file://{encoded}")
} else {
format!("file:///{encoded}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use indicatrix_formats::native::design::is_uuid;
const FILE_ID: &str = "0b9e6a4e-5f1d-4c7a-9a52-1e3f7d8c2b10";
const fn never() -> String {
panic!("a fresh id must not be made when one is already decided");
}
#[test]
fn a_file_id_is_kept_exactly_as_written() {
assert_eq!(resolve_design_uuid(FILE_ID, None, never), FILE_ID);
let upper = FILE_ID.to_ascii_uppercase();
assert_eq!(
resolve_design_uuid(&upper, None, never),
upper,
"opening and saving must not rewrite the file's id"
);
}
#[test]
fn a_file_id_beats_the_catalogue_entry_it_was_loaded_through() {
assert_eq!(
resolve_design_uuid(FILE_ID, Some("local://capps.indicatrix"), never),
FILE_ID
);
}
#[test]
fn a_design_without_an_id_gets_a_fresh_one() {
let made = resolve_design_uuid("", None, || "made-here".to_string());
assert_eq!(made, "made-here");
let real = resolve_design_uuid("", None, fresh_design_uuid);
assert!(is_uuid(&real), "{real}");
}
#[test]
fn a_stored_id_that_is_not_a_uuid_is_replaced_not_trusted() {
assert_eq!(
resolve_design_uuid("not-a-uuid", None, || "fresh".to_string()),
"fresh"
);
assert_eq!(
resolve_design_uuid(" ", None, || "fresh".to_string()),
"fresh"
);
}
#[test]
fn a_catalogue_design_without_an_id_gets_the_entrys_deterministic_uuid() {
let url = "local://capps-brilliant.asc";
let first = resolve_design_uuid("", Some(url), never);
let again = resolve_design_uuid("", Some(url), never);
assert_eq!(first, again, "reopening the same entry must find its data");
assert_eq!(first, catalogue_design_uuid(url));
assert!(is_uuid(&first), "{first}");
assert_ne!(
first,
resolve_design_uuid("", Some("local://other.asc"), never)
);
}
#[test]
fn a_blank_catalogue_url_counts_as_no_catalogue_entry() {
assert_eq!(
resolve_design_uuid("", Some(" "), || "fresh".to_string()),
"fresh"
);
}
#[test]
fn fresh_ids_are_valid_version_4_uuids_and_differ() {
let first = fresh_design_uuid();
let second = fresh_design_uuid();
assert!(is_uuid(&first), "{first}");
assert_eq!(&first[14..15], "4", "version nibble");
assert!(
matches!(&first[19..20], "8" | "9" | "a" | "b"),
"variant nibble in {first}"
);
assert_ne!(first, second);
}
#[test]
fn a_windows_path_becomes_an_encoded_file_url() {
assert_eq!(
file_url_from_path_text(r"C:\Users\Anna\My Designs\ring.asc"),
"file:///C:/Users/Anna/My%20Designs/ring.asc"
);
assert_eq!(
file_url_from_path_text(r"C:\Müller\ring.asc"),
"file:///C:/M%C3%BCller/ring.asc",
"accents are percent-encoded byte by byte"
);
}
#[test]
fn the_windows_verbatim_prefixes_are_dropped() {
assert_eq!(
file_url_from_path_text(r"\\?\C:\Users\Anna\ring.asc"),
file_url_from_path_text(r"C:\Users\Anna\ring.asc"),
"canonicalize adds the prefix; it must not change the key"
);
assert_eq!(
file_url_from_path_text(r"\\?\UNC\workshop\designs\ring.asc"),
"file://workshop/designs/ring.asc"
);
assert_eq!(
file_url_from_path_text(r"\\workshop\designs\ring.asc"),
"file://workshop/designs/ring.asc"
);
}
#[test]
fn a_unix_path_becomes_a_three_slash_file_url() {
assert_eq!(
file_url_from_path_text("/home/anna/designs/ring.asc"),
"file:///home/anna/designs/ring.asc"
);
}
#[test]
fn a_design_file_without_an_id_gets_its_locations_deterministic_uuid() {
let path = Path::new("a-design-that-does-not-exist-here/ring.asc");
let first = resolve_file_design_uuid("", path, never);
let again = resolve_file_design_uuid("", path, never);
assert_eq!(
first, again,
"opening the same file again must find its data"
);
assert!(is_uuid(&first), "{first}");
assert_eq!(&first[14..15], "5", "a name-based UUID, not a random one");
assert_eq!(first, catalogue_design_uuid(&file_design_url(path)));
assert_ne!(
first,
resolve_file_design_uuid("", Path::new("another-folder/ring.asc"), never),
"a different file is a different design"
);
}
#[test]
fn a_file_id_beats_the_file_location() {
let path = Path::new("somewhere/ring.indicatrix");
assert_eq!(resolve_file_design_uuid(FILE_ID, path, never), FILE_ID);
let upper = FILE_ID.to_ascii_uppercase();
assert_eq!(
resolve_file_design_uuid(&upper, path, never),
upper,
"opening and saving must not rewrite the file's id"
);
}
#[test]
fn a_stored_id_that_is_not_a_uuid_falls_back_to_the_file_location() {
let path = Path::new("somewhere/ring.asc");
assert_eq!(
resolve_file_design_uuid("not-a-uuid", path, never),
resolve_file_design_uuid("", path, never)
);
}
#[test]
fn two_spellings_of_one_existing_file_give_one_uuid() {
let folder = std::env::temp_dir().join(format!(
"indicatrix-cut-design-identity-{}",
std::process::id()
));
std::fs::create_dir_all(folder.join("inner")).expect("create the test folder");
let file = folder.join("ring.asc");
std::fs::write(&file, "a design").expect("write the test file");
let direct = resolve_file_design_uuid("", &file, never);
let roundabout_path = folder.join("inner").join("..").join("ring.asc");
let roundabout = resolve_file_design_uuid("", &roundabout_path, never);
let _ = std::fs::remove_dir_all(&folder);
assert_eq!(
direct, roundabout,
"the same file reached through `..` is the same design"
);
assert!(is_uuid(&direct), "{direct}");
}
}