use anyhow::{Result, anyhow, bail}; use derive_more::Display; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tokio::fs; use crate::{model::{local, rainy}, util}; // {namespace}-{name} #[derive(Eq, Hash, PartialEq, Clone, Serialize, Deserialize, Display)] pub struct PkgKey(pub String); // {namespace}-{name}-{version} #[derive(Clone, Serialize, Deserialize)] 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 icon: String, pub loc: Option, pub rmt: Option } #[derive(Clone, Default, PartialEq, Serialize, Deserialize)] pub enum Kind { Unchecked, #[default] Mod, Unsupported } #[derive(Clone, Default, Serialize, Deserialize)] #[allow(dead_code)] pub struct Local { pub version: String, pub path: PathBuf, pub dependencies: Vec, pub kind: Kind } #[derive(Clone, Default, Serialize, Deserialize)] #[allow(dead_code)] pub struct Remote { pub version: String, pub package_url: String, pub download_url: String, pub deprecated: bool, pub dependencies: Vec } 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, icon: v.icon, loc: None, rmt: Some(Remote { package_url: p.package_url, download_url: v.download_url, deprecated: p.is_deprecated, version: v.version_number, 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 dependencies = Self::sanitize_deps(mft.dependencies); Ok(Package { namespace: Self::dir_to_namespace(&dir)?, name: mft.name.clone(), description: mft.description.clone(), icon, loc: Some(Local { version: mft.version_number, path: dir.to_owned(), kind: Kind::Mod, 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(mut deps: Vec) -> Vec { let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)-[0-9\.]+$") .expect("Invalid regex"); for i in 0..deps.len() { let caps = regex.captures(&deps[i].0) .expect("Invalid dependency"); deps[i] = PkgKeyVersion(format!("{}-{}", caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str())); } let rv: Vec = unsafe { std::mem::transmute(deps) }; rv } }