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}, rainy}, util}; // {namespace}-{name} #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)] pub struct PkgKey(pub String); // {namespace}-{name}-{version} #[derive(Eq, Hash, PartialEq, PartialOrd, Ord, Clone, Serialize, Deserialize, Display)] pub struct PkgKeyVersion(String); #[derive(Clone, Default, Serialize, Deserialize)] #[allow(dead_code)] pub struct Package { pub namespace: String, pub name: String, pub description: String, pub loc: Option, pub rmt: Option } #[derive(Clone, PartialEq, Serialize, Deserialize)] pub enum Status { Unchecked, Unsupported, OK(BitFlags) } #[bitflags] #[repr(u8)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Feature { Mod, Hook, GameIO, Aime, AMNet, } #[derive(Clone, Serialize, Deserialize)] #[allow(dead_code)] pub struct Local { pub version: String, pub path: PathBuf, pub dependencies: BTreeSet, 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, pub dependencies: BTreeSet, } impl Package { pub fn from_rainy(mut p: rainy::V1Package) -> Option { 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) }) }) } pub async fn from_dir(dir: PathBuf) -> Result { 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 }) } pub fn key(&self) -> PkgKey { PkgKey(format!("{}-{}", self.namespace, self.name)) } pub fn path(&self) -> PathBuf { util::pkg_dir().join(self.key().0) } pub fn _dir_to_key(dir: &Path) -> Result { let (key, _) = Self::parse_dir_name(dir)?; Ok(key) } pub fn dir_to_namespace(dir: &Path) -> Result { let (_, n) = Self::parse_dir_name(dir)?; Ok(n) } fn manifest(dir: &Path) -> Result { 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) -> BTreeSet { let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$") .expect("Invalid regex"); let mut res = BTreeSet::::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.ends_with("hook") { flags |= Feature::Hook; } else if module == "amnet" { flags |= Feature::AMNet | Feature::Aime; } else if module == "aimeio" { flags |= Feature::Aime; } else if module.ends_with("io") { flags |= Feature::GameIO; } } return Status::OK(flags); } } } Status::Unsupported } }