Files
STARTLINER/rust/src/pkg.rs

235 lines
7.3 KiB
Rust

use anyhow::{Result, anyhow, bail};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeSet, path::{Path, PathBuf}};
use tokio::fs;
use enumflags2::{bitflags, make_bitflags, BitFlags};
use crate::{model::{local::{self, PackageManifest}, misc::Game, rainy}, util};
// {namespace}-{name}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKey(pub String);
// {namespace}-{name}-{version}
#[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display, Debug)]
pub struct PkgKeyVersion(String);
#[derive(Copy, Clone, Display, Debug, Serialize, Deserialize, Default)]
pub enum PackageSource {
#[default] Rainy,
Local(Game)
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct Package {
pub namespace: String,
pub name: String,
pub description: String,
pub loc: Option<Local>,
pub rmt: Option<Remote>,
pub source: PackageSource,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
pub enum Status {
Unchecked,
Unsupported,
OK(BitFlags<Feature>)
}
#[bitflags]
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Feature {
Mod,
Aime,
AMNet,
Mu3Hook,
Mu3IO,
ChusanHook,
}
#[derive(Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct Local {
pub version: String,
pub path: PathBuf,
pub dependencies: BTreeSet<PkgKey>,
pub status: Status,
pub icon: String,
}
#[derive(Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct Remote {
pub version: String,
pub package_url: String,
pub download_url: String,
pub icon: String,
pub deprecated: bool,
pub nsfw: bool,
pub categories: Vec<String>,
pub dependencies: BTreeSet<PkgKey>,
}
impl Package {
pub fn from_rainy(mut p: rainy::V1Package) -> Option<Package> {
if p.versions.len() == 0 {
return None;
}
let v = p.versions.swap_remove(0);
Some(Package {
namespace: p.owner,
name: v.name,
description: v.description,
loc: None,
rmt: Some(Remote {
package_url: p.package_url,
download_url: v.download_url,
icon: v.icon,
deprecated: p.is_deprecated,
nsfw: p.has_nsfw_content,
version: v.version_number,
categories: p.categories,
dependencies: Self::sanitize_deps(v.dependencies)
}),
source: PackageSource::Rainy,
})
}
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<Package> {
let str = fs::read_to_string(dir.join("manifest.json")).await?;
let mft: local::PackageManifest = serde_json::from_str(&str)?;
let icon = dir.join("icon.png")
.as_os_str()
.to_str()
.unwrap()
.to_owned();
let status = Self::parse_status(&mft);
let dependencies = Self::sanitize_deps(mft.dependencies);
Ok(Package {
namespace: Self::dir_to_namespace(&dir)?,
name: mft.name.clone(),
description: mft.description.clone(),
loc: Some(Local {
version: mft.version_number,
path: dir.to_owned(),
icon,
status,
dependencies
}),
rmt: None,
source
})
}
pub fn key(&self) -> PkgKey {
PkgKey(format!("{}-{}", self.namespace, self.name))
}
pub fn path(&self) -> PathBuf {
match self.source {
PackageSource::Rainy => util::pkg_dir().join(self.key().0),
PackageSource::Local(game) =>
util::pkg_dir()
.parent()
.unwrap()
.join(format!("pkg-{game}"))
.join(&self.name),
}
}
pub fn _dir_to_key(dir: &Path) -> Result<String> {
let (key, _) = Self::parse_dir_name(dir)?;
Ok(key)
}
pub fn dir_to_namespace(dir: &Path) -> Result<String> {
let (_, n) = Self::parse_dir_name(dir)?;
Ok(n)
}
fn manifest(dir: &Path) -> Result<local::PackageManifest> {
serde_json::from_reader(std::fs::File::open(dir.join("manifest.json"))?)
.map_err(|err| anyhow!("Invalid manifest: {}", err))
}
fn parse_dir_name(dir: &Path) -> Result<(String, String)> {
let mft = Self::manifest(dir)?;
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$")?;
let dir_name = dir.file_name()
.to_owned()
.ok_or_else(|| anyhow!("Invalid directory name"))?
.to_str()
.ok_or_else(|| anyhow!("Illegal directory name"))?;
let namespace;
if let Some(caps) = regex.captures(dir_name) {
let name_match = caps.get(2)
.ok_or_else(|| anyhow!("Invalid directory name"))?;
if name_match.as_str() != mft.name {
bail!("Invalid manifest or directory name");
}
namespace = caps.get(1)
.ok_or_else(|| anyhow!("Invalid directory name?"))?
.as_str()
.to_owned();
Ok((format!("{}-{}", namespace, mft.name), namespace))
} else {
bail!("Error reading {}: invalid directory name", dir_name);
}
}
fn sanitize_deps(src: BTreeSet<PkgKeyVersion>) -> BTreeSet<PkgKey> {
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$")
.expect("Invalid regex");
let mut res = BTreeSet::<PkgKey>::new();
for dep in src {
let caps = regex.captures(&dep.0)
.expect("Invalid dependency");
res.insert(PkgKey(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())));
}
res
}
fn parse_status(mft: &PackageManifest) -> Status {
if mft.installers.len() == 0 {
return Status::OK(make_bitflags!(Feature::Mod));//Unchecked
} else if mft.installers.len() == 1 {
if let Some(serde_json::Value::String(id)) = &mft.installers[0].get("identifier") {
if id == "rainycolor" {
return Status::OK(make_bitflags!(Feature::Mod));
} else if id == "segatools" {
// Multiple features in the same dll (yubideck etc.) should be supported at some point
let mut flags = BitFlags::default();
if let Some(serde_json::Value::String(module)) = mft.installers[0].get("module") {
if module == "mu3hook" {
flags |= Feature::Mu3Hook;
} else if module == "chusanhook" {
flags |= Feature::ChusanHook;
} else if module == "amnet" {
flags |= Feature::AMNet | Feature::Aime;
} else if module == "aimeio" {
flags |= Feature::Aime;
} else if module == "mu3io" {
flags |= Feature::Mu3IO;
}
}
return Status::OK(flags);
}
}
}
Status::Unsupported
}
}