use std::fs;
use std::path::Path;
use hmac::{Hmac, Mac};
use loftspec::identity::Key;
use loftspec_store::repository::GitCommand;
use serde_json::{Value, json};
use sha2::Sha256;
use super::event::Checkout;
use crate::settings;
const LOCAL: &str = ".loft";
const FILE: &str = "telemetry.json";
pub fn installation(kept: &mut Value) -> (String, bool) {
if let Some(token) = kept["installation"].as_str().filter(|token| valid(token)) {
return (token.to_owned(), false);
}
let token = random_token();
kept["installation"] = json!(token);
(token, true)
}
pub fn checkout(root: &Path) -> Option<Checkout> {
if !root.join("spec").is_dir() {
return None;
}
let kept = settings::read_at(&root.join(LOCAL).join(FILE));
let project = kept["project"].as_str().filter(|token| valid(token));
let secret = kept["branchSecret"]
.as_str()
.filter(|secret| secret.len() == 64 && secret.bytes().all(|b| b.is_ascii_hexdigit()));
let (project, secret) = if let (Some(project), Some(secret)) = (project, secret) {
(project.to_owned(), secret.to_owned())
} else {
let (project, secret) = (random_token(), random_secret());
keep(root, &json!({ "project": project, "branchSecret": secret }))?;
(project, secret)
};
let branch = GitCommand::default()
.branch(root)
.ok()
.flatten()
.map(|name| keyed(&secret, &name));
Some(Checkout { project, branch })
}
fn keep(root: &Path, tokens: &Value) -> Option<()> {
let local = root.join(LOCAL);
fs::create_dir_all(&local).ok()?;
let ignore = local.join(".gitignore");
if !ignore.exists() {
fs::write(&ignore, "*\n").ok()?;
}
settings::write_at(&local.join(FILE), tokens).ok()
}
fn keyed(secret: &str, name: &str) -> String {
let mut mac =
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC takes a key of any length");
mac.update(name.as_bytes());
format!("{:.16x}", mac.finalize().into_bytes())
}
fn random_token() -> String {
uuid::Uuid::new_v4().to_string()
}
fn random_secret() -> String {
format!(
"{}{}",
uuid::Uuid::new_v4().simple(),
uuid::Uuid::new_v4().simple()
)
}
fn valid(token: &str) -> bool {
Key::parse(token).is_ok_and(|key| key.version() == 4)
}
#[cfg(test)]
mod tests {
use super::keyed;
#[test]
fn a_branch_token_is_16_hex_digits_that_depend_on_the_secret_and_the_name() {
let secret = "0".repeat(64);
let token = keyed(&secret, "main");
assert_eq!(token.len(), 16);
assert!(token.bytes().all(|b| b.is_ascii_hexdigit()));
assert_eq!(keyed(&secret, "main"), token);
assert_ne!(keyed(&secret, "develop"), token);
assert_ne!(keyed(&"1".repeat(64), "main"), token);
}
}