use chrono::NaiveDateTime; use libarchive::read::{Archive, Builder}; use libarchive::{Entry, ReadFilter}; use sea_orm::ActiveValue::Set; use std::fmt; use std::fs; use std::io::{self, BufRead, BufReader, BufWriter, Read, Write}; use std::path::{Path, PathBuf}; use crate::db::entities::package; const IGNORED_FILES: [&str; 5] = [".BUILDINFO", ".INSTALL", ".MTREE", ".PKGINFO", ".CHANGELOG"]; #[derive(Debug)] pub struct Package { pub path: PathBuf, pub info: PkgInfo, pub files: Vec, pub compression: ReadFilter, } #[derive(Debug, Default)] 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 ParsePkgInfoError { InvalidSize, InvalidBuildDate, InvalidPgpSigSize, } impl fmt::Display for ParsePkgInfoError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { Self::InvalidSize => "invalid size", Self::InvalidBuildDate => "invalid build date", Self::InvalidPgpSigSize => "invalid pgp sig size", }; write!(f, "{}", s) } } impl PkgInfo { pub fn extend>(&mut self, line: S) -> Result<(), ParsePkgInfoError> { 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(|_| ParsePkgInfoError::InvalidSize)? } "url" => self.url = Some(value.to_string()), "arch" => self.arch = value.to_string(), "builddate" => { let seconds: i64 = value .parse() .map_err(|_| ParsePkgInfoError::InvalidBuildDate)?; self.build_date = NaiveDateTime::from_timestamp_millis(seconds * 1000) .ok_or(ParsePkgInfoError::InvalidBuildDate)? } "packager" => self.packager = Some(value.to_string()), "pgpsig" => self.pgpsig = Some(value.to_string()), "pgpsigsize" => { self.pgpsigsize = Some( value .parse() .map_err(|_| ParsePkgInfoError::InvalidPgpSigSize)?, ) } "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 !IGNORED_FILES.iter().any(|p| p == &path_name) { files.push(PathBuf::from(path_name)); } 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() ) } /// Write the formatted desc file to the provided writer pub fn write_desc(&self, w: &mut W) -> io::Result<()> { // We write a lot of small strings to the writer, so wrapping it in a BufWriter is // beneficial let mut w = BufWriter::new(w); let info = &self.info; writeln!(w, "%FILENAME%\n{}", self.file_name())?; let mut write = |key: &str, value: &str| { if !value.is_empty() { writeln!(w, "\n%{}%\n{}", key, value) } else { Ok(()) } }; write("NAME", &info.name)?; write("BASE", &info.base)?; write("VERSION", &info.version)?; if let Some(ref description) = info.description { write("DESC", description)?; } write("GROUPS", &info.groups.join("\n"))?; write("CSIZE", &info.csize.to_string())?; write("ISIZE", &info.size.to_string())?; write("SHA256SUM", &info.sha256sum)?; if let Some(ref url) = info.url { write("URL", url)?; } write("LICENSE", &info.licenses.join("\n"))?; write("ARCH", &info.arch)?; write("BUILDDATE", &info.build_date.timestamp().to_string())?; if let Some(ref packager) = info.packager { write("PACKAGER", packager)?; } write("REPLACES", &info.replaces.join("\n"))?; write("CONFLICTS", &info.conflicts.join("\n"))?; write("PROVIDES", &info.provides.join("\n"))?; write("DEPENDS", &info.depends.join("\n"))?; write("OPTDEPENDS", &info.optdepends.join("\n"))?; write("MAKEDEPENDS", &info.makedepends.join("\n"))?; write("CHECKDEPENDS", &info.checkdepends.join("\n"))?; Ok(()) } pub fn write_files(&self, w: &mut W) -> io::Result<()> { // We write a lot of small strings to the writer, so wrapping it in a BufWriter is // beneficial let mut w = BufWriter::new(w); writeln!(w, "%FILES%")?; for file in &self.files { writeln!(w, "{}", file.to_string_lossy())?; } Ok(()) } } 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() } } }