− use lazy_static::lazy_static;
− use std::collections::HashMap;
− use std::ffi::OsStr;
− use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
− use thiserror::*;
− use tracing::*;
− mod index;
− pub use index::*;
−
− /// A parsed .deb file.
− #[derive(Debug)]
− pub struct Deb {
− md5sums: HashMap<String, String>,
− control_file: String,
− _global: Header,
− _control: Header,
− data: Header,
− data_offset: u64,
− }
−
− /// A "stanza", i.e. a description of a Debian package.
− #[derive(Debug, Default, Clone)]
− pub struct Stanza<'a> {
− #[allow(missing_docs)]
− pub package: &'a str,
− #[allow(missing_docs)]
− pub version: Version<'a>,
− #[allow(missing_docs)]
− pub architecture: &'a str,
− #[allow(missing_docs)]
− pub depends: Vec<Dep<'a>>,
− #[allow(missing_docs)]
− pub sha256: Option<&'a str>,
− #[allow(missing_docs)]
− pub size: Option<usize>,
− /// Path of the .deb file inside an index.
− pub file_name: Option<&'a str>,
− }
−
− /// A dependency, which is either a "simple" dependency, or a list of
− /// alternatives.
− #[derive(Debug, Clone)]
− pub enum Dep<'a> {
− /// A simple dependency.
− Simple(SimpleDep<'a>),
− /// A dependency that can be supplied by multiple packages. This
− /// is an alternative mechanism to virtual packages.
− Alternatives {
− #[allow(missing_docs)]
− alt: Vec<SimpleDep<'a>>,
− },
− }
−
− /// A single dependency.
− #[derive(Debug, Clone)]
− pub struct SimpleDep<'a> {
− /// Name of the dependency.
− pub name: &'a str,
− /// Whether this dependency can have any version.
− pub any: bool,
− /// Constraints on dependencies.
− pub constraints: Vec<(&'a str, Version<'a>)>,
− }
−
− fn parse_control(s: &str) -> IResult<&str, Stanza> {
− let mut st = Stanza::default();
−
− for l in s.lines() {
− if l.is_empty() {
− let (_, b) = s.split_at(l.as_ptr() as usize - s.as_ptr() as usize);
− return Ok((b, st));
− }
−
− if let Some(i) = l.find(':') {
− let (key, val) = l.split_at(i);
− let (_, val) = val.split_at(1);
− debug!("key {:?} {:?}", key, val);
− match key.trim() {
− "Package" => st.package = val.trim(),
− "Version" => st.version = parse_version(val.trim()).unwrap().1,
− "Architecture" => st.architecture = val.trim(),
− "SHA256" => st.sha256 = Some(val.trim()),
− "Filename" => st.file_name = Some(val.trim()),
− "Depends" | "Pre-Depends" => {
− let d = parse_deps(val.trim());
− debug!("{:?}", d);
− if let Ok(("", a)) = d {
− st.depends = a
− } else {
− error!("error {:?}", val.trim());
− return IResult::Err(nom::Err::Error(nom::error::make_error(
− s,
− nom::error::ErrorKind::Tag,
− )));
− }
− }
− _ => {}
− }
− }
− }
−
− Ok(("", st))
− }
−
− #[test]
− fn test_grub_common() {
− use tracing_subscriber::prelude::*;
− use tracing_subscriber::util::SubscriberInitExt;
− tracing_subscriber::registry()
− .with(
− tracing_subscriber::EnvFilter::try_from_default_env()
− .unwrap_or_else(|_| String::new().into()),
− )
− .with(tracing_subscriber::fmt::layer())
− .try_init()
− .unwrap();
−
− let (rest, p) = parse_control("Package: grub-common\nArchitecture: amd64\nVersion: 2.12-1ubuntu7\nBuilt-Using: lzo2 (= 2.10-2build3)\nMulti-Arch: foreign\nPriority: optional\nSection: admin\nSource: grub2\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\nOriginal-Maintainer: GRUB Maintainers <pkg-grub-devel@alioth-lists.debian.net>\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 12476\nDepends: libc6 (>= 2.38), libdevmapper1.02.1 (>= 2:1.02.36), libefiboot1t64 (>=38), libefivar1t64 (>= 38), libfreetype6 (>= 2.2.1), libfuse3-3 (>= 3.2.3), liblzma5 (>= 5.1.1alpha+20120614), debconf (>= 0.5) | debconf-2.0, gettext-base, lsb-base (>= 3.0-6), python3, python3-apt\nRecommends: os-prober (>= 1.33)\nSuggests: multiboot-doc, grub-emu, mtools, xorriso (>= 0.5.6.pl00), desktop-base (>= 4.0.6), console-setup\nConflicts: init-select\nBreaks: apport (<< 2.1.1), friendly-recovery (<< 0.2.13), lupin-support (<< 0.55), mdadm (<< 2.6.7-2)\nReplaces: grub-coreboot (<< 2.00-4), grub-efi (<< 1.99-1), grub-efi-amd64 (<< 2.00-4), grub-efi-ia32 (<< 2.00-4), grub-efi-ia64 (<< 2.00-4), grub-ieee1275 (<< 2.00-4), grub-linuxbios (<< 1.96+20080831-1), grub-pc (<< 2.00-4), grub-yeeloong (<< 2.00-4), init-select\nFilename: pool/main/g/grub2/grub-common_2.12-1ubuntu7_amd64.deb\nSize: 2119862\nMD5sum: 86e63081ae4ee34d6a4f408ac6dbf0e4\nSHA1: b98f9f88e5480423e041fd647b438ba2f6e6f5ea\nSHA256: d8110b97b6fb6da40449c74b9cb527f02965dffaae8344399a711617f5a69249\nSHA512: 85c63b8d3e6a870df48533ab525df13abe9ec4861ff4b5577def94f65a2ebf6ac44a54a6cd0eca1c825e1c7aa2bd14e4d4ed53f83d381963ac1dc51daa16482a\nHomepage: https://www.gnu.org/software/grub/\nDescription: GRand Unified Bootloader (common files)\nTask: ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, kubuntu-desktop, xubuntu-minimal, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop,ubuntukylin-desktop-minimal, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop-minimal, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi, ubuntu-unity-desktop, edubuntu-desktop-gnome-minimal, edubuntu-desktop-gnome-raspi, ubuntucinnamon-desktop-minimal, ubuntucinnamon-desktop-raspi\nDescription-md5: 9c75036dc0a0792fedbc58df208ed227").unwrap();
−
− assert!(p.depends.iter().any(|x| match x {
− Dep::Simple(s) => s.name == "liblzma5",
− _ => false,
− }));
−
− assert!(rest.is_empty())
− }
−
− /// A version of a package.
− #[derive(Debug, Clone)]
− pub struct Version<'a> {
− /// Epoch, i.e. a Debian mechanism to reorder upstream versions if
− /// necessary, for example if the upstream versioning scheme
− /// changes.
− pub epoch: u32,
− /// Upstream version.
− pub upstream_version: Upstream<'a>,
− /// Debian-specific suffix to distinguish between patches.
− pub debian: Option<&'a str>,
− }
−
− impl<'a> Default for Version<'a> {
− fn default() -> Version<'a> {
− Version {
− epoch: 0,
− upstream_version: Upstream(""),
− debian: None,
− }
− }
− }
−
− fn parse_constraint<'a>(s: &'a str) -> IResult<&'a str, (&'a str, Version<'a>)> {
− let (s, constraint) = tag("<<")
− .or(tag("<="))
− .or(tag("="))
− .or(tag(">="))
− .or(tag(">>"))
− .parse(s)?;
− debug!("parse_constraint {:?}", constraint);
− let (s, _) = space0(s)?;
− let (s, version) = parse_version(s)?;
− debug!("parse_constraint {:?} {:?}", s, (constraint, &version));
− Ok((s, (constraint, version)))
− }
−
− fn parse_dep<'a>(s0: &'a str) -> IResult<&'a str, Dep<'a>> {
− let (s, name) = parse_name(s0)?;
− let (s, any) = if s.starts_with(":any") {
− let (_, b) = s.split_at(4);
− (b, true)
− } else {
− (s, false)
− };
− let (s, _) = space0(s)?;
− let mut constraints = Vec::new();
− if let Ok((s, _)) = tag::<_, _, ()>("(").parse(s) {
− let (s, version) = if let Some(i) = s.find(')') {
− let (a, b) = s.split_at(i);
− let (_, b) = b.split_at(1);
− (b, a)
− } else {
− return IResult::Err(nom::Err::Error(nom::error::make_error(
− s,
− nom::error::ErrorKind::Tag,
− )));
− };
−
− let mut v = version;
− while let Ok((v_, c)) = parse_constraint(v) {
− constraints.push(c);
− let (v_, _) = space0(v_)?;
− if let Ok((v_, _)) = tag::<_, _, ()>(",").parse(v_) {
− let (v_, _) = space0(v_)?;
− v = v_;
− } else {
− break;
− }
− }
−
− Ok((
− s,
− Dep::Simple(SimpleDep {
− name,
− any,
− constraints,
− }),
− ))
− } else {
− Ok((
− s,
− Dep::Simple(SimpleDep {
− name,
− any,
− constraints,
− }),
− ))
− }
− }
−
− fn parse_name(s: &str) -> IResult<&str, &str> {
− if let Some(i) = s.find(|c| {
− !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-')
− }) {
− if i > 0 {
− let (a, b) = s.split_at(i);
− return Ok((b, a));
− }
− } else {
− return Ok(("", s));
− }
− error!("ERROR {:?}", s);
− return IResult::Err(nom::Err::Error(nom::error::make_error(
− s,
− nom::error::ErrorKind::Tag,
− )));
− }
−
− fn parse_deps<'a>(s: &'a str) -> IResult<&'a str, Vec<Dep<'a>>> {
− let mut d = Vec::new();
− let (mut s, _) = space0(s)?;
− let mut alt_is_open = false;
− while let Ok((s_, d_)) = parse_dep(s) {
− let (s_, _) = space0(s_)?;
− if let Ok((s_, _)) = tag::<_, _, ()>("|").parse(s_) {
− let (s_, _) = space0(s_)?;
− s = s_;
− alt_is_open = true;
− if let Dep::Simple(d_) = d_ {
− if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
− alt.push(d_)
− } else {
− d.push(Dep::Alternatives { alt: vec![d_] })
− }
− } else {
− panic!("{:?}", s);
− }
− } else if let Ok((s_, _)) = tag::<_, _, ()>(",").parse(s_) {
− if alt_is_open {
− if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
− if let Dep::Simple(d_) = d_ {
− alt.push(d_)
− } else {
− panic!("{:?}", s);
− }
− }
− } else {
− d.push(d_)
− }
− alt_is_open = false;
− s = s_;
− } else {
− if alt_is_open {
− if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
− if let Dep::Simple(d_) = d_ {
− alt.push(d_)
− } else {
− panic!("{:?}", s);
− }
− }
− } else {
− d.push(d_)
− }
− s = s_;
− break;
− }
− let (s_, _) = space0(s)?;
− s = s_;
− }
− Ok((s, d))
− }
−
− /// The representation of an upstream version. Will implement `Ord`.
− #[derive(Debug, Clone, PartialEq, Eq)]
− pub struct Upstream<'a>(&'a str);
−
− impl<'a> std::ops::Deref for Upstream<'a> {
− type Target = str;
− fn deref(&self) -> &str {
− self.0
− }
− }
−
− impl<'a> PartialOrd for Upstream<'a> {
− /// Following the algorithm described here:
− /// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
− fn partial_cmp(&self, b: &Upstream<'a>) -> Option<std::cmp::Ordering> {
− fn split_initial(s: &str) -> (&str, &str, &str) {
− // Find `u`, the first digit, and `v > u`, the first non-digit after `u`.
− let mut u = s.len();
− let mut v = s.len();
− for (n, &b) in s.as_bytes().iter().enumerate() {
− if u == s.len() {
− if b >= b'0' && b <= b'9' {
− u = n
− }
− } else if b < b'0' || b > b'9' {
− v = n;
− break;
− }
− }
− let (ab, c) = s.split_at(v);
− let (a, b) = ab.split_at(u);
− (a, b, c)
− }
− let mut a = self.0;
− let mut b = b.0;
− use std::cmp::Ordering;
− loop {
− let (a0, a1, a_) = split_initial(a);
− let (b0, b1, b_) = split_initial(b);
− // Compare a0 / b0 using the modified ASCII ordering described in the Debian manual.
− let mut a0i = a0.as_bytes().iter();
− let mut b0i = b0.as_bytes().iter();
− loop {
− match (a0i.next(), b0i.next()) {
− (None, None) => break,
− (Some(c), Some(d)) if c == d => continue,
− (Some(b'~'), _) => return Some(Ordering::Less),
− (_, Some(b'~')) => return Some(Ordering::Greater),
− (Some(c), Some(d)) if c.is_ascii_alphabetic() && !d.is_ascii_alphabetic() => {
− return Some(Ordering::Less);
− }
− (Some(c), Some(d)) if d.is_ascii_alphabetic() && !c.is_ascii_alphabetic() => {
− return Some(Ordering::Greater);
− }
− (c, d) => return Some(c.cmp(&d)),
− }
− }
− // If we haven't returned, a0 == b0. Compare the digits part.
− let a1 = if a1.is_empty() {
− 0
− } else {
− a1.parse::<u64>().unwrap()
− };
− let b1 = if b1.is_empty() {
− 0
− } else {
− b1.parse::<u64>().unwrap()
− };
− let cmp1 = a1.cmp(&b1);
− if let Ordering::Equal = cmp1 {
− a = a_;
− b = b_;
− } else {
− return Some(cmp1);
− }
− }
− }
− }
−
− impl<'a> Ord for Upstream<'a> {
− fn cmp(&self, b: &Upstream<'a>) -> std::cmp::Ordering {
− self.partial_cmp(b).unwrap()
− }
− }
−
− #[test]
− fn test_upstream_ordering() {
− let mut x = [
− Upstream("~~"),
− Upstream("~~a"),
− Upstream("~"),
− Upstream(""),
− Upstream("a"),
− ];
− let y = x.clone();
− x.sort();
− assert_eq!(x, y,);
− }
−
− use nom::{
− bytes::tag,
− character::complete::{digit1, newline, space0},
− multi::separated_list1,
− IResult, Parser,
− };
−
− /// Parse a version in the Debian version format.
− fn parse_version<'a>(s: &'a str) -> IResult<&'a str, Version<'a>> {
− fn epoch(s: &str) -> IResult<&str, u32> {
− let (s, i) = digit1(s)?;
− let (s, _) = tag(":").parse(s)?;
− Ok((s, i.parse().unwrap()))
− }
−
− let (s, epoch) = epoch(s).unwrap_or((s, 0));
− lazy_static! {
− static ref RE_DASH: regex::Regex =
− regex::Regex::new(r"^[a-z0-9\.~+-]+-([a-z0-9\.~+]+)").unwrap();
− static ref RE: regex::Regex = regex::Regex::new(r"^[a-z0-9\.~+]+").unwrap();
− }
−
− let (s, v) = if let Some(m) = RE_DASH.find(s) {
− debug!("parse_version RE_DASH {:?}", s);
− let (a, b) = s.split_at(m.end());
− (b, a)
− } else if let Some(m) = RE.find(s) {
− debug!("parse_version RE {:?}", s);
− let (a, b) = s.split_at(m.end());
− (b, a)
− } else {
− debug!("parse_version failed {:?}", s);
− return IResult::Err(nom::Err::Error(nom::error::make_error(
− s,
− nom::error::ErrorKind::Tag,
− )));
− };
−
− let (upstream_version, debian) = if let Some(i) = v.rfind('-') {
− let (a, b) = v.split_at(i);
− (Upstream(a), Some(b.split_at(1).1))
− } else {
− (Upstream(v), None)
− };
−
− Ok((
− s,
− Version {
− epoch,
− upstream_version,
− debian,
− },
− ))
− }
−
− /// Structure of the "headers" in the debian file. No alignment
− /// necessary, the numbers are in decimal. No heap allocation.
− #[derive(Debug)]
− #[repr(C)]
− struct H {
− file_id: [u8; 16],
− timestamp: [u8; 12],
− owner_id: [u8; 6],
− group_id: [u8; 6],
− file_mode: [u8; 8],
− file_size: [u8; 10],
− end_char: [u8; 2],
− }
−
− /// Parsed version of `H`, still no heap allocation.
− #[allow(dead_code)]
− #[derive(Debug)]
− struct Header {
− file_id: [u8; 16],
− timestamp: u64,
− owner_id: u32,
− group_id: u32,
− file_mode: u32,
− file_size: u64,
− }
−
− #[allow(missing_docs)]
− /// Possible errors for .deb files.
− #[derive(Debug, Error)]
− pub enum Error {
− #[error("Deb file parse error")]
− ParseError,
− #[error(transparent)]
− Utf8(#[from] std::str::Utf8Error),
− #[error(transparent)]
− Int(#[from] std::num::ParseIntError),
− #[error(transparent)]
− IO(#[from] std::io::Error),
− #[error("Unsupported compression algorithm: {file_id}")]
− UnsupportedCompression { file_id: String },
− #[error("Version parse: {version}")]
− Version { version: String },
− }
−
− impl Header {
− pub fn file_id(&self) -> &str {
− std::str::from_utf8(&self.file_id).unwrap().trim()
− }
− }
−
− impl H {
− fn parse(&self) -> Result<Header, Error> {
− std::str::from_utf8(&self.file_id)?;
− Ok(Header {
− file_id: self.file_id,
− timestamp: std::str::from_utf8(&self.timestamp)?.trim().parse()?,
− owner_id: std::str::from_utf8(&self.owner_id)?.trim().parse()?,
− group_id: std::str::from_utf8(&self.group_id)?.trim().parse().unwrap(),
− file_mode: u32::from_str_radix(std::str::from_utf8(&self.file_mode)?.trim(), 8)?,
− file_size: std::str::from_utf8(&self.file_size)?.trim().parse()?,
− })
− }
− }
−
− fn read_hdr<R: Read>(r: &mut R) -> Result<Header, Error> {
− unsafe {
− assert_eq!(size_of::<H>(), HEADER_SIZE as usize);
− let mut b: H = std::mem::zeroed();
− let pb: *mut H = &mut b;
− r.read_exact(&mut std::slice::from_raw_parts_mut(
− pb as *mut u8,
− size_of::<H>(),
− ))?;
− debug!("{:?}", b);
− b.parse()
− }
− }
−
− const HEADER_SIZE: u64 = 60;
−
− impl Deb {
− /// Parse the control part, containing the description (stanza) of this package.
− pub fn parse_control(&self) -> Vec<Stanza> {
− if let Ok(("", st)) = separated_list1(newline, parse_control).parse(&self.control_file) {
− st
− } else {
− Vec::new()
− }
− }
−
− /// Return the md5sums of each file.
− pub fn md5sums(&self) -> &HashMap<String, String> {
− &self.md5sums
− }
−
− /// Decompress the data part of the deb file into a directory.
− pub fn decompress<R: BufRead + Seek, P: AsRef<std::path::Path>>(
− &self,
− mut r: R,
− path: P,
− ) -> Result<(), Error> {
− r.seek(SeekFrom::Start(self.data_offset))?;
− debug!("decompress {:?}", self.data.file_id());
− if self.data.file_id().ends_with(".tar.gz") {
− unimplemented!()
− } else if self.data.file_id().ends_with(".tar.zst") {
− let mut ar = tar::Archive::new(
− zstd::stream::read::Decoder::new(r.take(self.data.file_size)).unwrap(),
− );
− for e in ar.entries().unwrap() {
− let mut e = e.unwrap();
− debug!("zstd decompress {:?}", e.path());
− // f(&e.path()?);
− e.unpack_in(&path).unwrap();
− }
− } else if self.data.file_id().ends_with(".tar.xz") {
− let mut ar = tar::Archive::new(xz::read::XzDecoder::new(r.take(self.data.file_size)));
− for e in ar.entries().unwrap() {
− let mut e = e.unwrap();
− debug!("xz decompress {:?}", e.path());
− // f(&e.path()?);
− e.unpack_in(&path).unwrap();
− }
− } else if self.data.file_id().ends_with(".tar") {
− let mut ar = tar::Archive::new(r.take(self.data.file_size));
− for e in ar.entries().unwrap() {
− let mut e = e.unwrap();
− debug!("tar extract {:?}", e.path());
− // f(&e.path()?);
− e.unpack_in(&path).unwrap();
− }
− } else {
− unimplemented!()
− };
− Ok(())
− }
−
− /// Parse a `.deb` file from a buffered reader.
− pub fn read<R: BufRead + Seek>(mut r: R) -> Result<Deb, Error> {
− let mut b = [0; 8];
− r.read_exact(&mut b)?;
− if &b != b"!<arch>\n" {
− return Err(Error::ParseError);
− }
−
− let global = read_hdr(&mut r)?;
− trace!("GLOBAL {:?}", global);
− assert_eq!(global.file_size, 4);
− r.seek(SeekFrom::Current(global.file_size as i64))?;
−
− // r.skip_until(b'c')?; // 'control.…'
− // r.seek(SeekFrom::Current(-1))?;
− let control = read_hdr(&mut r)?;
− trace!("CONTROL {:?}", control);
−
− // Control files are expected to be short, decompress.
− let mut ctrl = vec![0; control.file_size as usize];
− r.read_exact(&mut ctrl)?;
−
− // gzip xz zstd none
− let ctrl_tar = if control.file_id().ends_with(".tar.gz") {
− unimplemented!()
− } else if control.file_id().ends_with(".tar.zst") {
− zstd::decode_all(&ctrl[..])?
− } else if control.file_id().ends_with(".tar.xz") {
− let mut decompressor = xz::read::XzDecoder::new(&ctrl[..]);
− let mut result = Vec::new();
− decompressor.read_to_end(&mut result)?;
− result
− } else if control.file_id().ends_with(".tar") {
− ctrl
− } else {
− return Err(Error::UnsupportedCompression {
− file_id: control.file_id().to_string(),
− });
− };
− let mut ctrl_tar = tar::Archive::new(&ctrl_tar[..]);
−
− let mut control_file = String::new();
− let mut md5sums = HashMap::new();
− for e in ctrl_tar.entries().unwrap() {
− let mut e = e.unwrap();
− if e.path()?.file_name() == Some(OsStr::new("control")) {
− e.read_to_string(&mut control_file)?;
− } else if e.path()?.file_name() == Some(OsStr::new("md5sums")) {
− let mut e = BufReader::new(e);
− let mut s = String::new();
− while e.read_line(&mut s)? > 0 {
− let mut it = s.split(' ').filter(|x| !x.is_empty());
− if let (Some(hash), Some(file)) = (it.next(), it.next()) {
− md5sums.insert(file.trim().to_string(), hash.trim().to_string());
− }
− s.clear();
− }
− }
− }
−
− if control.file_size % 2 == 1 {
− r.seek(SeekFrom::Current(1))?;
− }
− let data = read_hdr(&mut r)?;
− debug!("data = {:?}", data);
−
− Ok(Deb {
− md5sums,
− _global: global,
− _control: control,
− control_file,
− data,
− data_offset: r.seek(SeekFrom::Current(0))?,
− })
− }
− }