321 lines
9.3 KiB
Rust
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());
|
|
}
|
|
}
|