use std::{
borrow::Cow,
fs::{self, File},
io,
num::Wrapping,
path::Path,
};
use tempfile::Builder;
pub fn write_atomic<F>(path: impl AsRef<Path>, write: F) -> std::io::Result<()>
where
F: FnOnce(&mut std::fs::File) -> std::io::Result<()>,
{
let parent = path.as_ref().parent().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"cannot create file without parent directory",
)
})?;
fs::create_dir_all(parent)?;
let mut tmpfile = Builder::new().prefix(".m2dir.tmp.").tempfile_in(parent)?;
write(tmpfile.as_file_mut())?;
tmpfile.as_file().sync_all()?;
tmpfile.persist_noclobber(&path)?;
Ok(())
}
pub fn copy_atomic(from: impl AsRef<Path>, to: impl AsRef<Path>) -> std::io::Result<()> {
let mut from = File::open(&from)?;
write_atomic(to, |f| {
io::copy(&mut from, f)?;
Ok(())
})
}
pub fn fnv64(salt: &[u8], data: &[u8]) -> u64 {
let prime = Wrapping(1099511628211u64);
let offset: u64 = 14695981039346656037;
let mut sum = Wrapping(offset);
for b in salt {
let byte = Wrapping(u64::from(*b));
sum ^= byte;
sum *= prime;
}
for b in data {
let byte = Wrapping(u64::from(*b));
sum ^= byte;
sum *= prime;
}
sum.0
}
pub fn sanitize_filename(input: &str) -> String {
input.replace(['/', '"', '\'', '|', '\\', '*', '&', '$'], "_")
}
pub fn encode_folder_name<'a>(input: &'a str) -> Cow<'a, str> {
if !input.contains(['%', '/']) {
Cow::Borrowed(input)
} else {
let mut result = String::with_capacity(input.len());
let mut last_match = 0;
for (i, m) in input.match_indices(['%', '/']).peekable() {
result.push_str(&input[last_match..i]);
last_match = i + 1;
match m {
"/" => {
result.push_str("%2F");
}
"%" => {
result.push_str("%25");
}
_ => unreachable!("match broken"),
}
}
result.push_str(&input[last_match..]);
Cow::Owned(result)
}
}
pub fn decode_folder_name<'a>(input: &'a str) -> Cow<'a, str> {
if !input.contains('%') {
Cow::Borrowed(input)
} else {
let mut result: Vec<u8> = Vec::with_capacity(input.len());
let mut last_match = 0;
for (i, _) in input.match_indices('%').peekable() {
result.extend_from_slice(&input.as_bytes()[last_match..i]);
last_match = i;
if input[i..].len() >= 3 {
let h = char::from(input.as_bytes()[i + 1]).to_digit(16);
let l = char::from(input.as_bytes()[i + 2]).to_digit(16);
if let (Some(h), Some(l)) = (h, l) {
result.push(h as u8 * 0x10 + l as u8);
last_match += 3;
}
}
}
result.extend_from_slice(&input.as_bytes()[last_match..]);
Cow::Owned(String::from_utf8(result).expect("decode_folder_name: invalid UTF-8"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_folder_name() {
assert_eq!(decode_folder_name("foo"), "foo");
assert_eq!(decode_folder_name("foo%2Fbar"), "foo/bar");
assert_eq!(decode_folder_name("foo%2F"), "foo/");
assert_eq!(decode_folder_name("foo%2"), "foo%2");
assert_eq!(decode_folder_name("foo%"), "foo%");
assert_eq!(decode_folder_name("foo%%glurb"), "foo%%glurb");
assert_eq!(decode_folder_name("foo%2F%25bar"), "foo/%bar");
assert_eq!(decode_folder_name("foo%2Fbaz%25bar"), "foo/baz%bar");
assert_eq!(decode_folder_name("foo%252Fbar"), "foo%2Fbar");
assert_eq!(decode_folder_name("föö%252Fbär"), "föö%2Fbär");
assert_eq!(decode_folder_name("%2F%2F%2F"), "///");
assert_eq!(decode_folder_name("%E2%82%AC"), "€");
}
#[test]
fn test_encode_folder_name() {
assert_eq!(encode_folder_name("foo"), "foo");
assert_eq!(encode_folder_name("foo/bar"), "foo%2Fbar");
assert_eq!(encode_folder_name("foo%bar"), "foo%25bar");
assert_eq!(encode_folder_name("foo%/"), "foo%25%2F");
}
}