use std::collections::HashMap;
use std::io::{Read, Seek, Write};
use zip::read::ZipArchive;
use zip::write::ZipWriter;
use zip::CompressionMethod;
use crate::encoding::decode_gedcom_bytes;
use crate::types::GedcomData;
use crate::writer::GedcomWriter;
use crate::GedcomError;
pub const GEDCOM_FILENAME: &str = "gedcom.ged";
#[derive(Debug)]
pub enum GedzipError {
ZipError(zip::result::ZipError),
MissingGedcomFile,
GedcomError(GedcomError),
IoError(std::io::Error),
MissingMediaFile(String),
}
impl std::fmt::Display for GedzipError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZipError(e) => write!(f, "ZIP error: {e}"),
Self::MissingGedcomFile => write!(f, "GEDZIP archive missing required gedcom.ged file"),
Self::GedcomError(e) => write!(f, "GEDCOM error: {e}"),
Self::IoError(e) => write!(f, "I/O error: {e}"),
Self::MissingMediaFile(name) => {
write!(f, "Media file not found in archive: {name}")
}
}
}
}
impl std::error::Error for GedzipError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ZipError(e) => Some(e),
Self::GedcomError(e) => Some(e),
Self::IoError(e) => Some(e),
Self::MissingGedcomFile | Self::MissingMediaFile(_) => None,
}
}
}
impl From<zip::result::ZipError> for GedzipError {
fn from(err: zip::result::ZipError) -> Self {
Self::ZipError(err)
}
}
impl From<GedcomError> for GedzipError {
fn from(err: GedcomError) -> Self {
Self::GedcomError(err)
}
}
impl From<std::io::Error> for GedzipError {
fn from(err: std::io::Error) -> Self {
Self::IoError(err)
}
}
pub struct GedzipReader<R: Read + Seek> {
archive: ZipArchive<R>,
file_names: Vec<String>,
}
impl<R: Read + Seek> GedzipReader<R> {
pub fn new(reader: R) -> Result<Self, GedzipError> {
let archive = ZipArchive::new(reader)?;
let has_gedcom = archive.file_names().any(|name| name == GEDCOM_FILENAME);
if !has_gedcom {
return Err(GedzipError::MissingGedcomFile);
}
let file_names: Vec<String> = archive.file_names().map(String::from).collect();
Ok(Self {
archive,
file_names,
})
}
pub fn parse_gedcom(&mut self) -> Result<GedcomData, GedzipError> {
let bytes = self.read_gedcom_bytes()?;
let (content, _encoding) = decode_gedcom_bytes(&bytes)?;
let data = crate::GedcomBuilder::new().build(content.chars())?;
Ok(data)
}
pub fn read_gedcom_bytes(&mut self) -> Result<Vec<u8>, GedzipError> {
let mut file = self.archive.by_name(GEDCOM_FILENAME)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
#[must_use]
pub fn file_names(&self) -> &[String] {
&self.file_names
}
#[must_use]
pub fn media_files(&self) -> Vec<&str> {
self.file_names
.iter()
.filter(|name| *name != GEDCOM_FILENAME)
.map(String::as_str)
.collect()
}
pub fn read_media_file(&mut self, name: &str) -> Result<Vec<u8>, GedzipError> {
let mut file = self
.archive
.by_name(name)
.map_err(|_| GedzipError::MissingMediaFile(name.to_string()))?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
#[must_use]
pub fn contains_file(&self, name: &str) -> bool {
self.file_names.iter().any(|n| n == name)
}
#[must_use]
pub fn len(&self) -> usize {
self.archive.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.archive.len() <= 1
}
}
pub struct GedzipWriter<W: Write + Seek> {
zip: ZipWriter<W>,
has_gedcom: bool,
}
impl<W: Write + Seek> GedzipWriter<W> {
pub fn new(writer: W) -> Result<Self, GedzipError> {
let zip = ZipWriter::new(writer);
Ok(Self {
zip,
has_gedcom: false,
})
}
pub fn write_gedcom(&mut self, data: &GedcomData) -> Result<(), GedzipError> {
let writer = GedcomWriter::new();
let content = writer
.write_to_string(data)
.map_err(|e| GedzipError::GedcomError(GedcomError::InvalidFormat(e.to_string())))?;
self.write_gedcom_bytes(content.as_bytes())?;
Ok(())
}
pub fn write_gedcom_bytes(&mut self, bytes: &[u8]) -> Result<(), GedzipError> {
let options = zip::write::FileOptions::<()>::default()
.compression_method(CompressionMethod::Deflated);
self.zip.start_file(GEDCOM_FILENAME, options)?;
self.zip.write_all(bytes)?;
self.has_gedcom = true;
Ok(())
}
pub fn add_media_file(&mut self, name: &str, bytes: &[u8]) -> Result<(), GedzipError> {
let options = zip::write::FileOptions::<()>::default()
.compression_method(CompressionMethod::Deflated);
self.zip.start_file(name, options)?;
self.zip.write_all(bytes)?;
Ok(())
}
pub fn add_media_files<S: std::hash::BuildHasher>(
&mut self,
files: &HashMap<String, Vec<u8>, S>,
) -> Result<(), GedzipError> {
for (name, bytes) in files {
self.add_media_file(name, bytes)?;
}
Ok(())
}
pub fn finish(self) -> Result<W, GedzipError> {
Ok(self.zip.finish()?)
}
#[must_use]
pub fn has_gedcom(&self) -> bool {
self.has_gedcom
}
}
pub fn read_gedzip(bytes: &[u8]) -> Result<GedcomData, GedzipError> {
let cursor = std::io::Cursor::new(bytes);
let mut reader = GedzipReader::new(cursor)?;
reader.parse_gedcom()
}
pub fn write_gedzip(data: &GedcomData) -> Result<Vec<u8>, GedzipError> {
let cursor = std::io::Cursor::new(Vec::new());
let mut writer = GedzipWriter::new(cursor)?;
writer.write_gedcom(data)?;
let cursor = writer.finish()?;
Ok(cursor.into_inner())
}
pub fn write_gedzip_with_media<S: std::hash::BuildHasher>(
data: &GedcomData,
media_files: &HashMap<String, Vec<u8>, S>,
) -> Result<Vec<u8>, GedzipError> {
let cursor = std::io::Cursor::new(Vec::new());
let mut writer = GedzipWriter::new(cursor)?;
writer.write_gedcom(data)?;
writer.add_media_files(media_files)?;
let cursor = writer.finish()?;
Ok(cursor.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
fn create_minimal_gedcom() -> GedcomData {
let source = "0 HEAD\n1 GEDC\n2 VERS 7.0\n0 TRLR";
crate::GedcomBuilder::new()
.build_from_str(source)
.expect("Failed to parse minimal GEDCOM")
}
#[test]
fn test_write_and_read_minimal_gedzip() {
let data = create_minimal_gedcom();
let bytes = write_gedzip(&data).expect("Failed to write GEDZIP");
let parsed = read_gedzip(&bytes).expect("Failed to read GEDZIP");
assert!(parsed.is_gedcom_7());
}
#[test]
fn test_gedzip_with_media_files() {
let data = create_minimal_gedcom();
let mut media = HashMap::new();
media.insert("test.txt".to_string(), b"Hello, World!".to_vec());
media.insert("photos/image.jpg".to_string(), vec![0xFF, 0xD8, 0xFF, 0xE0]);
let bytes = write_gedzip_with_media(&data, &media).expect("Failed to write GEDZIP");
let cursor = std::io::Cursor::new(bytes);
let mut reader = GedzipReader::new(cursor).expect("Failed to create reader");
assert!(reader.contains_file(GEDCOM_FILENAME));
assert!(reader.contains_file("test.txt"));
assert!(reader.contains_file("photos/image.jpg"));
let media_files = reader.media_files();
assert_eq!(media_files.len(), 2);
let txt_content = reader
.read_media_file("test.txt")
.expect("Failed to read test.txt");
assert_eq!(txt_content, b"Hello, World!");
}
#[test]
fn test_missing_gedcom_file() {
let cursor = std::io::Cursor::new(Vec::new());
let mut zip = ZipWriter::new(cursor);
let options = zip::write::FileOptions::<()>::default();
zip.start_file("other.txt", options).unwrap();
zip.write_all(b"test").unwrap();
let cursor = zip.finish().unwrap();
let result = GedzipReader::new(std::io::Cursor::new(cursor.into_inner()));
assert!(matches!(result, Err(GedzipError::MissingGedcomFile)));
}
#[test]
fn test_reader_len_and_is_empty() {
let data = create_minimal_gedcom();
let bytes = write_gedzip(&data).expect("Failed to write");
let cursor = std::io::Cursor::new(bytes);
let reader = GedzipReader::new(cursor).expect("Failed to create reader");
assert_eq!(reader.len(), 1);
assert!(reader.is_empty());
let mut media = HashMap::new();
media.insert("test.txt".to_string(), b"test".to_vec());
let bytes = write_gedzip_with_media(&data, &media).expect("Failed to write");
let cursor = std::io::Cursor::new(bytes);
let reader = GedzipReader::new(cursor).expect("Failed to create reader");
assert_eq!(reader.len(), 2);
assert!(!reader.is_empty());
}
#[test]
fn test_gedzip_roundtrip_with_individuals() {
let source = r"0 HEAD
1 GEDC
2 VERS 7.0
0 @I1@ INDI
1 NAME John /Doe/
1 SEX M
0 @I2@ INDI
1 NAME Jane /Doe/
1 SEX F
0 @F1@ FAM
1 HUSB @I1@
1 WIFE @I2@
0 TRLR";
let data = crate::GedcomBuilder::new()
.build_from_str(source)
.expect("Failed to parse");
let bytes = write_gedzip(&data).expect("Failed to write");
let parsed = read_gedzip(&bytes).expect("Failed to read");
assert_eq!(parsed.individuals.len(), 2);
assert_eq!(parsed.families.len(), 1);
}
#[test]
fn test_read_missing_media_file() {
let data = create_minimal_gedcom();
let bytes = write_gedzip(&data).expect("Failed to write");
let cursor = std::io::Cursor::new(bytes);
let mut reader = GedzipReader::new(cursor).expect("Failed to create reader");
let result = reader.read_media_file("nonexistent.jpg");
assert!(matches!(result, Err(GedzipError::MissingMediaFile(_))));
}
}