You've already forked AstralRinth
forked from didirus/AstralRinth
* fix(app-lib, labrinth): stricter mrpack file path validation * chore: run `cargo fmt` * tweak: reject reserved Windows device names in mrpacks too
352 lines
9.8 KiB
Rust
352 lines
9.8 KiB
Rust
// IO error
|
|
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
|
|
|
|
use std::{
|
|
io::{ErrorKind, Write},
|
|
path::Path,
|
|
};
|
|
use tempfile::NamedTempFile;
|
|
use tokio::task::spawn_blocking;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum IOError {
|
|
#[error("{source}, path: {path}")]
|
|
IOPathError {
|
|
#[source]
|
|
source: std::io::Error,
|
|
path: String,
|
|
},
|
|
#[error(transparent)]
|
|
IOError(#[from] std::io::Error),
|
|
}
|
|
|
|
impl IOError {
|
|
pub fn from(source: std::io::Error) -> Self {
|
|
Self::IOError(source)
|
|
}
|
|
pub fn with_path(
|
|
source: std::io::Error,
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Self {
|
|
let path = path.as_ref();
|
|
|
|
Self::IOPathError {
|
|
source,
|
|
path: path.to_string_lossy().to_string(),
|
|
}
|
|
}
|
|
|
|
pub fn kind(&self) -> ErrorKind {
|
|
match self {
|
|
IOError::IOPathError { source, .. } => source.kind(),
|
|
IOError::IOError(source) => source.kind(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn canonicalize(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<std::path::PathBuf, IOError> {
|
|
let path = path.as_ref();
|
|
dunce::canonicalize(path).map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn read_dir(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<tokio::fs::ReadDir, IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::read_dir(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn create_dir(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::create_dir(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn create_dir_all(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::create_dir_all(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn remove_dir_all(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::remove_dir_all(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
/// Reads a text file to a string, automatically detecting its encoding and
|
|
/// substituting any invalid characters with the Unicode replacement character.
|
|
///
|
|
/// This function is best suited for reading Minecraft instance files, whose
|
|
/// encoding may vary depending on the platform, launchers, client versions
|
|
/// (older Minecraft versions tended to rely on the system's default codepage
|
|
/// more on Windows platforms), and mods used, while not being highly sensitive
|
|
/// to occasional occurrences of mojibake or character replacements.
|
|
pub async fn read_any_encoding_to_string(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(String, &'static encoding_rs::Encoding), IOError> {
|
|
let path = path.as_ref();
|
|
let file_bytes =
|
|
tokio::fs::read(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})?;
|
|
|
|
let file_encoding = {
|
|
let mut encoding_detector = chardetng::EncodingDetector::new();
|
|
encoding_detector.feed(&file_bytes, true);
|
|
encoding_detector.guess(None, true)
|
|
};
|
|
|
|
let (file_string, actual_file_encoding, _) =
|
|
file_encoding.decode(&file_bytes);
|
|
Ok((file_string.to_string(), actual_file_encoding))
|
|
}
|
|
|
|
pub async fn read(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<Vec<u8>, IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::read(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn write(
|
|
path: impl AsRef<std::path::Path>,
|
|
data: impl AsRef<[u8]>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref().to_owned();
|
|
let data = data.as_ref().to_owned();
|
|
spawn_blocking(move || {
|
|
let cloned_path = path.clone();
|
|
sync_write(data, path).map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: cloned_path.to_string_lossy().to_string(),
|
|
})
|
|
})
|
|
.await
|
|
.map_err(|_| std::io::Error::other("background task failed"))??;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn sync_write(
|
|
data: impl AsRef<[u8]>,
|
|
path: impl AsRef<Path>,
|
|
) -> Result<(), std::io::Error> {
|
|
let mut tempfile =
|
|
NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| {
|
|
std::io::Error::other(
|
|
"could not get parent directory for temporary file",
|
|
)
|
|
})?)?;
|
|
tempfile.write_all(data.as_ref())?;
|
|
let tmp_path = tempfile.into_temp_path();
|
|
let path = path.as_ref();
|
|
tmp_path.persist(path)?;
|
|
std::io::Result::Ok(())
|
|
}
|
|
|
|
pub fn is_same_disk(old_dir: &Path, new_dir: &Path) -> Result<bool, IOError> {
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::MetadataExt;
|
|
Ok(old_dir.metadata()?.dev() == new_dir.metadata()?.dev())
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
{
|
|
let old_dir = canonicalize(old_dir)?;
|
|
let new_dir = canonicalize(new_dir)?;
|
|
|
|
let old_component = old_dir.components().next();
|
|
let new_component = new_dir.components().next();
|
|
|
|
match (old_component, new_component) {
|
|
(
|
|
Some(std::path::Component::Prefix(old)),
|
|
Some(std::path::Component::Prefix(new)),
|
|
) => Ok(old.as_os_str() == new.as_os_str()),
|
|
_ => Ok(false),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn rename_or_move(
|
|
from: impl AsRef<std::path::Path>,
|
|
to: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let from = from.as_ref();
|
|
let to = to.as_ref();
|
|
|
|
if to
|
|
.parent()
|
|
.map_or(Ok(false), |to_dir| is_same_disk(from, to_dir))?
|
|
{
|
|
tokio::fs::rename(from, to)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: from.to_string_lossy().to_string(),
|
|
})
|
|
} else {
|
|
move_recursive(from, to).await
|
|
}
|
|
}
|
|
|
|
#[async_recursion::async_recursion]
|
|
async fn move_recursive(from: &Path, to: &Path) -> Result<(), IOError> {
|
|
if from.is_file() {
|
|
copy(from, to).await?;
|
|
remove_file(from).await?;
|
|
return Ok(());
|
|
}
|
|
|
|
create_dir(to).await?;
|
|
|
|
let mut dir = read_dir(from).await?;
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let new_path = to.join(entry.file_name());
|
|
move_recursive(&entry.path(), &new_path).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn copy(
|
|
from: impl AsRef<std::path::Path>,
|
|
to: impl AsRef<std::path::Path>,
|
|
) -> Result<u64, IOError> {
|
|
let from: &Path = from.as_ref();
|
|
let to = to.as_ref();
|
|
tokio::fs::copy(from, to)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: from.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn remove_file(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::remove_file(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn open_file(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<tokio::fs::File, IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::File::open(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn remove_dir(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<(), IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::remove_dir(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
pub async fn metadata(
|
|
path: impl AsRef<std::path::Path>,
|
|
) -> Result<std::fs::Metadata, IOError> {
|
|
let path = path.as_ref();
|
|
tokio::fs::metadata(path)
|
|
.await
|
|
.map_err(|e| IOError::IOPathError {
|
|
source: e,
|
|
path: path.to_string_lossy().to_string(),
|
|
})
|
|
}
|
|
|
|
/// Gets a resource file from the executable. Returns `theseus::Result<(TempDir, PathBuf)>`.
|
|
#[macro_export]
|
|
macro_rules! get_resource_file {
|
|
(directory: $relative_dir:expr, file: $file_name:expr) => {
|
|
'get_resource_file: {
|
|
let dir = match tempfile::tempdir() {
|
|
Ok(dir) => dir,
|
|
Err(e) => {
|
|
break 'get_resource_file $crate::Result::Err(
|
|
$crate::util::io::IOError::from(e).into(),
|
|
);
|
|
}
|
|
};
|
|
let path = dir.path().join($file_name);
|
|
if let Err(e) = $crate::util::io::write(
|
|
&path,
|
|
include_bytes!(concat!($relative_dir, "/", $file_name)),
|
|
)
|
|
.await
|
|
{
|
|
break 'get_resource_file $crate::Result::Err(e.into());
|
|
}
|
|
let path = match $crate::util::io::canonicalize(path) {
|
|
Ok(path) => path,
|
|
Err(e) => {
|
|
break 'get_resource_file $crate::Result::Err(e.into());
|
|
}
|
|
};
|
|
$crate::Result::Ok((dir, path))
|
|
}
|
|
};
|
|
|
|
($relative_dir:literal / $file_name:literal) => {
|
|
get_resource_file!(directory: $relative_dir, file: $file_name)
|
|
};
|
|
|
|
(env $dir_env_name:literal / $file_name:literal) => {
|
|
get_resource_file!(directory: env!($dir_env_name), file: $file_name)
|
|
};
|
|
}
|