legacympt-rs/mpt/src/util.rs
LordMZTE 5ead609763
Some checks failed
continuous-integration/drone/push Build is failing
0.1.3
2022-03-05 01:49:37 +01:00

321 lines
9.3 KiB
Rust

use addonscript::manifest::{link::Link, Manifest, Repository};
use crossterm::style::{Attribute, Attributes, Color, ContentStyle, Stylize};
use indicatif::ProgressStyle;
use log::info;
use miette::{Diagnostic, IntoDiagnostic, WrapErr};
use mlua::{Lua, LuaSerdeExt};
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() -> miette::Result<Config> {
info!("reading config");
let conf = tokio::fs::read("modpacktoolsconfig.toml")
.await
.into_diagnostic()
.wrap_err("Failed to read config")?;
toml::from_slice(&conf)
.into_diagnostic()
.wrap_err("Failed to parse config")
}
/// 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(
defines: Vec<String>,
command: &str,
) -> miette::Result<(Config, Manifest)> {
let config = parse_config().await?;
let src = Path::new(&config.locations.src);
let mut is_lua = false;
let path = if src.join("modpack.json5").exists() {
src.join("modpack.json5")
} else if src.join("modpack.json").exists() {
src.join("modpack.json")
} else {
is_lua = true;
src.join("modpack.lua")
};
info!("reading manifest");
let data = tokio::fs::read(path)
.await
.into_diagnostic()
.wrap_err("Failed to read manifest")?;
let manifest = if is_lua {
let lua = Lua::new();
let mpt = lua.create_table().into_diagnostic()?;
mpt.set("defines", defines).into_diagnostic()?;
mpt.set("command", command).into_diagnostic()?;
lua.globals().set("mpt", mpt).into_diagnostic()?;
lua.load(&data)
.exec()
.into_diagnostic()
.wrap_err("Failed to execute lua manifest")?;
let lua_manifest = lua
.globals()
.get("manifest")
.into_diagnostic()
.wrap_err("Failed to get manifest value")?;
lua.from_value(lua_manifest).into_diagnostic().wrap_err(
"Failed to deserialize lua manifest. Did you set the global `manifest` to the correct \
data?",
)?
} else {
let data = std::str::from_utf8(&data)
.into_diagnostic()
.wrap_err("Manifest is invalid UTF-8")?;
json5::from_str::<Manifest>(data)
.into_diagnostic()
.wrap_err("Failed to parse manifest")?
};
Ok((config, manifest))
}
#[derive(Debug, Error, Diagnostic, 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) -> miette::Result<()> {
info!("creating temporary directory");
tokio::fs::create_dir_all(&config.locations.temp_dir)
.await
.into_diagnostic()
.wrap_err("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, Diagnostic, 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()
.and_then(Iterator::last)
.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)?)
}
#[derive(Debug, Error, Diagnostic)]
pub enum LinkFileNameError {
#[error("Empty path has no file name!")]
EmptyPath,
#[error("Failed to get file name of URL link: {0}")]
Url(#[from] UrlFileNameError),
}
pub fn link_file_name(link: &Link) -> Result<String, LinkFileNameError> {
match link {
Link::File(path) => Ok(path
.file_name()
.ok_or(LinkFileNameError::EmptyPath)?
.to_string_lossy()
.to_string()),
Link::Http(url) => Ok(url_file_name(url)?),
}
}
/// Copies a directory inclding all files
pub async fn copy_dir(from: PathBuf, to: PathBuf) -> io::Result<()> {
info!(
"Copying directory {} to {}",
from.to_string_lossy(),
to.to_string_lossy()
);
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());
}
}