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::*;
#[derive(Debug)]
pub struct Deb {
md5sums: HashMap<String, String>,
control_file: String,
_global: Header,
_control: Header,
data: Header,
data_offset: u64,
}
#[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>,
pub file_name: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub enum Dep<'a> {
Simple(SimpleDep<'a>),
Alternatives {
#[allow(missing_docs)]
alt: Vec<SimpleDep<'a>>,
},
}
#[derive(Debug, Clone)]
pub struct SimpleDep<'a> {
pub name: &'a str,
pub any: bool,
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())
}
#[derive(Debug, Clone)]
pub struct Version<'a> {
pub epoch: u32,
pub upstream_version: Upstream<'a>,
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))
}
#[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> {
fn partial_cmp(&self, b: &Upstream<'a>) -> Option<std::cmp::Ordering> {
fn split_initial(s: &str) -> (&str, &str, &str) {
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);
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)),
}
}
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,
};
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,
},
))
}
#[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],
}
#[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)]
#[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 {
pub fn parse_control(&self) -> Vec<Stanza> {
if let Ok(("", st)) = separated_list1(newline, parse_control).parse(&self.control_file) {
st
} else {
Vec::new()
}
}
pub fn md5sums(&self) -> &HashMap<String, String> {
&self.md5sums
}
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());
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());
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());
e.unpack_in(&path).unwrap();
}
} else {
unimplemented!()
};
Ok(())
}
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))?;
let control = read_hdr(&mut r)?;
trace!("CONTROL {:?}", control);
let mut ctrl = vec![0; control.file_size as usize];
r.read_exact(&mut ctrl)?;
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))?,
})
}
}