1#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::missing_panics_doc)]
4#![allow(clippy::module_name_repetitions)]
5#![allow(clippy::redundant_closure_for_method_calls)]
6
7pub mod blake2b256;
8pub mod fs;
9pub mod rand;
10pub mod serde;
11
12pub use crate::blake2b256::Blake2b256;
13
14use base64::Engine;
15use base64::engine::general_purpose::URL_SAFE_NO_PAD;
16use blake2::{Digest, digest::FixedOutput};
17use std::{
18 collections::HashSet,
19 ffi::OsStr,
20 io::{self, BufRead, Write},
21 path::{Path, PathBuf},
22 process,
23};
24
25#[derive(Debug, thiserror::Error)]
26pub enum YAMLIOError {
27 #[error("I/O: {}", _0)]
28 IO(#[from] std::io::Error),
29
30 #[error("Can't save to root path")]
31 RootPath,
32
33 #[error("YAML: {}", _0)]
34 YAML(#[from] serde_yaml::Error),
35}
36
37#[must_use]
39pub fn now() -> chrono::DateTime<chrono::offset::FixedOffset> {
40 let date = chrono::offset::Local::now();
41 date.with_timezone(date.offset())
42}
43
44#[must_use]
45pub fn blake2b256sum(bytes: &[u8]) -> [u8; 32] {
46 let mut hasher = Blake2b256::new();
47 hasher.update(bytes);
48 hasher.finalize_fixed().into()
49}
50
51pub fn blake2b256sum_file(path: &Path) -> io::Result<[u8; 32]> {
52 let mut hasher = Blake2b256::new();
53 read_file_to_digest_input(path, &mut hasher)?;
54 Ok(hasher.finalize_fixed().into())
55}
56
57pub fn base64_decode<T: ?Sized + AsRef<[u8]>>(input: &T) -> Result<Vec<u8>, base64::DecodeError> {
58 URL_SAFE_NO_PAD.decode(input)
59}
60
61pub fn base64_encode<T: ?Sized + AsRef<[u8]>>(input: &T) -> String {
62 URL_SAFE_NO_PAD.encode(input)
63}
64
65#[must_use]
89pub fn sanitize_name_for_fs(s: &str) -> PathBuf {
90 let mut buffer = String::new();
91 for ch in s.chars().take(16) {
92 match ch {
93 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => buffer.push(ch),
94 _ => {
95 buffer.push('_');
101 }
102 }
103 }
104 buffer.push('-');
105 buffer.push_str(&base64_encode(&blake2b256sum(s.as_bytes())[..16]));
106 PathBuf::from(buffer)
107}
108
109#[must_use]
127pub fn sanitize_url_for_fs(url: &str) -> PathBuf {
128 let mut buffer = String::new();
129
130 let trimmed = url.trim();
131
132 let stripped = if let Some(t) = trimmed.strip_prefix("http://") {
133 t
134 } else if let Some(t) = trimmed.strip_prefix("https://") {
135 t
136 } else {
137 trimmed
138 };
139
140 for ch in stripped.chars().take(48) {
141 match ch {
142 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => buffer.push(ch),
143 _ => {
144 buffer.push('_');
150 }
151 }
152 }
153 buffer.push('-');
154 buffer.push_str(&base64_encode(&blake2b256sum(trimmed.as_bytes())[..16]));
155 PathBuf::from(buffer)
156}
157
158pub fn is_equal_default<T: Default + PartialEq>(t: &T) -> bool {
159 *t == T::default()
160}
161
162pub fn is_vec_empty<T>(t: &[T]) -> bool {
163 t.is_empty()
164}
165
166#[must_use]
167pub fn is_set_empty<T>(t: &HashSet<T>) -> bool {
168 t.is_empty()
169}
170
171pub fn read_file_to_digest_input(
172 path: &Path,
173 input: &mut impl blake2::digest::Update,
174) -> io::Result<()> {
175 let file = std::fs::File::open(path)?;
176
177 let mut reader = io::BufReader::new(file);
178
179 loop {
180 let length = {
181 let buffer = reader.fill_buf()?;
182 input.update(buffer);
183 buffer.len()
184 };
185 if length == 0 {
186 break;
187 }
188 reader.consume(length);
189 }
190
191 Ok(())
192}
193
194#[derive(Debug, thiserror::Error)]
195pub enum CancelledError {
196 #[error("Cancelled by the user")]
197 ByUser,
198 #[error("Cancelled due to terminal I/O error")]
199 NoInput,
200}
201
202pub fn try_again_or_cancel() -> std::result::Result<(), CancelledError> {
203 if !yes_or_no_was_y("Try again (Y/n)")
204 .map_err(|_| CancelledError::NoInput)?
205 .unwrap_or(true)
206 {
207 return Err(CancelledError::ByUser);
208 }
209
210 Ok(())
211}
212
213pub fn yes_or_no_was_y(msg: &str) -> io::Result<Option<bool>> {
214 loop {
215 let reply = rprompt::prompt_reply_from_bufread(
216 &mut std::io::stdin().lock(),
217 &mut std::io::stderr(),
218 format!("{msg} "),
219 )?;
220
221 match reply.as_str() {
222 "y" | "Y" => return Ok(Some(true)),
223 "n" | "N" => return Ok(Some(false)),
224 "" => return Ok(None),
225 _ => {}
226 }
227 }
228}
229
230pub fn run_with_shell_cmd(cmd: &OsStr, arg: Option<&Path>) -> io::Result<std::process::ExitStatus> {
231 Ok(run_with_shell_cmd_custom(cmd, arg, false)?.status)
232}
233
234pub fn run_with_shell_cmd_capture_stdout(cmd: &OsStr, arg: Option<&Path>) -> io::Result<Vec<u8>> {
235 let output = run_with_shell_cmd_custom(cmd, arg, true)?;
236 if !output.status.success() {
237 return Err(std::io::Error::other("command failed with non-zero status"));
238 }
239 Ok(output.stdout)
240}
241
242pub fn run_with_shell_cmd_custom(
243 cmd: &OsStr,
244 arg: Option<&Path>,
245 capture_stdout: bool,
246) -> io::Result<std::process::Output> {
247 if cfg!(windows) {
248 let mut proc = process::Command::new("cmd.exe");
252 if let Some(arg) = arg {
253 proc.arg("/c").arg("%CREV_CMD% %CREV_ARG%");
254 proc.env("CREV_CMD", cmd);
255 proc.env("CREV_ARG", arg);
256 } else {
257 proc.arg("/c").arg("%CREV_CMD%");
258 proc.env("CREV_CMD", cmd);
259 }
260 proc
261 } else if cfg!(unix) {
262 let mut proc = process::Command::new("/bin/sh");
263 if let Some(arg) = arg {
264 proc.arg("-c").arg(format!(
265 "{} {}",
266 cmd.to_str().ok_or_else(|| std::io::Error::new(
267 io::ErrorKind::InvalidData,
268 "not a valid unicode"
269 ))?,
270 shell_escape::escape(arg.display().to_string().into())
271 ));
272 } else {
273 proc.arg("-c").arg(cmd);
274 }
275 proc
276 } else {
277 panic!("What platform are you running this on? Please submit a PR!");
278 }
279 .stdin(process::Stdio::inherit())
280 .stderr(process::Stdio::inherit())
281 .stdout(if capture_stdout {
282 process::Stdio::piped()
283 } else {
284 process::Stdio::inherit()
285 })
286 .output()
287}
288
289pub fn save_to_yaml_file<T>(path: &Path, t: &T) -> Result<(), YAMLIOError>
290where
291 T: ::serde::Serialize,
292{
293 std::fs::create_dir_all(path.parent().ok_or(YAMLIOError::RootPath)?)?;
294 let text = serde_yaml::to_string(t)?;
295 store_str_to_file(path, &text)?;
296 Ok(())
297}
298
299pub fn read_from_yaml_file<T>(path: &Path) -> Result<T, YAMLIOError>
300where
301 T: ::serde::de::DeserializeOwned,
302{
303 let text = std::fs::read_to_string(path)?;
304
305 Ok(serde_yaml::from_str(&text)?)
306}
307
308#[inline]
309pub fn store_str_to_file(path: &Path, s: &str) -> io::Result<()> {
310 store_to_file_with(path, |f| f.write_all(s.as_bytes())).and_then(|res| res)
311}
312
313pub fn store_to_file_with<E, F>(path: &Path, f: F) -> io::Result<Result<(), E>>
314where
315 F: Fn(&mut dyn io::Write) -> Result<(), E>,
316{
317 std::fs::create_dir_all(path.parent().expect("Not a root path"))?;
318 let tmp_path = path.with_extension("tmp");
319 let mut file = std::fs::File::create(&tmp_path)?;
320 if let Err(e) = f(&mut file) {
321 return Ok(Err(e));
322 }
323 file.flush()?;
324 file.sync_data()?;
325 drop(file);
326 std::fs::rename(tmp_path, path)?;
327 Ok(Ok(()))
328}