use {
crate::{format::XarChecksum, Error, XarResult},
base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
digest::DynDigest,
serde::Deserialize,
std::{
fmt::{Display, Formatter},
io::{Read, Write},
ops::{Deref, DerefMut},
},
x509_certificate::{CapturedX509Certificate, X509CertificateError},
xml::{
common::XmlVersion,
writer::{EmitterConfig, EventWriter, XmlEvent},
},
};
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TableOfContents {
toc: XarToC,
}
impl Deref for TableOfContents {
type Target = XarToC;
fn deref(&self) -> &Self::Target {
&self.toc
}
}
impl DerefMut for TableOfContents {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.toc
}
}
impl TableOfContents {
pub fn from_reader(reader: impl Read) -> XarResult<Self> {
Ok(serde_xml_rs::from_reader(reader)?)
}
pub fn files(&self) -> XarResult<Vec<(String, File)>> {
let mut files = self
.toc
.files
.iter()
.map(|f| f.files(None))
.collect::<XarResult<Vec<_>>>()?
.into_iter()
.flat_map(|x| x.into_iter())
.collect::<Vec<_>>();
files.sort_by(|a, b| a.1.id.cmp(&b.1.id));
Ok(files)
}
pub fn to_xml(&self) -> XarResult<Vec<u8>> {
let mut emitter = EmitterConfig::new().create_writer(std::io::BufWriter::new(vec![]));
self.write_xml(&mut emitter)?;
emitter
.into_inner()
.into_inner()
.map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))
}
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>) -> XarResult<()> {
writer.write(XmlEvent::StartDocument {
version: XmlVersion::Version10,
encoding: Some("UTF-8"),
standalone: None,
})?;
writer.write(XmlEvent::start_element("xar"))?;
writer.write(XmlEvent::start_element("toc"))?;
writer.write(XmlEvent::start_element("creation-time"))?;
writer.write(XmlEvent::characters(&self.creation_time))?;
writer.write(XmlEvent::end_element())?;
self.checksum.write_xml(writer)?;
if let Some(sig) = &self.signature {
sig.write_xml(writer, "signature")?;
}
if let Some(sig) = &self.x_signature {
sig.write_xml(writer, "x-signature")?;
}
for file in &self.files {
file.write_xml(writer)?;
}
writer.write(XmlEvent::end_element().name("toc"))?;
writer.write(XmlEvent::end_element().name("xar"))?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct XarToC {
pub creation_time: String,
pub checksum: Checksum,
#[serde(rename = "file")]
pub files: Vec<File>,
pub signature: Option<Signature>,
pub x_signature: Option<Signature>,
}
impl XarToC {
pub fn signatures(&self) -> Vec<&Signature> {
let mut res = vec![];
if let Some(sig) = &self.signature {
res.push(sig);
}
if let Some(sig) = &self.x_signature {
res.push(sig);
}
res
}
pub fn find_signature(&self, style: SignatureStyle) -> Option<&Signature> {
self.signatures().into_iter().find(|sig| sig.style == style)
}
pub fn visit_files_mut(&mut self, cb: &dyn Fn(&mut File)) {
for file in self.files.iter_mut() {
cb(file);
file.visit_files_mut(cb);
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Checksum {
pub style: ChecksumType,
pub offset: u64,
pub size: u64,
}
impl Checksum {
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>) -> XarResult<()> {
writer.write(XmlEvent::start_element("checksum").attr("style", &self.style.to_string()))?;
writer.write(XmlEvent::start_element("offset"))?;
writer.write(XmlEvent::characters(&format!("{}", self.offset)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("size"))?;
writer.write(XmlEvent::characters(&format!("{}", self.size)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element().name("checksum"))?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChecksumType {
#[serde(alias = "NONE")]
None,
#[serde(alias = "SHA1")]
Sha1,
#[serde(alias = "SHA256")]
Sha256,
#[serde(alias = "SHA512")]
Sha512,
#[serde(alias = "MD5")]
Md5,
}
impl Display for ChecksumType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => f.write_str("none"),
Self::Sha1 => f.write_str("sha1"),
Self::Sha256 => f.write_str("sha256"),
Self::Sha512 => f.write_str("sha512"),
Self::Md5 => f.write_str("md5"),
}
}
}
impl TryFrom<XarChecksum> for ChecksumType {
type Error = Error;
fn try_from(v: XarChecksum) -> Result<Self, Self::Error> {
match v {
XarChecksum::None => Ok(Self::None),
XarChecksum::Sha1 => Ok(Self::Sha1),
XarChecksum::Md5 => Ok(Self::Md5),
XarChecksum::Sha256 => Ok(Self::Sha256),
XarChecksum::Sha512 => Ok(Self::Sha512),
XarChecksum::Other(_) => Err(Error::Unsupported("unknown checksum type")),
}
}
}
impl From<ChecksumType> for XarChecksum {
fn from(v: ChecksumType) -> Self {
match v {
ChecksumType::None => Self::None,
ChecksumType::Sha1 => Self::Sha1,
ChecksumType::Sha256 => Self::Sha256,
ChecksumType::Sha512 => Self::Sha512,
ChecksumType::Md5 => Self::Md5,
}
}
}
impl ChecksumType {
pub fn digest_data(&self, data: &[u8]) -> XarResult<Vec<u8>> {
let mut h: Box<dyn DynDigest> = match self {
Self::None => return Err(Error::Unsupported("cannot digest None checksum")),
Self::Md5 => Box::<md5::Md5>::default(),
Self::Sha1 => Box::<sha1::Sha1>::default(),
Self::Sha256 => Box::<sha2::Sha256>::default(),
Self::Sha512 => Box::<sha2::Sha512>::default(),
};
h.update(data);
Ok(h.finalize().to_vec())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct File {
pub id: u64,
pub ctime: Option<String>,
pub mtime: Option<String>,
pub atime: Option<String>,
#[serde(rename = "name")]
pub names: Vec<String>,
#[serde(rename = "type")]
pub file_type: FileType,
pub mode: Option<String>,
pub deviceno: Option<u32>,
pub inode: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub user: Option<String>,
pub group: Option<String>,
pub size: Option<u64>,
pub data: Option<FileData>,
pub ea: Option<Ea>,
#[serde(rename = "FinderCreateTime")]
pub finder_create_time: Option<FinderCreateTime>,
#[serde(default, rename = "file")]
pub files: Vec<File>,
}
impl File {
pub fn files(&self, directory: Option<&str>) -> XarResult<Vec<(String, File)>> {
let name = self
.names
.iter()
.last()
.ok_or(Error::TableOfContentsCorrupted("missing file name"))?;
let full_path = if let Some(d) = directory {
format!("{d}/{name}")
} else {
name.clone()
};
let mut files = vec![(full_path.clone(), self.clone())];
for f in &self.files {
files.extend(f.files(Some(&full_path))?);
}
Ok(files)
}
pub fn visit_files_mut(&mut self, cb: &dyn Fn(&mut File)) {
for f in self.files.iter_mut() {
cb(f);
f.visit_files_mut(cb)
}
}
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>) -> XarResult<()> {
writer.write(XmlEvent::start_element("file").attr("id", &format!("{}", self.id)))?;
if let Some(data) = &self.data {
data.write_xml(writer)?;
}
if let Some(fct) = &self.finder_create_time {
writer.write(XmlEvent::start_element("FinderCreateTime"))?;
writer.write(XmlEvent::start_element("nanoseconds"))?;
writer.write(XmlEvent::characters(&format!("{}", fct.nanoseconds)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("time"))?;
writer.write(XmlEvent::characters(&fct.time))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element())?;
}
if let Some(time) = &self.ctime {
writer.write(XmlEvent::start_element("ctime"))?;
writer.write(XmlEvent::characters(time))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(time) = &self.mtime {
writer.write(XmlEvent::start_element("mtime"))?;
writer.write(XmlEvent::characters(time))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(time) = &self.atime {
writer.write(XmlEvent::start_element("atime"))?;
writer.write(XmlEvent::characters(time))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.group {
writer.write(XmlEvent::start_element("group"))?;
writer.write(XmlEvent::characters(v))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.gid {
writer.write(XmlEvent::start_element("gid"))?;
writer.write(XmlEvent::characters(&format!("{v}")))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.user {
writer.write(XmlEvent::start_element("user"))?;
writer.write(XmlEvent::characters(v))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.uid {
writer.write(XmlEvent::start_element("uid"))?;
writer.write(XmlEvent::characters(&format!("{v}")))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.mode {
writer.write(XmlEvent::start_element("mode"))?;
writer.write(XmlEvent::characters(v))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.deviceno {
writer.write(XmlEvent::start_element("deviceno"))?;
writer.write(XmlEvent::characters(&format!("{v}")))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(v) = &self.inode {
writer.write(XmlEvent::start_element("inode"))?;
writer.write(XmlEvent::characters(&format!("{v}")))?;
writer.write(XmlEvent::end_element())?;
}
if let Some(ea) = &self.ea {
ea.write_xml(writer)?;
}
writer.write(XmlEvent::start_element("type"))?;
writer.write(XmlEvent::characters(&self.file_type.to_string()))?;
writer.write(XmlEvent::end_element())?;
for name in &self.names {
writer.write(XmlEvent::start_element("name"))?;
writer.write(XmlEvent::characters(name))?;
writer.write(XmlEvent::end_element())?;
}
for file in &self.files {
file.write_xml(writer)?;
}
writer.write(XmlEvent::end_element().name("file"))?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileType {
File,
Directory,
HardLink,
Link,
}
impl Display for FileType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
FileType::File => f.write_str("file"),
FileType::Directory => f.write_str("directory"),
FileType::HardLink => f.write_str("hardlink"),
FileType::Link => f.write_str("symlink"),
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct FileData {
pub offset: u64,
pub size: u64,
pub length: u64,
pub extracted_checksum: FileChecksum,
pub archived_checksum: FileChecksum,
pub encoding: FileEncoding,
}
impl FileData {
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>) -> XarResult<()> {
writer.write(XmlEvent::start_element("data"))?;
writer.write(XmlEvent::start_element("length"))?;
writer.write(XmlEvent::characters(&format!("{}", self.length)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("offset"))?;
writer.write(XmlEvent::characters(&format!("{}", self.offset)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("size"))?;
writer.write(XmlEvent::characters(&format!("{}", self.size)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("encoding").attr("style", &self.encoding.style))?;
writer.write(XmlEvent::end_element())?;
self.extracted_checksum
.write_xml(writer, "extracted-checksum")?;
self.archived_checksum
.write_xml(writer, "archived-checksum")?;
writer.write(XmlEvent::end_element().name("data"))?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileChecksum {
pub style: ChecksumType,
#[serde(rename = "$value")]
pub checksum: String,
}
impl FileChecksum {
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>, name: &str) -> XarResult<()> {
writer.write(XmlEvent::start_element(name).attr("style", &self.style.to_string()))?;
writer.write(XmlEvent::characters(&self.checksum))?;
writer.write(XmlEvent::end_element())?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileEncoding {
pub style: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Ea {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<u64>,
pub name: String,
pub offset: u64,
pub size: u64,
pub length: u64,
pub extracted_checksum: FileChecksum,
pub archived_checksum: FileChecksum,
pub encoding: FileEncoding,
}
impl Ea {
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>) -> XarResult<()> {
let mut ea = XmlEvent::start_element("ea");
let id = self.id.map(|x| format!("{}", x));
if let Some(id) = &id {
ea = ea.attr("id", id.as_str());
}
writer.write(ea)?;
writer.write(XmlEvent::start_element("name"))?;
writer.write(XmlEvent::characters(&self.name))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("offset"))?;
writer.write(XmlEvent::characters(&format!("{}", self.offset)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("size"))?;
writer.write(XmlEvent::characters(&format!("{}", self.size)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("length"))?;
writer.write(XmlEvent::characters(&format!("{}", self.length)))?;
writer.write(XmlEvent::end_element())?;
self.extracted_checksum
.write_xml(writer, "extracted-checksum")?;
self.archived_checksum
.write_xml(writer, "archived-checksum")?;
writer.write(XmlEvent::start_element("encoding").attr("style", &self.encoding.style))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::end_element())?;
Ok(())
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FinderCreateTime {
pub nanoseconds: u64,
pub time: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Signature {
pub style: SignatureStyle,
pub offset: u64,
pub size: u64,
#[serde(rename = "KeyInfo")]
pub key_info: KeyInfo,
}
impl Signature {
pub fn x509_certificates(&self) -> XarResult<Vec<CapturedX509Certificate>> {
self.key_info.x509_certificates()
}
pub fn write_xml<W: Write>(&self, writer: &mut EventWriter<W>, name: &str) -> XarResult<()> {
writer.write(XmlEvent::start_element(name).attr("style", &self.style.to_string()))?;
writer.write(XmlEvent::start_element("offset"))?;
writer.write(XmlEvent::characters(&format!("{}", &self.offset)))?;
writer.write(XmlEvent::end_element())?;
writer.write(XmlEvent::start_element("size"))?;
writer.write(XmlEvent::characters(&format!("{}", &self.size)))?;
writer.write(XmlEvent::end_element())?;
writer.write(
XmlEvent::start_element("KeyInfo").ns("", "http://www.w3.org/2000/09/xmldsig#"),
)?;
writer.write(XmlEvent::start_element("X509Data"))?;
for cert in &self.key_info.x509_data.x509_certificate {
writer.write(XmlEvent::start_element("X509Certificate"))?;
writer.write(XmlEvent::characters(cert))?;
writer.write(XmlEvent::end_element())?;
}
writer.write(XmlEvent::end_element().name("X509Data"))?;
writer.write(XmlEvent::end_element().name("KeyInfo"))?;
writer.write(XmlEvent::end_element().name(name))?;
Ok(())
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum SignatureStyle {
Cms,
Rsa,
}
impl Display for SignatureStyle {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Cms => f.write_str("CMS"),
Self::Rsa => f.write_str("RSA"),
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KeyInfo {
#[serde(rename = "X509Data")]
pub x509_data: X509Data,
}
impl KeyInfo {
pub fn from_certificates<'a>(
certs: impl Iterator<Item = &'a CapturedX509Certificate>,
) -> XarResult<Self> {
Ok(Self {
x509_data: X509Data {
x509_certificate: certs
.map(|cert| {
let der = cert.encode_der()?;
let s = STANDARD_ENGINE.encode(der);
let mut lines = vec![];
let mut remaining = s.as_str();
loop {
if remaining.len() > 72 {
let res = remaining.split_at(72);
lines.push(res.0);
remaining = res.1;
} else {
lines.push(remaining);
break;
}
}
Ok(lines.join("\n"))
})
.collect::<XarResult<Vec<_>>>()?,
},
})
}
pub fn x509_certificates(&self) -> XarResult<Vec<CapturedX509Certificate>> {
self.x509_data.x509_certificates()
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct X509Data {
#[serde(rename = "X509Certificate")]
pub x509_certificate: Vec<String>,
}
impl X509Data {
pub fn x509_certificates(&self) -> XarResult<Vec<CapturedX509Certificate>> {
Ok(self
.x509_certificate
.iter()
.map(|data| {
let data = format!(
"-----BEGIN CERTIFICATE-----\r\n{data}\r\n-----END CERTIFICATE-----\r\n"
);
CapturedX509Certificate::from_pem(data)
})
.collect::<Result<Vec<_>, X509CertificateError>>()?)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn file_checksum_serialize() -> XarResult<()> {
let xml = "<checksum style=\"sha1\">checksum</checksum>";
let _: FileChecksum = serde_xml_rs::from_reader(std::io::BufReader::new(xml.as_bytes()))?;
let xml = "<checksum style=\"SHA1\">checksum</checksum>";
let _: FileChecksum = serde_xml_rs::from_reader(std::io::BufReader::new(xml.as_bytes()))?;
Ok(())
}
}