amadeus_node/utils/
misc.rs1use eetf::convert::TryAsRef;
2use eetf::{Atom, Binary, List, Term};
3use num_traits::ToPrimitive;
4use std::collections::HashMap;
5use std::ops::{Deref, DerefMut};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7use tracing::warn;
8
9pub trait Typename {
11 fn typename(&self) -> &'static str;
14}
15
16pub fn get_unix_secs_now() -> u32 {
18 SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_secs).unwrap_or(0) as u32
19}
20
21pub fn get_unix_millis_now() -> u64 {
22 SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_millis).unwrap_or(0) as u64
23}
24
25pub fn get_unix_nanos_now() -> u128 {
26 SystemTime::now().duration_since(UNIX_EPOCH).as_ref().map(Duration::as_nanos).unwrap_or(0)
27}
28
29pub fn pk_hex(pk: &[u8]) -> String {
30 let mut s = String::with_capacity(pk.len() * 2);
31 for b in pk {
32 s.push_str(&format!("{:02x}", b));
33 }
34 s
35}
36
37pub fn hexdump(data: &[u8]) -> String {
39 let mut out = String::new();
40 for (i, chunk) in data.chunks(16).enumerate() {
41 let address = i * 16;
42 let offset_str = format!("{address:08X}");
44
45 let mut hex_bytes = String::new();
47 for b in chunk {
48 hex_bytes.push_str(&format!("{:02X} ", b));
49 }
50 while hex_bytes.len() < 48 {
52 hex_bytes.push(' ');
53 }
54
55 let ascii: String = chunk.iter().map(|&b| if (32..=126).contains(&b) { b as char } else { '.' }).collect();
57
58 out.push_str(&format!("{offset_str} {hex_bytes} {ascii}\n"));
59 }
60 if out.ends_with('\n') {
61 out.pop();
62 }
63 out
64}
65
66pub fn ascii(input: &str) -> String {
68 input
69 .chars()
70 .filter(|&c| {
71 let code = c as u32;
72 code == 32
73 || (123..=126).contains(&code)
74 || (('!' as u32)..=('@' as u32)).contains(&code)
75 || (('[' as u32)..=('_' as u32)).contains(&code)
76 || (('0' as u32)..=('9' as u32)).contains(&code)
77 || (('A' as u32)..=('Z' as u32)).contains(&code)
78 || (('a' as u32)..=('z' as u32)).contains(&code)
79 })
80 .collect()
81}
82
83pub fn is_ascii_clean(input: &str) -> bool {
84 ascii(input) == input
85}
86
87pub fn alphanumeric(input: &str) -> String {
88 input.chars().filter(|c| c.is_ascii_alphanumeric()).collect()
89}
90
91pub fn is_alphanumeric(input: &str) -> bool {
92 alphanumeric(input) == input
93}
94
95pub fn url(url: &str) -> String {
97 url.trim_end_matches('/').to_string()
98}
99
100pub fn url_with(url: &str, path: &str) -> String {
102 format!("{}{}", url, path)
103}
104
105pub trait TermExt {
107 fn as_atom(&self) -> Option<&Atom>;
108 fn get_integer(&self) -> Option<i128>;
109 fn get_binary(&self) -> Option<&[u8]>;
110 fn get_list(&self) -> Option<&[Term]>;
111 fn get_string(&self) -> Option<String>;
112 fn get_term_map(&self) -> Option<TermMap>;
113 fn parse_list<T, E>(&self, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
114 where
115 E: std::fmt::Display;
116}
117
118impl TermExt for Term {
119 fn as_atom(&self) -> Option<&Atom> {
120 TryAsRef::<Atom>::try_as_ref(self)
121 }
122
123 fn get_integer(&self) -> Option<i128> {
124 match self {
125 Term::FixInteger(i) => Some(i.value as i128),
126 Term::BigInteger(bi) => bi.value.to_i128(),
127 _ => None,
128 }
129 }
130
131 fn get_binary(&self) -> Option<&[u8]> {
132 TryAsRef::<Binary>::try_as_ref(self).map(|b| b.bytes.as_slice())
133 }
134
135 fn get_list(&self) -> Option<&[Term]> {
136 TryAsRef::<List>::try_as_ref(self).map(|l| l.elements.as_slice())
137 }
138
139 fn get_string(&self) -> Option<String> {
140 if let Term::ByteList(bl) = self {
142 std::str::from_utf8(&bl.bytes).ok().map(|s| s.to_owned())
143 } else if let Term::Binary(b) = self {
144 std::str::from_utf8(&b.bytes).ok().map(|s| s.to_owned())
145 } else if let Term::Atom(a) = self {
146 Some(a.name.clone())
147 } else {
148 None
149 }
150 }
151
152 fn get_term_map(&self) -> Option<TermMap> {
153 match self {
154 Term::Map(m) => Some(TermMap(m.map.clone())),
155 _ => None,
156 }
157 }
158
159 fn parse_list<T, E>(&self, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
160 where
161 E: std::fmt::Display,
162 {
163 self.get_list().map(|list| parse_list(list, parser)).unwrap_or_default()
164 }
165}
166
167#[derive(Default, Clone, Debug)]
168pub struct TermMap(pub HashMap<Term, Term>);
169
170impl Deref for TermMap {
171 type Target = HashMap<Term, Term>;
172 fn deref(&self) -> &Self::Target {
173 &self.0
174 }
175}
176
177impl DerefMut for TermMap {
178 fn deref_mut(&mut self) -> &mut Self::Target {
179 &mut self.0
180 }
181}
182
183impl TermMap {
184 pub fn get_term_map(&self, key: &str) -> Option<Self> {
185 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_term_map)
186 }
187
188 pub fn get_binary<'a, A>(&'a self, key: &str) -> Option<A>
189 where
190 A: TryFrom<&'a [u8]>,
191 {
192 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_binary).and_then(|b| A::try_from(b).ok())
193 }
194
195 pub fn get_integer<I>(&self, key: &str) -> Option<I>
196 where
197 I: TryFrom<i128>,
198 {
199 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_integer).and_then(|b| I::try_from(b).ok())
200 }
201
202 pub fn get_list(&self, key: &str) -> Option<&[Term]> {
203 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_list)
204 }
205
206 pub fn get_atom(&self, key: &str) -> Option<&Atom> {
207 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::as_atom)
208 }
209
210 pub fn get_string(&self, key: &str) -> Option<String> {
211 self.0.get(&Term::Atom(Atom::from(key))).and_then(TermExt::get_string)
212 }
213
214 pub fn parse_list<T, E>(&self, key: &str, parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
215 where
216 E: std::fmt::Display,
217 {
218 self.0.get(&Term::Atom(Atom::from(key))).map(|term| term.parse_list(parser)).unwrap_or_default()
219 }
220
221 pub fn into_term(self) -> Term {
222 Term::Map(eetf::Map { map: self.0 })
223 }
224}
225
226pub fn bools_to_bitvec(mask: &[bool]) -> Vec<u8> {
227 let mut out = vec![0u8; mask.len().div_ceil(8)];
228 for (i, &b) in mask.iter().enumerate() {
229 if b {
230 out[i / 8] |= 1 << (7 - (i % 8));
231 }
232 }
233 out
234}
235
236pub fn bitvec_to_bools(bytes: Vec<u8>) -> Vec<bool> {
237 let mut out = Vec::with_capacity(bytes.len() * 8);
238 for b in bytes {
239 for i in (0..8).rev() {
241 out.push(((b >> i) & 1) != 0);
243 }
244 }
245 out
246}
247pub fn format_duration(total_seconds: u32) -> String {
268 if total_seconds < 60 {
269 return format!("{}s", total_seconds);
270 }
271
272 let minutes = total_seconds / 60;
273 let seconds = total_seconds % 60;
274
275 if minutes < 60 {
276 return format!("{}m {}s", minutes, seconds);
277 }
278
279 let hours = minutes / 60;
280 let minutes = minutes % 60;
281
282 if hours < 24 {
283 return format!("{}h {}m", hours, minutes);
284 }
285
286 let days = hours / 24;
287 let hours = hours % 24;
288
289 if days < 30 {
290 return format!("{}d {}h", days, hours);
291 }
292
293 let months = days / 30; let days = days % 30;
295
296 if months < 12 {
297 return format!("{}mo {}d", months, days);
298 }
299
300 let years = months / 12;
301 let months = months % 12;
302
303 format!("{}y {}mo {}d", years, months, days)
304}
305
306pub fn parse_list<T, E>(list: &[Term], parser: impl Fn(&[u8]) -> Result<T, E>) -> Vec<T>
308where
309 E: std::fmt::Display,
310{
311 list.iter()
312 .filter_map(|term| {
313 term.get_binary().and_then(|bytes| parser(bytes).map_err(|e| warn!("Failed to parse item: {}", e)).ok())
314 })
315 .collect()
316}
317
318pub fn serialize_list<T, E>(items: &[T], serializer: impl Fn(&T) -> Result<Vec<u8>, E>) -> Option<Term>
320where
321 E: std::fmt::Display,
322{
323 let terms: Result<Vec<_>, _> =
324 items.iter().map(|item| serializer(item).map(|bytes| Term::Binary(Binary::from(bytes)))).collect();
325 terms.ok().map(|terms| Term::List(List::from(terms)))
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn hexdump_basic() {
334 let s = hexdump(&[0x41, 0x00, 0x7F]);
335 assert!(s.starts_with("00000000 "));
336 assert!(s.contains("41 00 7F"));
337 assert!(s.ends_with("A.."));
338 }
339
340 #[test]
341 fn string_helpers() {
342 assert!(is_ascii_clean("AZaz09_-!"));
343 assert!(!is_ascii_clean("hi🙂"));
344 assert_eq!(alphanumeric("Abc-123"), "Abc123");
345 assert!(is_alphanumeric("abc123"));
346 assert!(!is_alphanumeric("a_b"));
347 }
348
349 #[test]
350 fn ext_and_urls() {
351 assert_eq!(url("http://a/b/"), "http://a/b");
352 assert_eq!(url("http://a/b"), "http://a/b");
353 assert_eq!(url_with("http://a/b", "/c"), "http://a/b/c");
354 }
355
356 #[test]
357 fn bitvec_roundtrip_prefix() {
358 let mask = vec![true, false, true, true, false, false, false, true, true];
359 let bytes = bools_to_bitvec(&mask);
360 assert_eq!(bytes.len(), 2);
361 let bools = bitvec_to_bools(bytes.clone());
362 assert_eq!(&bools[..mask.len()], &mask[..]);
363 for b in &bools[mask.len()..8 * bytes.len()] {
364 assert!(!*b);
365 }
366 }
367}