use crate::db::entities::package; use std::{ fmt, fs, io::{self, BufRead, BufReader, Read}, path::{Path, PathBuf}, }; use chrono::NaiveDateTime; use libarchive::{ read::{Archive, Builder}, Entry, ReadFilter, }; use sea_orm::ActiveValue::Set; #[derive(Debug, Clone)] pub struct Package { pub path: PathBuf, pub info: PkgInfo, pub files: Vec, pub compression: ReadFilter, } #[derive(Debug, Default, Clone)] pub struct PkgInfo { pub base: String, pub name: String, pub version: String, pub arch: String, pub description: Option, pub size: i64, pub csize: i64, pub url: Option, pub build_date: NaiveDateTime, pub packager: Option, pub pgpsig: Option, pub pgpsigsize: Option, pub groups: Vec, pub licenses: Vec, pub replaces: Vec, pub depends: Vec, pub conflicts: Vec, pub provides: Vec, pub optdepends: Vec, pub makedepends: Vec, pub checkdepends: Vec, pub sha256sum: String, } #[derive(Debug, PartialEq, Eq)] pub enum InvalidPkgInfoError { Size, BuildDate, PgpSigSize, } impl fmt::Display for InvalidPkgInfoError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Self::Size => "invalid size", Self::BuildDate => "invalid build date", Self::PgpSigSize => "invalid pgp sig size", }; write!(f, "{}", s) } } impl PkgInfo { pub fn extend>(&mut self, line: S) -> Result<(), InvalidPkgInfoError> { let line = line.as_ref(); if !line.starts_with('#') { if let Some((key, value)) = line.split_once('=').map(|(k, v)| (k.trim(), v.trim())) { match key { "pkgname" => self.name = value.to_string(), "pkgbase" => self.base = value.to_string(), "pkgver" => self.version = value.to_string(), "pkgdesc" => self.description = Some(value.to_string()), "size" => self.size = value.parse().map_err(|_| InvalidPkgInfoError::Size)?, "url" => self.url = Some(value.to_string()), "arch" => self.arch = value.to_string(), "builddate" => { let seconds: i64 = value.parse().map_err(|_| InvalidPkgInfoError::BuildDate)?; self.build_date = chrono::DateTime::from_timestamp_millis(seconds * 1000) .ok_or(InvalidPkgInfoError::BuildDate)? .naive_utc(); } "packager" => self.packager = Some(value.to_string()), "pgpsig" => self.pgpsig = Some(value.to_string()), "pgpsigsize" => { self.pgpsigsize = Some(value.parse().map_err(|_| InvalidPkgInfoError::PgpSigSize)?) } "group" => self.groups.push(value.to_string()), "license" => self.licenses.push(value.to_string()), "replaces" => self.replaces.push(value.to_string()), "depend" => self.depends.push(value.to_string()), "conflict" => self.conflicts.push(value.to_string()), "provides" => self.provides.push(value.to_string()), "optdepend" => self.optdepends.push(value.to_string()), "makedepend" => self.makedepends.push(value.to_string()), "checkdepend" => self.checkdepends.push(value.to_string()), _ => (), } } } Ok(()) } pub fn parse(reader: R) -> io::Result { let mut info = Self::default(); let buf_reader = BufReader::new(reader); for line in buf_reader.lines() { info.extend(line?).map_err(|e| { io::Error::new(io::ErrorKind::Other, format!("pkg info parse error: {}", e)) })?; } Ok(info) } } impl Package { pub fn open>(path: P) -> io::Result { let mut builder = Builder::new(); // There are chosen kind of arbitrarily, most likely only zstd, gzip and xz will ever be // used builder.support_filter(libarchive::ReadFilter::Zstd)?; builder.support_filter(libarchive::ReadFilter::Gzip)?; builder.support_filter(libarchive::ReadFilter::Xz)?; builder.support_filter(libarchive::ReadFilter::Bzip2)?; builder.support_filter(libarchive::ReadFilter::Lzma)?; builder.support_format(libarchive::ReadFormat::Tar)?; let mut ar = builder.open_file(path.as_ref())?; let compression = ar.filter(0).ok_or(io::Error::new( io::ErrorKind::Other, "Unknown compression type.", ))?; let mut info: Option = None; let mut files: Vec = Vec::new(); for entry in ar.entries() { let entry = entry?; let path_name = entry.pathname(); if !path_name.starts_with('.') { files.push(PathBuf::from(path_name)); } else if path_name == ".PKGINFO" { info = Some(PkgInfo::parse(entry)?); } } if let Some(mut info) = info { // I'll take my chances on a file size fitting in an i64 info.csize = fs::metadata(path.as_ref())?.len().try_into().unwrap(); info.sha256sum = sha256::try_digest(path.as_ref())?; Ok(Package { path: path.as_ref().to_path_buf(), info, compression, files, }) } else { Err(io::Error::new( io::ErrorKind::Other, "No .PKGINFO file found.", )) } } pub fn full_name(&self) -> String { format!( "{}-{}-{}", self.info.name, self.info.version, self.info.arch ) } pub fn file_name(&self) -> String { // This unwrap should be safe, because we only allow passing through compressions with // known file extensions format!( "{}.pkg.tar.{}", self.full_name(), self.compression.extension().unwrap() ) } } impl From for package::ActiveModel { fn from(pkg: Package) -> Self { let info = pkg.info; package::ActiveModel { base: Set(info.base), name: Set(info.name), version: Set(info.version), arch: Set(info.arch), size: Set(info.size), c_size: Set(info.csize), description: Set(info.description), url: Set(info.url), build_date: Set(info.build_date), packager: Set(info.packager), pgp_sig: Set(info.pgpsig), pgp_sig_size: Set(info.pgpsigsize), sha256_sum: Set(info.sha256sum), ..Default::default() } } }