mkit_git_bridge/
author.rs1use crate::b64;
9use mkit_core::object::{Identity, IdentityKind};
10
11pub const BRIDGE_EMAIL: &str = "bridge@mkit.invalid";
13
14fn name_byte_ok(b: u8) -> bool {
16 !(b == b'<' || b == b'>' || b < 0x20 || b == 0x7F)
17}
18
19#[must_use]
21pub fn display_name(identity: &Identity) -> String {
22 match identity.kind {
23 IdentityKind::Ed25519 => {
24 format!("mkit:ed25519:{}", crate::gitobj::bytes_hex(&identity.bytes))
25 }
26 IdentityKind::DidKey => {
27 if identity.bytes.iter().all(|&b| name_byte_ok(b))
28 && let Ok(s) = std::str::from_utf8(&identity.bytes)
29 {
30 format!("did:key:{s}")
31 } else {
32 opaque_name(&identity.bytes)
33 }
34 }
35 IdentityKind::Opaque => {
36 if identity.bytes.iter().all(|&b| name_byte_ok(b))
37 && let Ok(s) = std::str::from_utf8(&identity.bytes)
38 {
39 s.to_owned()
40 } else {
41 opaque_name(&identity.bytes)
42 }
43 }
44 }
45}
46
47fn opaque_name(payload: &[u8]) -> String {
48 format!("mkit:opaque:{}", b64::encode(payload))
49}
50
51#[must_use]
56pub fn line(identity: &Identity, timestamp: u64) -> Vec<u8> {
57 let mut out = Vec::new();
58 out.extend_from_slice(display_name(identity).as_bytes());
59 out.extend_from_slice(b" <");
60 out.extend_from_slice(BRIDGE_EMAIL.as_bytes());
61 out.extend_from_slice(b"> ");
62 out.extend_from_slice(timestamp.to_string().as_bytes());
63 out.extend_from_slice(b" +0000");
64 out
65}
66
67#[must_use]
71pub fn parse_timestamp(line: &[u8]) -> Option<u64> {
72 let s = std::str::from_utf8(line).ok()?;
74 let rest = s.strip_suffix(" +0000")?;
75 let (_, ts) = rest.rsplit_once(' ')?;
76 ts.parse::<u64>().ok()
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn ed25519_name_is_hex() {
85 let id = Identity::ed25519([0xAA; 32]);
86 let n = display_name(&id);
87 assert!(n.starts_with("mkit:ed25519:aaaa"));
88 assert_eq!(n.len(), "mkit:ed25519:".len() + 64);
89 }
90
91 #[test]
92 fn printable_opaque_is_verbatim() {
93 let id = Identity::opaque(b"Alice Example".to_vec());
94 assert_eq!(display_name(&id), "Alice Example");
95 }
96
97 #[test]
98 fn angle_bracket_opaque_falls_back_to_base64() {
99 let id = Identity::opaque(b"Alice <alice@example.com>".to_vec());
100 let n = display_name(&id);
101 assert!(n.starts_with("mkit:opaque:"), "got {n}");
102 assert!(!n.contains('<'));
103 }
104
105 #[test]
106 fn non_utf8_opaque_falls_back_to_base64() {
107 let id = Identity::opaque(vec![0xFF, 0xFE, 0x00]);
108 assert!(display_name(&id).starts_with("mkit:opaque:"));
109 }
110
111 #[test]
112 fn line_round_trips_timestamp() {
113 let id = Identity::opaque(b"x".to_vec());
114 let l = line(&id, 1_700_000_000);
115 assert!(l.ends_with(b" +0000"));
116 assert_eq!(parse_timestamp(&l), Some(1_700_000_000));
117 }
118}