1use mkit_core::hash::Hash;
6
7#[must_use]
10pub fn hex_hash(h: &Hash) -> String {
11 mkit_core::hash::to_hex(h)
12}
13
14#[must_use]
16pub fn short_hash(h: &Hash, n: usize) -> String {
17 let full = hex_hash(h);
18 let take = n.clamp(4, 64);
19 full[..take].to_owned()
20}
21
22static HEX_ALPHABET: &[u8; 16] = b"0123456789abcdef";
23
24#[must_use]
27pub fn short_identity(id: &mkit_core::Identity) -> String {
28 match id.kind {
29 mkit_core::IdentityKind::Opaque if id.bytes.len() == 8 => {
30 let mut arr = [0u8; 8];
31 arr.copy_from_slice(&id.bytes);
32 u64::from_le_bytes(arr).to_string()
33 }
34 mkit_core::IdentityKind::DidKey => {
37 let s = String::from_utf8_lossy(&id.bytes);
38 let prefix: String = s.chars().take(8).collect();
39 format!("did:key:{prefix}")
40 }
41 mkit_core::IdentityKind::Opaque if printable_text(&id.bytes).is_some() => {
45 printable_text(&id.bytes).unwrap_or_default().to_owned()
46 }
47 kind => {
48 let kind_name = match kind {
49 mkit_core::IdentityKind::Ed25519 => "ed25519",
50 mkit_core::IdentityKind::DidKey => "did:key",
51 mkit_core::IdentityKind::Opaque => "opaque",
52 };
53 let take = id.bytes.len().min(4);
54 let mut hex = String::with_capacity(take * 2);
55 for b in &id.bytes[..take] {
56 hex.push(HEX_ALPHABET[(b >> 4) as usize] as char);
57 hex.push(HEX_ALPHABET[(b & 0x0F) as usize] as char);
58 }
59 format!("{kind_name}:{hex}")
60 }
61 }
62}
63
64fn printable_text(bytes: &[u8]) -> Option<&str> {
67 let s = std::str::from_utf8(bytes).ok()?;
68 (!s.is_empty() && !s.chars().any(char::is_control)).then_some(s)
69}
70
71#[must_use]
80pub fn full_identity(id: &mkit_core::Identity) -> String {
81 match id.kind {
82 mkit_core::IdentityKind::Opaque if id.bytes.len() == 8 => {
83 let mut arr = [0u8; 8];
84 arr.copy_from_slice(&id.bytes);
85 format!("mid:{}", u64::from_le_bytes(arr))
86 }
87 mkit_core::IdentityKind::Ed25519 => format!("ed25519:{}", to_hex(&id.bytes)),
88 mkit_core::IdentityKind::DidKey => {
91 format!("did:key:{}", String::from_utf8_lossy(&id.bytes))
92 }
93 mkit_core::IdentityKind::Opaque => format!("opaque:{}", to_hex(&id.bytes)),
94 }
95}
96
97#[must_use]
103pub fn json_escape(s: &str) -> String {
104 let mut out = String::with_capacity(s.len() + 2);
105 for c in s.chars() {
106 match c {
107 '"' => out.push_str("\\\""),
108 '\\' => out.push_str("\\\\"),
109 '\n' => out.push_str("\\n"),
110 '\r' => out.push_str("\\r"),
111 '\t' => out.push_str("\\t"),
112 '\x08' => out.push_str("\\b"),
113 '\x0c' => out.push_str("\\f"),
114 c if (c as u32) < 0x20 => {
115 use std::fmt::Write as _;
116 let _ = write!(out, "\\u{:04x}", c as u32);
117 }
118 c => out.push(c),
119 }
120 }
121 out
122}
123
124#[must_use]
135pub fn human_date_utc(secs: u64) -> String {
136 let days = i64::try_from(secs / 86_400).unwrap_or(i64::MAX);
137 let rem = secs % 86_400;
138 let hour = rem / 3_600;
139 let minute = (rem % 3_600) / 60;
140 let second = rem % 60;
141
142 let z = days + 719_468;
144 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
145 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
148 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if month <= 2 { y + 1 } else { y };
153
154 format!("{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02} +0000")
155}
156
157fn to_hex(bytes: &[u8]) -> String {
158 let mut out = String::with_capacity(bytes.len() * 2);
159 for b in bytes {
160 out.push(HEX_ALPHABET[(b >> 4) as usize] as char);
161 out.push(HEX_ALPHABET[(b & 0x0F) as usize] as char);
162 }
163 out
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use mkit_core::hash;
170
171 #[test]
172 fn hex_hash_is_64_chars() {
173 let h = hash::hash(b"hello");
174 assert_eq!(hex_hash(&h).len(), 64);
175 assert!(hex_hash(&h).chars().all(|c| c.is_ascii_hexdigit()));
176 }
177
178 #[test]
179 fn short_hash_clamps() {
180 let h = hash::hash(b"x");
181 assert_eq!(short_hash(&h, 0).len(), 4);
182 assert_eq!(short_hash(&h, 8).len(), 8);
183 assert_eq!(short_hash(&h, 999).len(), 64);
184 }
185
186 #[test]
187 fn json_escape_basic() {
188 assert_eq!(json_escape("hello"), "hello");
189 assert_eq!(json_escape("a\"b"), "a\\\"b");
190 assert_eq!(json_escape("a\\b"), "a\\\\b");
191 assert_eq!(json_escape("a\nb"), "a\\nb");
192 assert_eq!(json_escape("a\tb"), "a\\tb");
193 }
194
195 #[test]
196 fn json_escape_control_chars() {
197 assert_eq!(json_escape("\x01"), "\\u0001");
199 assert_eq!(json_escape("\x7f"), "\x7f");
201 }
202
203 #[test]
204 fn human_date_utc_epoch() {
205 assert_eq!(human_date_utc(0), "1970-01-01 00:00:00 +0000");
206 }
207
208 #[test]
209 fn human_date_utc_known_instant() {
210 assert_eq!(human_date_utc(1_700_000_000), "2023-11-14 22:13:20 +0000");
212 }
213
214 #[test]
215 fn human_date_utc_leap_day() {
216 assert_eq!(human_date_utc(1_582_934_400), "2020-02-29 00:00:00 +0000");
218 }
219
220 #[test]
221 fn full_identity_mid() {
222 let id = mkit_core::Identity {
223 kind: mkit_core::IdentityKind::Opaque,
224 bytes: 42u64.to_le_bytes().to_vec(),
225 };
226 assert_eq!(full_identity(&id), "mid:42");
227 }
228
229 #[test]
230 fn full_identity_ed25519() {
231 let id = mkit_core::Identity {
232 kind: mkit_core::IdentityKind::Ed25519,
233 bytes: vec![0xab; 32],
234 };
235 let s = full_identity(&id);
236 assert!(s.starts_with("ed25519:"));
237 assert_eq!(s.len(), "ed25519:".len() + 64);
238 }
239}