diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-17 14:32:37 +0800 |
| commit | 5c0ba37eda80d39e6ceca59bb1d5f4942f858995 (patch) | |
| tree | 948723f9cedf7ccb0707fa6ee516bd30fe20fd10 /server/src/util.rs | |
| download | strategy-lab-5c0ba37eda80d39e6ceca59bb1d5f4942f858995.tar.gz | |
chore: establish Strategy Lab source baseline (development, not release)
Diffstat (limited to 'server/src/util.rs')
| -rw-r--r-- | server/src/util.rs | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/server/src/util.rs b/server/src/util.rs new file mode 100644 index 0000000..5a82806 --- /dev/null +++ b/server/src/util.rs @@ -0,0 +1,138 @@ +use sha2::{Digest, Sha256}; + +pub fn sha256_hex(data: &[u8]) -> String { + let mut h = Sha256::new(); + h.update(data); + hex::encode(h.finalize()) +} + +/// Synchronous UTC timestamp helper, ISO 8601 with millisecond precision. +pub fn now_iso() -> String { + chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true) +} + +pub fn plus_hours(hours: i64) -> String { + (chrono::Utc::now() + chrono::Duration::hours(hours)) + .to_rfc3339_opts(chrono::SecondsFormat::Millis, true) +} + +pub fn new_id() -> String { + uuid::Uuid::new_v4().to_string() +} + +pub fn gen_token() -> String { + use rand::RngCore; + let mut buf = [0u8; 32]; + rand::rngs::OsRng.fill_bytes(&mut buf); + hex::encode(buf) +} + +/// Line based unified diff (full document, no hunks) used by the AI draft diff. +pub fn unified_diff(a: &str, b: &str) -> String { + let x: Vec<&str> = a.lines().collect(); + let y: Vec<&str> = b.lines().collect(); + let n = x.len(); + let m = y.len(); + // LCS table + let mut dp = vec![vec![0usize; m + 1]; n + 1]; + for i in (0..n).rev() { + for j in (0..m).rev() { + dp[i][j] = if x[i] == y[j] { + dp[i + 1][j + 1] + 1 + } else { + dp[i + 1][j].max(dp[i][j + 1]) + }; + } + } + let mut out = String::new(); + let mut ctx = std::collections::VecDeque::<usize>::new(); + let (mut i, mut j) = (0usize, 0usize); + while i < n || j < m { + if i < n && j < m && x[i] == y[j] { + ctx.push_back(i); + if ctx.len() > 2 { + let keep = ctx.pop_front().unwrap(); + out.push_str(&format!(" {}\n", x[keep])); + } + i += 1; + j += 1; + } else { + // entering a change: emit queued context lines first + for k in ctx.drain(..) { + out.push_str(&format!(" {}\n", x[k])); + } + if i < n && (j >= m || dp[i + 1][j] >= dp[i][j + 1]) { + out.push_str(&format!("-{}\n", x[i])); + i += 1; + } else { + out.push_str(&format!("+{}\n", y[j])); + j += 1; + } + } + } + out +} + +/// Deterministic canonical JSON string (recursively sorted keys) for hashing. +pub fn canonical_json(v: &serde_json::Value) -> String { + fn sort(v: &serde_json::Value) -> serde_json::Value { + match v { + serde_json::Value::Array(a) => serde_json::Value::Array(a.iter().map(sort).collect()), + serde_json::Value::Object(o) => { + let mut keys: Vec<(String, serde_json::Value)> = + o.iter().map(|(k, v)| (k.clone(), sort(v))).collect(); + keys.sort_by(|a, b| a.0.cmp(&b.0)); + serde_json::Value::Object(keys.into_iter().collect()) + } + other => other.clone(), + } + } + sort(v).to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn now_iso_is_synchronous_iso8601() { + let t = now_iso(); + let parsed = chrono::DateTime::parse_from_rfc3339(&t).expect("ISO timestamp"); + assert!(parsed.timestamp() > 1_700_000_000); + assert!(t.ends_with('Z')); + } + + #[test] + fn plus_hours_and_tokens() { + let e = chrono::DateTime::parse_from_rfc3339(&plus_hours(1)).unwrap(); + let n = chrono::DateTime::parse_from_rfc3339(&now_iso()).unwrap(); + assert!((e - n).num_minutes() >= 59); + let a = gen_token(); + assert_eq!(a.len(), 64); + assert_ne!(a, gen_token()); + assert_eq!(new_id().len(), 36); + } + + #[test] + fn sha256_is_stable() { + assert_eq!(sha256_hex(b"abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); + } + + #[test] + fn unified_diff_marks_changes_with_context() { + let a = "x\nkeep\ny"; + let b = "x\nkeep\nz"; + let d = unified_diff(a, b); + assert!(d.contains("-y\n+z\n"), "diff: {d}"); + assert!(d.contains(" keep")); + let same = unified_diff(a, "x\nkeep\ny"); + assert!(!same.contains('+') && !same.contains('-'), "no diff: {same}"); + } + + #[test] + fn canonical_json_sorts_recursively() { + let v: serde_json::Value = + serde_json::from_str(r#"{"b":1,"a":{"y":[3,2],"x":true}}"#).unwrap(); + assert_eq!(canonical_json(&v), r#"{"a":{"x":true,"y":[3,2]},"b":1}"#); + } +} |
