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 & b"), "a & b");
300 assert_eq!(decode_entities("a &zZz; b"), "a &zZz; b");
301 assert_eq!(decode_entities("a & b"), "a & b");
302 assert_eq!(decode_entities("a > b"), "a > b");
303 assert_eq!(decode_entities("a & b"), "a & b");
304 assert_eq!(decode_entities("a < b > c"), "a < b > c");
305 }
306}