use crate::io::ReadConfig;
use crate::toc::{read_toc, TocEntry};
use crate::types::{ArchiveError, CompressionMethod, Section, Version};
use chrono::prelude::*;
use flate2::read::GzDecoder;
use flate2::read::ZlibDecoder;
use std::fmt;
use std::fs::File;
use std::io;
use std::string::String;
pub const K_VERS_1_10: Version = (1, 10, 0);
pub const K_VERS_1_11: Version = (1, 11, 0);
#[allow(dead_code)]
pub const K_VERS_1_12: Version = (1, 12, 0);
#[allow(dead_code)]
pub const K_VERS_1_13: Version = (1, 13, 0);
pub const K_VERS_1_14: Version = (1, 14, 0);
pub const K_VERS_1_15: Version = (1, 15, 0);
pub const K_VERS_1_16: Version = (1, 16, 0);
#[derive(Debug, PartialEq)]
pub struct Archive {
pub version: Version,
pub compression_method: CompressionMethod,
pub create_date: NaiveDateTime,
pub database_name: String,
pub server_version: String,
pub pgdump_version: String,
pub toc_entries: Vec<TocEntry>,
io_config: ReadConfig,
}
impl fmt::Display for Archive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"version={}.{}.{} compression={}",
self.version.0, self.version.1, self.version.2, self.compression_method
)
}
}
impl Archive {
pub fn parse(f: &mut (impl io::Read + ?Sized)) -> Result<Archive, ArchiveError> {
let mut buffer = vec![0; 5];
f.read_exact(buffer.as_mut_slice())?;
if buffer != "PGDMP".as_bytes() {
return Err(ArchiveError::InvalidData(
"file does not start with PGDMP".into(),
));
}
let mut io_config = ReadConfig::new();
let version: Version = (
io_config.read_byte(f)?,
io_config.read_byte(f)?,
io_config.read_byte(f)?,
);
if version < K_VERS_1_10 || version > K_VERS_1_16 {
return Err(ArchiveError::UnsupportedVersionError(version));
}
io_config.int_size = io_config.read_byte(f)? as usize;
io_config.offset_size = io_config.read_byte(f)? as usize;
if io_config.read_byte(f)? != 1 {
return Err(ArchiveError::InvalidData(
"file format must be 1 (custom)".into(),
));
}
let compression_method = if version >= K_VERS_1_15 {
io_config
.read_byte(f)?
.try_into()
.or(Err(ArchiveError::InvalidData(
"invalid compression method".into(),
)))?
} else {
let compression = io_config.read_int(f)?;
match compression {
-1 => Ok(CompressionMethod::ZSTD),
0 => Ok(CompressionMethod::None),
1..=9 => Ok(CompressionMethod::Gzip(compression)),
_ => Err(ArchiveError::InvalidData(
"invalid compression method".into(),
)),
}?
};
let created_sec = io_config.read_int(f)?;
let created_min = io_config.read_int(f)?;
let created_hour = io_config.read_int(f)?;
let created_mday = io_config.read_int(f)?;
let created_mon = io_config.read_int(f)?;
let created_year = io_config.read_int(f)?;
let _created_isdst = io_config.read_int(f)?;
let create_date = NaiveDate::from_ymd_opt(
(created_year + 1900) as i32,
(created_mon + 1) as u32,
created_mday as u32,
)
.ok_or(ArchiveError::InvalidData("invalid creation date".into()))?
.and_hms_opt(created_hour as u32, created_min as u32, created_sec as u32)
.ok_or(ArchiveError::InvalidData(
"invalid time in creation date".into(),
))?;
let database_name = io_config.read_string(f)?;
let server_version = io_config.read_string(f)?;
let pgdump_version = io_config.read_string(f)?;
let toc_entries = read_toc(f, &io_config, version)?;
Ok(Archive {
version,
compression_method,
create_date,
database_name,
server_version,
pgdump_version,
toc_entries,
io_config,
})
}
pub fn find_toc_entry(&self, section: Section, desc: &str, tag: &str) -> Option<&TocEntry> {
self.toc_entries
.iter()
.find(|e| e.section == section && e.desc == desc && e.tag == tag)
}
pub fn read_data(
&self,
f: &mut File,
entry: &TocEntry,
) -> Result<Box<dyn io::Read>, ArchiveError> {
let reader = self.io_config.read_data(f, entry.offset)?;
match self.compression_method {
CompressionMethod::None => Ok(reader),
CompressionMethod::ZSTD => Ok(Box::new(ZlibDecoder::new(reader))),
CompressionMethod::Gzip(_) => Ok(Box::new(GzDecoder::new(reader))),
_ => Err(ArchiveError::CompressionMethodNotSupported(
self.compression_method,
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use hex_literal::hex;
#[test]
fn v14_header() -> Result<(), ArchiveError> {
let mut input = &hex!(
"50 47 44 4d 50" "01 0e 00" "04" "08" "01" "01 01 00 00 00" "00 14 00 00 00" "00 35 00 00 00" "00 07 00 00 00" "00 18 00 00 00" "00 0a 00 00 00" "00 7a 00 00 00" "00 00 00 00 00" "00 07 00 00 00 77 69 63 68 65 72 74" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 00 00 00 00" )[..];
let header = Archive::parse(&mut input)?;
assert_eq!(
header,
Archive {
version: (1, 14, 0),
compression_method: CompressionMethod::ZSTD,
create_date: NaiveDate::from_ymd_opt(2022, 11, 24)
.unwrap()
.and_hms_opt(7, 53, 20)
.unwrap(),
database_name: String::from("wichert"),
server_version: String::from("14.6 (Homebrew)"),
pgdump_version: String::from("14.6 (Homebrew)"),
toc_entries: vec![],
io_config: ReadConfig {
int_size: 4,
offset_size: 8
}
}
);
Ok(())
}
#[test]
fn v15_header() -> Result<(), ArchiveError> {
let mut input = &hex!(
"50 47 44 4d 50" "01 0f 00" "04" "08" "01" "02" "00 14 00 00 00" "00 35 00 00 00" "00 07 00 00 00" "00 18 00 00 00" "00 0a 00 00 00" "00 7a 00 00 00" "00 00 00 00 00" "00 07 00 00 00 77 69 63 68 65 72 74" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 00 00 00 00" )[..];
let header = Archive::parse(&mut input)?;
assert_eq!(
header,
Archive {
version: (1, 15, 0),
compression_method: CompressionMethod::LZ4,
create_date: NaiveDate::from_ymd_opt(2022, 11, 24)
.unwrap()
.and_hms_opt(7, 53, 20)
.unwrap(),
database_name: String::from("wichert"),
server_version: String::from("14.6 (Homebrew)"),
pgdump_version: String::from("14.6 (Homebrew)"),
toc_entries: vec![],
io_config: ReadConfig {
int_size: 4,
offset_size: 8
}
}
);
Ok(())
}
#[test]
fn header_create_date_with_zero_indexed_month() -> Result<(), ArchiveError> {
let mut input = &hex!(
"50 47 44 4d 50" "01 0e 00" "04" "08" "01" "01 01 00 00 00" "00 14 00 00 00" "00 35 00 00 00" "00 07 00 00 00" "00 18 00 00 00" "00 00 00 00 00" "00 7a 00 00 00" "00 00 00 00 00" "00 07 00 00 00 77 69 63 68 65 72 74" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 0f 00 00 00 31 34 2e 36 20 28 48 6f 6d 65 62 72 65 77 29" "00 00 00 00 00" )[..];
let header = Archive::parse(&mut input)?;
assert_eq!(
header,
Archive {
version: (1, 14, 0),
compression_method: CompressionMethod::ZSTD,
create_date: NaiveDate::from_ymd_opt(2022, 1, 24)
.unwrap()
.and_hms_opt(7, 53, 20)
.unwrap(),
database_name: String::from("wichert"),
server_version: String::from("14.6 (Homebrew)"),
pgdump_version: String::from("14.6 (Homebrew)"),
toc_entries: vec![],
io_config: ReadConfig {
int_size: 4,
offset_size: 8
}
}
);
Ok(())
}
}