cadmus_core/
helpers.rs

1use anyhow::{Context, Error};
2use entities::ENTITIES;
3use fxhash::FxHashMap;
4use lazy_static::lazy_static;
5use serde::de::{self, Visitor};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::borrow::Cow;
8use std::char;
9use std::fmt;
10use std::fs::{self, File, Metadata};
11use std::io::{self, BufReader, BufWriter};
12use std::num::ParseIntError;
13use std::ops::{Deref, DerefMut};
14use std::path::{Component, Path, PathBuf};
15use std::str::FromStr;
16use std::time::SystemTime;
17use walkdir::DirEntry;
18
19lazy_static! {
20    pub static ref CHARACTER_ENTITIES: FxHashMap<&'static str, &'static str> = {
21        let mut m = FxHashMap::default();
22        for e in ENTITIES.iter() {
23            m.insert(e.entity, e.characters);
24        }
25        m
26    };
27}
28
29pub fn decode_entities(text: &str) -> Cow<'_, str> {
30    if text.find('&').is_none() {
31        return Cow::Borrowed(text);
32    }
33
34    let mut cursor = text;
35    let mut buf = String::with_capacity(text.len());
36
37    while let Some(start_index) = cursor.find('&') {
38        buf.push_str(&cursor[..start_index]);
39        cursor = &cursor[start_index..];
40        if let Some(end_index) = cursor.find(';') {
41            if let Some(repl) = CHARACTER_ENTITIES.get(&cursor[..=end_index]) {
42                buf.push_str(repl);
43            } else if cursor[1..].starts_with('#') {
44                let radix = if cursor[2..].starts_with('x') { 16 } else { 10 };
45                let drift_index = 2 + radix as usize / 16;
46                if let Some(ch) = u32::from_str_radix(&cursor[drift_index..end_index], radix)
47                    .ok()
48                    .and_then(char::from_u32)
49                {
50                    buf.push(ch);
51                } else {
52                    buf.push_str(&cursor[..=end_index]);
53                }
54            } else {
55                buf.push_str(&cursor[..=end_index]);
56            }
57            cursor = &cursor[end_index + 1..];
58        } else {
59            break;
60        }
61    }
62
63    buf.push_str(cursor);
64    Cow::Owned(buf)
65}
66
67pub fn load_json<T, P: AsRef<Path>>(path: P) -> Result<T, Error>
68where
69    for<'a> T: Deserialize<'a>,
70{
71    let file = File::open(path.as_ref())
72        .with_context(|| format!("can't open file {}", path.as_ref().display()))?;
73    let reader = BufReader::new(file);
74    serde_json::from_reader(reader)
75        .with_context(|| format!("can't parse JSON from {}", path.as_ref().display()))
76        .map_err(Into::into)
77}
78
79pub fn save_json<T, P: AsRef<Path>>(data: &T, path: P) -> Result<(), Error>
80where
81    T: Serialize,
82{
83    let file = File::create(path.as_ref())
84        .with_context(|| format!("can't create file {}", path.as_ref().display()))?;
85    let writer = BufWriter::new(file);
86    serde_json::to_writer_pretty(writer, data)
87        .with_context(|| format!("can't serialize to JSON file {}", path.as_ref().display()))
88        .map_err(Into::into)
89}
90
91pub fn load_toml<T, P: AsRef<Path>>(path: P) -> Result<T, Error>
92where
93    for<'a> T: Deserialize<'a>,
94{
95    let s = fs::read_to_string(path.as_ref())
96        .with_context(|| format!("can't read file {}", path.as_ref().display()))?;
97    toml::from_str(&s)
98        .with_context(|| format!("can't parse TOML content from {}", path.as_ref().display()))
99        .map_err(Into::into)
100}
101
102#[cfg_attr(feature = "otel", tracing::instrument(skip(data, path), fields(file_path = %path.as_ref().display())))]
103pub fn save_toml<T, P: AsRef<Path>>(data: &T, path: P) -> Result<(), Error>
104where
105    T: Serialize,
106{
107    let path_ref = path.as_ref();
108    tracing::debug!(file_path = %path_ref.display(), "serializing data to TOML");
109    let s = toml::to_string(data).context("can't convert to TOML format")?;
110
111    tracing::debug!(
112        file_path = %path_ref.display(),
113        toml_size = s.len(),
114        "writing TOML to file"
115    );
116
117    match fs::write(path_ref, &s) {
118        Ok(()) => {
119            let file_size = path_ref.metadata().ok().map(|m| m.len());
120
121            tracing::debug!(
122                file_path = %path_ref.display(),
123                file_size = ?file_size,
124                "successfully wrote TOML file"
125            );
126
127            Ok(())
128        }
129        Err(e) => {
130            tracing::error!(
131                file_path = %path_ref.display(),
132                error = %e,
133                "failed to write TOML file"
134            );
135            Err(anyhow::Error::new(e))
136                .context(format!("can't write to file {}", path_ref.display()))
137        }
138    }
139}
140
141pub trait Fingerprint {
142    fn fingerprint(&self, epoch: SystemTime) -> io::Result<Fp>;
143}
144
145impl Fingerprint for Metadata {
146    fn fingerprint(&self, epoch: SystemTime) -> io::Result<Fp> {
147        let m = self
148            .modified()?
149            .duration_since(epoch)
150            .map_or_else(|e| e.duration().as_secs(), |v| v.as_secs());
151        Ok(Fp(m.rotate_left(32) ^ self.len()))
152    }
153}
154
155#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
156pub struct Fp(u64);
157
158impl Deref for Fp {
159    type Target = u64;
160
161    fn deref(&self) -> &Self::Target {
162        &self.0
163    }
164}
165
166impl DerefMut for Fp {
167    fn deref_mut(&mut self) -> &mut Self::Target {
168        &mut self.0
169    }
170}
171
172impl FromStr for Fp {
173    type Err = ParseIntError;
174
175    fn from_str(s: &str) -> Result<Self, Self::Err> {
176        u64::from_str_radix(s, 16).map(Fp)
177    }
178}
179
180impl fmt::Display for Fp {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "{:016X}", self.0)
183    }
184}
185
186impl Serialize for Fp {
187    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
188    where
189        S: Serializer,
190    {
191        serializer.serialize_str(&self.to_string())
192    }
193}
194
195struct FpVisitor;
196
197impl<'de> Visitor<'de> for FpVisitor {
198    type Value = Fp;
199
200    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
201        formatter.write_str("a string")
202    }
203
204    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
205    where
206        E: de::Error,
207    {
208        Self::Value::from_str(value)
209            .map_err(|e| E::custom(format!("can't parse fingerprint: {}", e)))
210    }
211}
212
213impl<'de> Deserialize<'de> for Fp {
214    fn deserialize<D>(deserializer: D) -> Result<Fp, D::Error>
215    where
216        D: Deserializer<'de>,
217    {
218        deserializer.deserialize_str(FpVisitor)
219    }
220}
221
222pub trait Normalize: ToOwned {
223    fn normalize(&self) -> Self::Owned;
224}
225
226impl Normalize for Path {
227    fn normalize(&self) -> PathBuf {
228        let mut result = PathBuf::default();
229
230        for c in self.components() {
231            match c {
232                Component::ParentDir => {
233                    result.pop();
234                }
235                Component::CurDir => (),
236                _ => result.push(c),
237            }
238        }
239
240        result
241    }
242}
243
244pub trait AsciiExtension {
245    fn to_alphabetic_digit(self) -> Option<u32>;
246}
247
248impl AsciiExtension for char {
249    fn to_alphabetic_digit(self) -> Option<u32> {
250        if self.is_ascii_uppercase() {
251            Some(self as u32 - 65)
252        } else {
253            None
254        }
255    }
256}
257
258pub mod datetime_format {
259    use chrono::NaiveDateTime;
260    use serde::{self, Deserialize, Deserializer, Serializer};
261
262    pub const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
263
264    pub fn serialize<S>(date: &NaiveDateTime, serializer: S) -> Result<S::Ok, S::Error>
265    where
266        S: Serializer,
267    {
268        let s = format!("{}", date.format(FORMAT));
269        serializer.serialize_str(&s)
270    }
271
272    pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDateTime, D::Error>
273    where
274        D: Deserializer<'de>,
275    {
276        let s = String::deserialize(deserializer)?;
277        NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
278    }
279}
280
281pub trait IsHidden {
282    fn is_hidden(&self) -> bool;
283}
284
285impl IsHidden for DirEntry {
286    fn is_hidden(&self) -> bool {
287        self.file_name()
288            .to_str()
289            .map_or(false, |s| s.starts_with('.'))
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_entities() {
299        assert_eq!(decode_entities("a &amp b"), "a &amp b");
300        assert_eq!(decode_entities("a &zZz; b"), "a &zZz; b");
301        assert_eq!(decode_entities("a &amp; b"), "a & b");
302        assert_eq!(decode_entities("a &#x003E; b"), "a > b");
303        assert_eq!(decode_entities("a &#38; b"), "a & b");
304        assert_eq!(decode_entities("a &lt; b &gt; c"), "a < b > c");
305    }
306}