amadeus_utils/
misc.rs

1use bitvec::prelude::*;
2use eetf::convert::TryAsRef;
3use eetf::{Atom, Binary, List, Term};
4use num_traits::ToPrimitive;
5use std::collections::HashMap;
6use std::ops::{Deref, DerefMut};
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8use tracing::warn;
9
10use crate::types::{Hash, PublicKey};
11
12/// Trait for types that can provide their type name as a static string
13pub trait Typename {
14    /// Get the type name for this instance
15    /// For enums, this can return different names based on the variant
16    fn typename(&self) -> &'static str;
17}
18
19// FIXME: u32 is fine until early 2106, after that it will overflow
20pub fn get_unix_secs_now() -> u32 {
21    SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_secs).unwrap_or(0) as u32
22}
23
24pub fn get_unix_millis_now() -> u64 {
25    SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_millis).unwrap_or(0) as u64
26}
27
28pub fn get_unix_nanos_now() -> u128 {
29    SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_nanos).unwrap_or(0)
30}
31
32/// DEPRECATED: This function incorrectly uses base58 encoding
33/// Elixir uses raw binary pubkeys in keys, not base58
34/// Use bcat() instead for building binary keys
35#[deprecated(note = "Use bcat() for raw binary keys instead of base58")]
36pub fn pk_hex(pk: &[u8]) -> String {
37    bs58::encode(pk).into_string()
38}
39
40/// Decode base58 string to fixed-size byte array
41/// Returns None if decoding fails or size doesn't match
42pub fn decode_base58_array<const N: usize>(s: &str) -> Option<[u8; N]> {
43    bs58::decode(s).into_vec().ok().and_then(|bytes| bytes.try_into().ok())
44}
45
46/// Decode base58 string to 48-byte public key
47pub fn decode_base58_pk(s: &str) -> Option<PublicKey> {
48    decode_base58_array::<48>(s).map(PublicKey::from)
49}
50
51/// Decode base58 string to 32-byte hash
52pub fn decode_base58_hash(s: &str) -> Option<Hash> {
53    decode_base58_array::<32>(s).map(Hash::from)
54}
55
56/// Concatenate multiple byte slices into a single Vec<u8>
57/// Example: bcat(&[b"bic:coin:balance:", pk, b":AMA"])
58#[inline]
59pub fn bcat(slices: &[&[u8]]) -> Vec<u8> {
60    slices.iter().flat_map(|&s| s).copied().collect()
61}
62
63/// Produce a hex dump similar to `hexdump -C` for a binary slice.
64pub fn hexdump(data: &[u8]) -> String {
65    let mut out = String::new();
66    for (i, chunk) in data.chunks(16).enumerate() {
67        let address = i * 16;
68        // 8-digit upper-case hex address
69        let offset_str = format!("{address:08X}");
70
71        // hex bytes (2 hex chars per byte + 1 space => up to 48 chars)
72        let mut hex_bytes = String::new();
73        for b in chunk {
74            hex_bytes.push_str(&format!("{:02X} ", b));
75        }
76        // pad to 48 characters to align ASCII column
77        while hex_bytes.len() < 48 {
78            hex_bytes.push(' ');
79        }
80
81        // ASCII representation (32..=126 printable)
82        let ascii: String = chunk.iter().map(|&b| if (32..=126).contains(&b) { b as char } else { '.' }).collect();
83
84        out.push_str(&format!("{offset_str}  {hex_bytes}  {ascii}\n"));
85    }
86    if out.ends_with('\n') {
87        out.pop();
88    }
89    out
90}
91
92/// Keep only ASCII characters considered printable for our use-case.
93pub fn ascii(input: &str) -> String {
94    input
95        .chars()
96        .filter(|&c| {
97            let code = c as u32;
98            code == 32
99                || (123..=126).contains(&code)
100                || (('!' as u32)..=('@' as u32)).contains(&code)
101                || (('[' as u32)..=('_' as u32)).contains(&code)
102                || (('0' as u32)..=('9' as u32)).contains(&code)
103                || (('A' as u32)..=('Z' as u32)).contains(&code)
104                || (('a' as u32)..=('z' as u32)).contains(&code)
105        })
106        .collect()
107}
108
109pub fn is_ascii_clean(input: &str) -> bool {
110    ascii(input) == input
111}
112
113pub fn alphanumeric(input: &str) -> String {
114    input.chars().filter(|c| c.is_ascii_alphanumeric()).collect()
115}
116
117pub fn is_alphanumeric(input: &str) -> bool {
118    alphanumeric(input) == input
119}
120
121/// Trim trailing slash from url
122pub fn url(url: &str) -> String {
123    url.trim_end_matches('/').to_string()
124}
125
126/// Trim trailing slash on base and append path verbatim
127pub fn url_with(url: &str, path: &str) -> String {
128    format!("{}{}", url, path)
129}
130
131/// **DEPRECATED**: Lightweight helpers for ETF Term manipulation (legacy)
132/// Use `vecpak::VecpakExt` trait instead for the primary vecpak format.
133/// This trait is kept for backwards compatibility with legacy ETF code only.
134pub trait TermExt {
135    fn as_atom(&self) -> Option<&Atom>;
136    fn get_integer(&self) -> Option<i128>;
137    fn get_binary(&self) -> Option<&[u8]>;
138    fn get_list(&self) -> Option<&[Term]>;
139    fn get_string(&self) -> Option<String>;
140    fn get_term_map(&self) -> Option<TermMap>;
141    fn parse_list<T, E>(&self, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
142    where
143        E: std::fmt::Display;
144}
145
146impl TermExt for Term {
147    fn as_atom(&self) -> Option<&Atom> {
148        TryAsRef::<Atom>::try_as_ref(self)
149    }
150
151    fn get_integer(&self) -> Option<i128> {
152        match self {
153            Term::FixInteger(i) => Some(i.value as i128),
154            Term::BigInteger(bi) => bi.value.to_i128(),
155            _ => None,
156        }
157    }
158
159    fn get_binary(&self) -> Option<&[u8]> {
160        TryAsRef::<Binary>::try_as_ref(self).map(|b| b.bytes.as_slice())
161    }
162
163    fn get_list(&self) -> Option<&[Term]> {
164        TryAsRef::<List>::try_as_ref(self).map(|l| l.elements.as_slice())
165    }
166
167    fn get_string(&self) -> Option<String> {
168        // Erlang strings come across either as ByteList or Binary
169        if let Term::ByteList(bl) = self {
170            std::str::from_utf8(&bl.bytes).ok().map(|s| s.to_owned())
171        } else if let Term::Binary(b) = self {
172            std::str::from_utf8(&b.bytes).ok().map(|s| s.to_owned())
173        } else if let Term::Atom(a) = self {
174            Some(a.name.clone())
175        } else {
176            None
177        }
178    }
179
180    fn get_term_map(&self) -> Option<TermMap> {
181        match self {
182            Term::Map(m) => Some(TermMap(m.map.clone())),
183            _ => None,
184        }
185    }
186
187    fn parse_list<T, E>(&self, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
188    where
189        E: std::fmt::Display,
190    {
191        self.get_list().map(|list| parse_list(list, parser)).unwrap_or_default()
192    }
193}
194
195/// **DEPRECATED**: ETF Term-based map wrapper (legacy)
196/// Use `vecpak::PropListMap` instead for the primary vecpak format.
197/// This struct is kept for backwards compatibility with legacy ETF code only.
198#[derive(Default, Clone, Debug)]
199pub struct TermMap(pub HashMap<Term, Term>);
200
201impl Deref for TermMap {
202    type Target = HashMap<Term, Term>;
203    fn deref(&self) -> &Self::Target {
204        &self.0
205    }
206}
207
208impl DerefMut for TermMap {
209    fn deref_mut(&mut self) -> &mut Self::Target {
210        &mut self.0
211    }
212}
213
214impl TermMap {
215    pub fn get_term_map(&self, key: &str) -> Option<Self> {
216        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_term_map)
217    }
218
219    pub fn get_binary<'a, A>(&'a self, key: &str) -> Option<A>
220    where
221        A: TryFrom<&'a [u8]>,
222    {
223        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_binary).and_then(|b| A::try_from(b).ok())
224    }
225
226    pub fn get_integer<I>(&self, key: &str) -> Option<I>
227    where
228        I: TryFrom<i128>,
229    {
230        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_integer).and_then(|b| I::try_from(b).ok())
231    }
232
233    pub fn get_list(&self, key: &str) -> Option<&[Term]> {
234        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_list)
235    }
236
237    pub fn get_atom(&self, key: &str) -> Option<&Atom> {
238        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::as_atom)
239    }
240
241    pub fn get_string(&self, key: &str) -> Option<String> {
242        self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_string)
243    }
244
245    pub fn parse_list<T, E>(&self, key: &str, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
246    where
247        E: std::fmt::Display,
248    {
249        self.0.get(&Term::Atom(Atom::from(key))).map(|term| term.parse_list(parser)).unwrap_or_default()
250    }
251
252    pub fn into_term(self) -> Term {
253        Term::Map(eetf::Map { map: self.0 })
254    }
255}
256
257/// **DEPRECATED**: Encode a list of binary values as ETF term
258/// Use `list_of_binaries_to_vecpak` instead for new code.
259#[deprecated(since = "0.1.0", note = "Use list_of_binaries_to_vecpak instead")]
260pub fn eetf_list_of_binaries(list_of_binaries: Vec<Vec<u8>>) -> Result<Vec<u8>, eetf::EncodeError> {
261    let elements: Vec<Term> = list_of_binaries.into_iter().map(|bytes| Term::from(Binary { bytes })).collect();
262    let term = Term::from(List::from(elements));
263    let mut out = Vec::new();
264    term.encode(&mut out)?;
265    Ok(out)
266}
267
268pub fn list_of_binaries_to_vecpak(list_of_binaries: Vec<Vec<u8>>) -> Vec<u8> {
269    use crate::vecpak::{Term as VecpakTerm, encode};
270    let elements: Vec<VecpakTerm> = list_of_binaries.into_iter().map(VecpakTerm::Binary).collect();
271    encode(VecpakTerm::List(elements))
272}
273
274pub fn bitvec_to_bin(mask: &BitVec<u8, Msb0>) -> Vec<u8> {
275    mask.as_raw_slice().to_vec()
276}
277
278pub fn bin_to_bitvec(bytes: Vec<u8>) -> BitVec<u8, Msb0> {
279    BitVec::from_vec(bytes)
280}
281
282/// Calculate percentage of true bits in a mask relative to total count
283pub fn get_bits_percentage(mask: &BitVec<u8, Msb0>, total_count: usize) -> f64 {
284    if total_count == 0 {
285        return 0.0;
286    }
287    let true_bits = mask.count_ones();
288    (true_bits as f64) / (total_count as f64)
289}
290// fn bitvec_to_bools(bytes: &[u8]) -> Vec<bool> {
291//     let mut out = Vec::with_capacity(bytes.len() * 8);
292//     for (_, byte) in bytes.iter().enumerate() {
293//         for bit in 0..8 {
294//             let val = (byte >> (7 - bit)) & 1u8;
295//             out.push(val == 1u8);
296//         }
297//     }
298//     out
299// }
300
301/// Creates string representation as bytes, compatible with Erlang's :erlang.integer_to_binary/1
302
303/// Format a duration into human-readable form following the requirements:
304/// - seconds if less than a minute
305/// - minutes plus seconds if less than hour
306/// - hours and minutes if less than day
307/// - days and hours if less than month
308/// - months and days if less than year
309/// - years, months and days if bigger than year
310pub fn format_duration(total_seconds: u32) -> String {
311    if total_seconds < 60 {
312        return format!("{}s", total_seconds);
313    }
314
315    let minutes = total_seconds / 60;
316    let seconds = total_seconds % 60;
317
318    if minutes < 60 {
319        return format!("{}m {}s", minutes, seconds);
320    }
321
322    let hours = minutes / 60;
323    let minutes = minutes % 60;
324
325    if hours < 24 {
326        return format!("{}h {}m", hours, minutes);
327    }
328
329    let days = hours / 24;
330    let hours = hours % 24;
331
332    if days < 30 {
333        return format!("{}d {}h", days, hours);
334    }
335
336    let months = days / 30; // Approximate months as 30 days
337    let days = days % 30;
338
339    if months < 12 {
340        return format!("{}mo {}d", months, days);
341    }
342
343    let years = months / 12;
344    let months = months % 12;
345
346    format!("{}y {}mo {}d", years, months, days)
347}
348
349/// Parse a list of ETF terms into structured data using the provided parser
350pub fn parse_list<T, E>(list: &[Term], parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
351where
352    E: std::fmt::Display,
353{
354    list.iter()
355        .filter_map(|term| {
356            term.get_binary().and_then(|bytes| parser(bytes).map_err(|e| warn!("Failed to parse item: {}", e)).ok())
357        })
358        .collect()
359}
360
361/// Serialize a list of items into an ETF List term using the provided serializer
362pub fn serialize_list<T, E>(items: &[T], serializer: impl Fn(&T) -> Result<Vec<u8>, E>) -> Option<Term>
363where
364    E: std::fmt::Display,
365{
366    let terms: Result<Vec<_>, _> =
367        items.iter().map(|item| serializer(item).map(|bytes| Term::Binary(Binary::from(bytes)))).collect();
368    terms.ok().map(|terms| Term::List(List::from(terms)))
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn hexdump_basic() {
377        let s = hexdump(&[0x41, 0x00, 0x7F]);
378        assert!(s.starts_with("00000000  "));
379        assert!(s.contains("41 00 7F"));
380        assert!(s.ends_with("A.."));
381    }
382
383    #[test]
384    fn string_helpers() {
385        assert!(is_ascii_clean("AZaz09_-!"));
386        assert!(!is_ascii_clean("hi🙂"));
387        assert_eq!(alphanumeric("Abc-123"), "Abc123");
388        assert!(is_alphanumeric("abc123"));
389        assert!(!is_alphanumeric("a_b"));
390    }
391
392    #[test]
393    fn ext_and_urls() {
394        assert_eq!(url("http://a/b/"), "http://a/b");
395        assert_eq!(url("http://a/b"), "http://a/b");
396        assert_eq!(url_with("http://a/b", "/c"), "http://a/b/c");
397    }
398
399    #[test]
400    fn bitvec_roundtrip_prefix() {
401        let mut mask = BitVec::<u8, Msb0>::new();
402        mask.extend([true, false, true, true, false, false, false, true, true]);
403        let bytes = bitvec_to_bin(&mask);
404        assert_eq!(bytes.len(), 2);
405        let bools = bin_to_bitvec(bytes.clone());
406        assert_eq!(&bools[..mask.len()], &mask[..]);
407        for i in mask.len()..8 * bytes.len() {
408            assert!(!bools[i]);
409        }
410    }
411
412    #[test]
413    fn bcat_concatenates_slices() {
414        let pk = [0x01, 0x02, 0x03];
415        let result = bcat(&[b"prefix:", &pk, b":suffix"]);
416        assert_eq!(result, b"prefix:\x01\x02\x03:suffix");
417    }
418
419    #[test]
420    fn bcat_empty() {
421        let result = bcat(&[]);
422        assert_eq!(result, b"");
423    }
424
425    #[test]
426    fn bcat_builds_keys() {
427        let pk = [0xAA, 0xBB, 0xCC];
428        let result = bcat(&[b"bic:coin:balance:", &pk, b":AMA"]);
429        assert_eq!(result, b"bic:coin:balance:\xAA\xBB\xCC:AMA");
430    }
431
432    #[test]
433    fn test_decode_base58_pk() {
434        // Test valid 48-byte public key
435        let test_pk = PublicKey::from([0u8; 48]);
436        let encoded = bs58::encode(&test_pk).into_string();
437        let decoded = decode_base58_pk(&encoded);
438        assert_eq!(decoded, Some(test_pk));
439
440        // Test invalid base58
441        assert_eq!(decode_base58_pk("not-valid-base58!"), None);
442
443        // Test wrong size (32 bytes instead of 48)
444        let wrong_size = [0u8; 32];
445        let encoded_wrong = bs58::encode(&wrong_size).into_string();
446        assert_eq!(decode_base58_pk(&encoded_wrong), None);
447    }
448
449    #[test]
450    fn test_decode_base58_hash() {
451        // Test valid 32-byte hash
452        let test_hash = Hash::from([0xFF; 32]);
453        let encoded = bs58::encode(&test_hash).into_string();
454        let decoded = decode_base58_hash(&encoded);
455        assert_eq!(decoded, Some(test_hash));
456
457        // Test wrong size (48 bytes instead of 32)
458        let wrong_size = [0u8; 48];
459        let encoded_wrong = bs58::encode(&wrong_size).into_string();
460        assert_eq!(decode_base58_hash(&encoded_wrong), None);
461    }
462
463    #[test]
464    fn test_decode_base58_array() {
465        // Test arbitrary size array
466        let test_bytes: [u8; 16] = [0x12; 16];
467        let encoded = bs58::encode(&test_bytes).into_string();
468        let decoded: Option<[u8; 16]> = decode_base58_array(&encoded);
469        assert_eq!(decoded, Some(test_bytes));
470    }
471}