legacympt-rs/mpt/src/util.rs

245 lines
7.1 KiB
Rust

use addonscript::manifest::{Manifest, Repository};
use anyhow::Context;
use crossterm::style::{Attribute, Attributes, Color, ContentStyle, Stylize};
use indicatif::ProgressStyle;
use percent_encoding::percent_decode;
use std::{
collections::HashMap,
io,
path::{Path, PathBuf},
string::FromUtf8Error,
};
use thiserror::Error;
use url::Url;
use walkdir::WalkDir;
use crate::config::Config;
/// reads and parses the config from the current working directory
pub async fn parse_config() -> anyhow::Result<Config> {
let conf = tokio::fs::read("modpacktoolsconfig.toml").await?;
Ok(toml::from_slice(&conf)?)
}
/// parses the config from the current working directory, reads the location of
/// the manifest file from it, parses the manifest and returns both the conig
/// and the manifest.
pub async fn parse_config_and_manifest() -> anyhow::Result<(Config, Manifest)> {
let config = parse_config().await?;
let src = Path::new(&config.locations.src);
let path = if src.join("modpack.json5").exists() {
src.join("modpack.json5")
} else {
src.join("modpack.json")
};
let data = tokio::fs::read(path).await?;
let data = std::str::from_utf8(&data)?;
let manifest = json5::from_str::<Manifest>(data)?;
Ok((config, manifest))
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum MavenParseError {
#[error("Maven Artifact specifier has invalid format!")]
InvalidFormat,
#[error("Url parse error while processing maven artifact: {0}")]
UrlParseError(#[from] url::ParseError),
}
/// parses a maven artifact specifier and generates a URL to the artifact in a
/// given maven repo.
pub fn mvn_artifact_to_url(art: &str, repo: &Repository) -> Result<Url, MavenParseError> {
let mut splits = art.split(':');
let group_id = splits.next().ok_or(MavenParseError::InvalidFormat)?;
let artifact_id = splits.next().ok_or(MavenParseError::InvalidFormat)?;
let version = splits.next().ok_or(MavenParseError::InvalidFormat)?;
if splits.count() != 0 {
return Err(MavenParseError::InvalidFormat);
}
let mut url = repo.url.clone();
if !url.path().ends_with('/') {
url.set_path(&format!("{}/", repo.url.path()));
}
let url = url.join(&format!(
"{gid}/{aid}/{v}/{aid}-{v}.jar",
gid = group_id.replace('.', "/"),
aid = artifact_id,
v = version
))?;
Ok(url)
}
/// convenience trait to color strings with given preset styles for the
/// modpacktools CLI
pub trait CliStyle: Stylize {
/// formats the object with the style used for information messages
fn info(self) -> Self::Styled;
}
impl<T: Stylize> CliStyle for T
where
T::Styled: AsRef<ContentStyle> + AsMut<ContentStyle>,
{
fn info(self) -> T::Styled {
let mut s = self.stylize();
s.as_mut().foreground_color = Some(Color::Cyan);
s.as_mut().attributes = Attributes::from(Attribute::Italic);
s
}
}
/// the default style for progress bars
pub fn progress_style() -> ProgressStyle {
ProgressStyle::default_bar()
.template("{prefix:.bold} [{wide_bar:.green}] {pos}/{len}")
.progress_chars("█▇▆▅▄▃▂▁ ")
}
/// creates the modpacktools temporary directory (set in the config)
pub async fn make_tmp_dir(config: &Config) -> anyhow::Result<()> {
tokio::fs::create_dir_all(&config.locations.temp_dir)
.await
.context("Failed to create temporary directory")?;
Ok(())
}
/// Converts a vec of addonscript repositories into a map of the ID and the
/// repo.
#[inline]
pub fn repo_map(repos: Vec<Repository>) -> HashMap<String, Repository> {
repos.into_iter().map(|r| (r.id.clone(), r)).collect()
}
/// Parses a curseforge artifact found in modpack.json files into the project id
/// and file id.
pub fn parse_curseforge_artifact(artifact: &str) -> Result<(&str, &str), MavenParseError> {
let mut splits = artifact.split(':').skip(1);
let p_id = splits.next().ok_or(MavenParseError::InvalidFormat)?;
let f_id = splits.next().ok_or(MavenParseError::InvalidFormat)?;
if splits.count() != 0 {
return Err(MavenParseError::InvalidFormat);
}
Ok((p_id, f_id))
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum UrlFileNameError {
#[error("URL is base URL without file name!")]
BaseUrl,
#[error("URL file name is invalid utf8: {0}")]
InvalidUtf8(#[from] FromUtf8Error),
}
/// Gets the last path segment (file name) of a URL
pub fn url_file_name(url: &Url) -> Result<String, UrlFileNameError> {
let file_name = url
.path_segments()
.map(Iterator::last)
.flatten()
.ok_or(UrlFileNameError::BaseUrl)?;
if file_name.is_empty() {
return Err(UrlFileNameError::BaseUrl);
}
let bytes = percent_decode(file_name.as_bytes()).collect::<Vec<_>>();
Ok(String::from_utf8(bytes)?)
}
/// Copies a directory inclding all files
pub async fn copy_dir(from: PathBuf, to: PathBuf) -> io::Result<()> {
for file in WalkDir::new(&from) {
let file = file?;
let is_dir = file.path().is_dir();
let target = to.join(
file.path()
.strip_prefix(&from)
// this should be safe
.expect("Couldn't strip base path"),
);
if is_dir {
tokio::fs::create_dir_all(target).await?;
} else {
tokio::fs::copy(file.path(), target).await?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use addonscript::manifest::RepositoryType;
use super::*;
fn repo() -> Repository {
Repository {
id: "test".into(),
repo_type: RepositoryType::Maven,
url: Url::parse("https://example.com/maven").unwrap(),
}
}
#[test]
fn artifact_to_url_valid() {
assert_eq!(
mvn_artifact_to_url("de.mzte:test:0.1", &repo()).unwrap(),
Url::parse("https://example.com/maven/de/mzte/test/0.1/test-0.1.jar").unwrap(),
);
assert_eq!(
mvn_artifact_to_url("test:test:test", &repo()).unwrap(),
Url::parse("https://example.com/maven/test/test/test/test-test.jar").unwrap(),
);
}
#[test]
fn artifact_to_url_invalid() {
assert!(mvn_artifact_to_url("test", &repo()).is_err());
assert!(mvn_artifact_to_url("test:test", &repo()).is_err());
assert!(mvn_artifact_to_url("test:test:test:test", &repo()).is_err());
}
#[test]
fn url_file_name_valid() {
assert_eq!(
url_file_name(&Url::parse("https://example.com/test.txt").unwrap()),
Ok("test.txt".to_string())
);
assert_eq!(
url_file_name(&Url::parse("https://example.com/test").unwrap()),
Ok("test".to_string())
);
}
#[test]
fn url_file_name_invalid() {
assert!(url_file_name(&Url::parse("https://example.com").unwrap()).is_err());
}
#[test]
fn curse_artifact_valid() {
assert_eq!(parse_curseforge_artifact("x:a:b"), Ok(("a", "b")));
}
#[test]
fn curse_artifact_invalid() {
assert!(parse_curseforge_artifact("x").is_err());
assert!(parse_curseforge_artifact("x:x:x:x").is_err());
}
}