#![forbid(unsafe_code)]
#![no_std]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
mod crc_reader;
use alloc::borrow::Cow;
use alloc::ffi::CString;
use core::default::Default;
use core::fmt;
#[cfg(feature = "std")]
use std::io::Read;
#[cfg(feature = "std")]
use std::{env, io, time};
pub use crc_reader::Crc;
#[cfg(feature = "std")]
pub use crc_reader::CrcReader;
static FHCRC: u8 = 1 << 1;
static FEXTRA: u8 = 1 << 2;
static FNAME: u8 = 1 << 3;
static FCOMMENT: u8 = 1 << 4;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum FileSystemType {
Fat = 0,
Amiga = 1,
Vms = 2,
Unix = 3,
Vcms = 4,
AtariTos = 5,
Hpfs = 6,
Macintosh = 7,
Zsystem = 8,
Cpm = 9,
Tops20OrNTFS = 10,
NTFS = 11,
SmsQdos = 12,
Riscos = 13,
Vfat = 14,
Mvs = 15,
Beos = 16,
TandemNsk = 17,
Theos = 18,
Apple = 19,
Unknown = 255,
}
impl FileSystemType {
pub const fn as_u8(&self) -> u8 {
*self as u8
}
pub fn from_u8(value: u8) -> FileSystemType {
use FileSystemType::*;
match value {
0 => Fat,
1 => Amiga,
2 => Vms,
3 => Unix,
4 => Vcms,
5 => AtariTos,
6 => Hpfs,
7 => Macintosh,
8 => Zsystem,
9 => Cpm,
10 => Tops20OrNTFS,
11 => NTFS,
12 => SmsQdos,
13 => Riscos,
14 => Vfat,
15 => Mvs,
16 => Beos,
17 => TandemNsk,
18 => Theos,
19 => Apple,
_ => Unknown,
}
}
}
impl fmt::Display for FileSystemType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use FileSystemType::*;
match *self {
Fat => "FAT filesystem (MS-DOS, OS/2, NT/Win32)",
Amiga => "Amiga",
Vms => "VMS or OpenVMS",
Unix => "Unix type system/Linux",
Vcms => "VM/CMS",
AtariTos => "Atari TOS",
Hpfs => "HPFS filesystem (OS/2, NT)",
Macintosh => "Macintosh operating system (Classic Mac OS, OS/X, macOS, iOS etc.)",
Zsystem => "Z-System",
Cpm => "CP/M",
Tops20OrNTFS => "NTFS (New zlib versions) or TOPS-20",
NTFS => "NTFS",
SmsQdos => "SMS/QDOS",
Riscos => "Acorn RISC OS",
Vfat => "VFAT file system (Win95, NT)",
Mvs => "MVS or PRIMOS",
Beos => "BeOS",
TandemNsk => "Tandem/NSK",
Theos => "THEOS",
Apple => "macOS, OS/X, iOS or watchOS",
_ => "Unknown or unset",
}
.fmt(f)
}
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ExtraFlags {
#[default]
Default = 0,
MaximumCompression = 2,
FastestCompression = 4,
}
impl ExtraFlags {
pub fn from_u8(value: u8) -> ExtraFlags {
use ExtraFlags::*;
match value {
2 => MaximumCompression,
4 => FastestCompression,
_ => Default,
}
}
pub const fn as_u8(&self) -> u8 {
*self as u8
}
}
impl fmt::Display for ExtraFlags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ExtraFlags::Default => "No extra flags (Default) or unknown.",
ExtraFlags::MaximumCompression => "Maximum compression algorithm (DEFLATE).",
ExtraFlags::FastestCompression => "Fastest compression algorithm (DEFLATE)",
}
.fmt(f)
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct GzBuilder {
extra: Option<alloc::vec::Vec<u8>>,
filename: Option<CString>,
comment: Option<CString>,
mtime: u32,
os: Option<FileSystemType>,
xfl: ExtraFlags,
}
impl GzBuilder {
pub fn new() -> GzBuilder {
GzBuilder {
extra: None,
filename: None,
comment: None,
mtime: 0,
os: None,
xfl: ExtraFlags::Default,
}
}
pub fn mtime(mut self, mtime: u32) -> GzBuilder {
self.mtime = mtime;
self
}
pub fn extra<T: Into<alloc::vec::Vec<u8>>>(mut self, extra: T) -> GzBuilder {
self.extra = Some(extra.into());
self
}
pub fn filename<T: Into<alloc::vec::Vec<u8>>>(mut self, filename: T) -> GzBuilder {
self.filename = Some(CString::new(filename).unwrap());
self
}
pub fn comment<T: Into<alloc::vec::Vec<u8>>>(mut self, comment: T) -> GzBuilder {
self.comment = Some(CString::new(comment).unwrap());
self
}
pub fn os(mut self, os: FileSystemType) -> GzBuilder {
self.os = Some(os);
self
}
pub fn xfl(mut self, xfl: ExtraFlags) -> GzBuilder {
self.xfl = xfl;
self
}
pub fn into_header_xfl(mut self, lvl: ExtraFlags) -> alloc::vec::Vec<u8> {
self.xfl = lvl;
self.into_header()
}
pub fn into_header(self) -> alloc::vec::Vec<u8> {
self.into_header_inner(false)
}
pub fn into_header_with_checksum(self) -> alloc::vec::Vec<u8> {
self.into_header_inner(true)
}
fn into_header_inner(self, use_crc: bool) -> alloc::vec::Vec<u8> {
let GzBuilder {
extra,
filename,
comment,
mtime,
os,
xfl,
} = self;
let os = match os {
Some(f) => f,
None => {
#[cfg(feature = "std")]
match env::consts::OS {
"linux" | "freebsd" | "dragonfly" | "netbsd" | "openbsd" | "solaris"
| "bitrig" => FileSystemType::Unix,
"macos" => FileSystemType::Apple,
"win32" => FileSystemType::Tops20OrNTFS,
_ => FileSystemType::Unknown,
}
#[cfg(not(feature = "std"))]
FileSystemType::Unknown
}
};
let mut flg = 0;
if use_crc {
flg |= FHCRC;
};
let mut header = alloc::vec![0u8; 10];
if let Some(v) = extra {
flg |= FEXTRA;
header.push((v.len()) as u8);
header.push((v.len() >> 8) as u8);
header.extend(v);
}
if let Some(filename) = filename {
flg |= FNAME;
header.extend(filename.as_bytes_with_nul().iter().cloned());
}
if let Some(comment) = comment {
flg |= FCOMMENT;
header.extend(comment.as_bytes_with_nul().iter().cloned());
}
header[0] = 0x1f;
header[1] = 0x8b;
header[2] = 8;
header[3] = flg;
header[4] = mtime as u8;
header[5] = (mtime >> 8) as u8;
header[6] = (mtime >> 16) as u8;
header[7] = (mtime >> 24) as u8;
header[8] = xfl.as_u8();
header[9] = os.as_u8();
if use_crc {
let mut crc = Crc::new();
crc.update(&header);
let checksum = crc.sum() as u16;
header.extend(&[checksum as u8, (checksum >> 8) as u8]);
}
header
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GzHeader {
extra: Option<alloc::vec::Vec<u8>>,
filename: Option<alloc::vec::Vec<u8>>,
comment: Option<alloc::vec::Vec<u8>>,
mtime: u32,
os: u8,
xfl: u8,
}
impl GzHeader {
pub fn filename(&self) -> Option<&[u8]> {
self.filename.as_ref().map(|s| &s[..])
}
pub fn extra(&self) -> Option<&[u8]> {
self.extra.as_ref().map(|s| &s[..])
}
pub fn comment(&self) -> Option<&[u8]> {
self.comment.as_ref().map(|s| &s[..])
}
pub const fn mtime(&self) -> u32 {
self.mtime
}
#[cfg(feature = "std")]
pub fn mtime_as_datetime(&self) -> Option<time::SystemTime> {
if self.mtime == 0 {
None
} else {
let duration = time::Duration::new(u64::from(self.mtime), 0);
let datetime = time::UNIX_EPOCH + duration;
Some(datetime)
}
}
pub const fn os(&self) -> u8 {
self.os
}
pub const fn xfl(&self) -> u8 {
self.xfl
}
}
#[inline]
fn into_string<'a>(data: Option<&'a [u8]>) -> alloc::borrow::Cow<'a, str> {
data.map_or_else(
|| Cow::Borrowed("(Not set)"),
|d| alloc::string::String::from_utf8_lossy(d),
)
}
impl fmt::Display for GzHeader {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Filename: {}\n\
Comment: {}\n\
Extra: {:?}\n\
mtime: {}\n\
os: {}\n\
xfl: {}",
into_string(self.filename()),
into_string(self.comment()),
self.extra,
self.mtime,
FileSystemType::from_u8(self.os),
ExtraFlags::Default, )
}
}
#[cfg(feature = "std")]
fn corrupt() -> io::Error {
io::Error::new(
io::ErrorKind::InvalidInput,
"corrupt gzip stream does not have a matching header checksum",
)
}
#[cfg(feature = "std")]
fn bad_header() -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, "invalid gzip header")
}
#[cfg(feature = "std")]
fn read_le_u16<R: Read>(r: &mut R) -> io::Result<u16> {
let mut b = [0; 2];
r.read_exact(&mut b)?;
Ok((b[0] as u16) | ((b[1] as u16) << 8))
}
#[cfg(feature = "std")]
pub fn read_gz_header<R: Read>(r: &mut R) -> io::Result<GzHeader> {
let mut crc_reader = CrcReader::new(r);
let mut header = [0; 10];
crc_reader.read_exact(&mut header)?;
let id1 = header[0];
let id2 = header[1];
if id1 != 0x1f || id2 != 0x8b {
return Err(bad_header());
}
let cm = header[2];
if cm != 8 {
return Err(bad_header());
}
let flg = header[3];
let mtime = (header[4] as u32)
| ((header[5] as u32) << 8)
| ((header[6] as u32) << 16)
| ((header[7] as u32) << 24);
let xfl = header[8];
let os = header[9];
let extra = if flg & FEXTRA != 0 {
let xlen = read_le_u16(&mut crc_reader)?;
let mut extra = alloc::vec![0; xlen as usize];
crc_reader.read_exact(&mut extra)?;
Some(extra)
} else {
None
};
let filename = if flg & FNAME != 0 {
let mut b = alloc::vec::Vec::new();
for byte in crc_reader.by_ref().bytes() {
let byte = byte?;
if byte == 0 {
break;
}
b.push(byte);
}
Some(b)
} else {
None
};
let comment = if flg & FCOMMENT != 0 {
let mut b = alloc::vec::Vec::new();
for byte in crc_reader.by_ref().bytes() {
let byte = byte?;
if byte == 0 {
break;
}
b.push(byte);
}
Some(b)
} else {
None
};
if flg & FHCRC != 0 {
let calced_crc = crc_reader.crc().sum() as u16;
let stored_crc = read_le_u16(&mut crc_reader)?;
if calced_crc != stored_crc {
return Err(corrupt());
}
}
Ok(GzHeader {
extra,
filename,
comment,
mtime,
os,
xfl,
})
}
#[cfg(test)]
mod tests {
extern crate std;
use super::*;
use std::io::Cursor;
fn roundtrip_inner(use_crc: bool) {
const COMMENT: &[u8] = b"Comment";
const FILENAME: &[u8] = b"Filename";
const MTIME: u32 = 12345;
const OS: FileSystemType = FileSystemType::NTFS;
const XFL: ExtraFlags = ExtraFlags::FastestCompression;
let header = GzBuilder::new()
.comment(COMMENT)
.filename(FILENAME)
.mtime(MTIME)
.os(OS)
.xfl(ExtraFlags::FastestCompression)
.into_header_inner(use_crc);
let mut reader = Cursor::new(header.clone());
let header_read = read_gz_header(&mut reader).unwrap();
assert_eq!(header_read.comment().unwrap(), COMMENT);
assert_eq!(header_read.filename().unwrap(), FILENAME);
assert_eq!(header_read.mtime(), MTIME);
assert_eq!(header_read.os(), OS.as_u8());
assert_eq!(header_read.xfl(), XFL.as_u8());
}
#[test]
fn roundtrip() {
roundtrip_inner(false);
}
#[test]
fn roundtrip_with_crc() {
roundtrip_inner(true);
}
#[test]
fn filesystem_enum() {
for n in 0..20 {
assert_eq!(n, FileSystemType::from_u8(n).as_u8());
}
for n in 20..(u8::MAX as u16) + 1 {
assert_eq!(FileSystemType::from_u8(n as u8), FileSystemType::Unknown);
}
}
}