rs-zip2sqlite 0.1.0

Converts the zip file to an sqlite archive.
Documentation
use std::io;
use std::path::Path;

use io::Read;

use rusqlite::Connection;
use rusqlite::Transaction;
use rusqlite::params;

use rusqlite::backup::Backup;

use rawzip::ZipArchiveEntryWayfinder;
use rawzip::ZipFileHeaderRecord;
use rawzip::ZipSliceArchive;

use rawzip::time::Utc;
use rawzip::time::ZipDateTime;
use rawzip::time::ZipDateTimeKind;

#[derive(Debug)]
pub enum ZipToSqliteErr {
    UnableToInsertEntry(String),
    InvalidEntryPath,
    InvalidEntryIndex,
    UnableToGetEntry,
    UnableToStartTransaction(String),
    UnableToCommit(String),
    InvalidZipArchive(String),
    UnableToPersist(String),
    UnableToOpenDb(String),
    UnableToOpenInMemDb(String),
    UnableToReadZip(String),
    UnableToCreateTable(String),
}

pub struct ZipEntry<'a> {
    pub name: &'a str,

    /// Access permissions
    pub mode: u32,

    /// Unixtime in seconds
    pub mtime: i64,

    /// The original file size
    pub sz: i64,

    pub data: &'a [u8],
}

impl<'a> ZipEntry<'a> {
    pub fn to_sqlite(&self, con: &Transaction) -> Result<(), ZipToSqliteErr> {
        con.execute(
            r#"
                INSERT INTO sqlar
                VALUES (
                    ?1, -- name
                    ?2, -- mode
                    ?3, -- mtime
                    ?4, -- sz
                    ?5  -- data
                )
            "#,
            params![self.name, self.mode, self.mtime, self.sz, self.data],
        )
        .map_err(|e| ZipToSqliteErr::UnableToInsertEntry(format!("{e}")))
        .map(|_| ())
    }
}

pub struct ZipArchive<'a> {
    inner: ZipSliceArchive<&'a [u8]>,
}

impl<'a> ZipArchive<'a> {
    pub fn data(&self, index: ZipArchiveEntryWayfinder) -> Result<&[u8], ZipToSqliteErr> {
        let entry = self
            .inner
            .get_entry(index)
            .map_err(|_| ZipToSqliteErr::InvalidEntryIndex)?;
        Ok(entry.data())
    }
}

impl<'a> ZipArchive<'a> {
    pub fn to_sqlite(&self, con: &mut Connection) -> Result<(), ZipToSqliteErr> {
        let tx = con
            .transaction()
            .map_err(|e| ZipToSqliteErr::UnableToStartTransaction(format!("{e}")))?;

        tx.execute(
            r#"
                CREATE TABLE IF NOT EXISTS sqlar (
                    name TEXT PRIMARY KEY,
                    mode INT,
                    mtime INT,
                    sz INT,
                    data BLOB
                )
            "#,
            [],
        )
        .map_err(|e| ZipToSqliteErr::UnableToCreateTable(format!("{e}")))?;

        let entries = self.inner.entries();
        for rhdr in entries {
            let hdr = rhdr.map_err(|_| ZipToSqliteErr::UnableToGetEntry)?;
            let index: ZipArchiveEntryWayfinder = hdr.wayfinder();
            let zh = ZipItemHeader { inner: hdr };

            let name: &str = zh.name()?;
            let mode: u32 = zh.mode();
            let mtime: i64 = zh
                .unixtime()
                .or_else(ZipItemHeader::zip_epoch)
                .unwrap_or_default();
            let data: &[u8] = self.data(index)?;
            let sz: i64 = data.len() as i64;

            let ent = ZipEntry {
                name,
                mode,
                mtime,
                sz,
                data,
            };

            ent.to_sqlite(&tx)?;
        }

        tx.commit()
            .map_err(|e| ZipToSqliteErr::UnableToCommit(format!("{e}")))?;
        Ok(())
    }
}

pub struct ZipItemHeader<'a> {
    inner: ZipFileHeaderRecord<'a>,
}

impl<'a> ZipItemHeader<'a> {
    pub fn name(&self) -> Result<&str, ZipToSqliteErr> {
        let fpath = self.inner.file_path();
        let raw: &[u8] = fpath.as_bytes();
        std::str::from_utf8(raw).map_err(|_| ZipToSqliteErr::InvalidEntryPath)
    }

    pub fn mode(&self) -> u32 {
        self.inner.mode().value()
    }

    pub fn unixtime(&self) -> Option<i64> {
        let modified = self.inner.last_modified();
        match modified {
            ZipDateTimeKind::Utc(u) => Some(u.to_unix()),
            ZipDateTimeKind::Local(_) => None,
        }
    }

    pub fn zip_epoch() -> Option<i64> {
        let zdt: Option<_> = ZipDateTime::<Utc>::from_components(1980, 1, 1, 0, 0, 0, 0);
        zdt.map(|z| z.to_unix())
    }
}

pub fn bytes2zip2sqlite(zbytes: &[u8], con: &mut Connection) -> Result<(), ZipToSqliteErr> {
    let zs: ZipSliceArchive<_> = rawzip::ZipArchive::<()>::from_slice(zbytes)
        .map_err(|e| ZipToSqliteErr::InvalidZipArchive(format!("{e}")))?;
    let z = ZipArchive { inner: zs };
    z.to_sqlite(con)
}

pub fn mem2f(mem: &Connection, f: &mut Connection) -> Result<(), ZipToSqliteErr> {
    let bkup = Backup::new(mem, f).map_err(|e| ZipToSqliteErr::UnableToPersist(format!("{e}")))?;
    bkup.run_to_completion(4, core::time::Duration::from_millis(500), None)
        .map_err(|e| ZipToSqliteErr::UnableToPersist(format!("{e}")))?;
    Ok(())
}

pub fn mem2file<P>(mem: &Connection, filename: P) -> Result<(), ZipToSqliteErr>
where
    P: AsRef<Path>,
{
    let mut con: Connection =
        Connection::open(&filename).map_err(|e| ZipToSqliteErr::UnableToOpenDb(format!("{e}")))?;
    mem2f(mem, &mut con)
}

pub fn bytes2zip2sqlite2mem(zbytes: &[u8]) -> Result<Connection, ZipToSqliteErr> {
    let mut con: Connection = Connection::open_in_memory()
        .map_err(|e| ZipToSqliteErr::UnableToOpenInMemDb(format!("{e}")))?;
    bytes2zip2sqlite(zbytes, &mut con)?;
    Ok(con)
}

pub fn bytes2zip2sqlite2file<P>(bytes: &[u8], filename: P) -> Result<(), ZipToSqliteErr>
where
    P: AsRef<Path>,
{
    let saved: Connection = bytes2zip2sqlite2mem(bytes)?;
    mem2file(&saved, filename)
}

pub fn reader2bytes<R>(rdr: R, limit: u64) -> Result<Vec<u8>, ZipToSqliteErr>
where
    R: Read,
{
    let mut limited = rdr.take(limit);
    let mut buf: Vec<u8> = vec![];
    limited
        .read_to_end(&mut buf)
        .map_err(|e| ZipToSqliteErr::UnableToReadZip(format!("{e}")))?;
    Ok(buf)
}

pub fn stdin2bytes(limit: u64) -> Result<Vec<u8>, ZipToSqliteErr> {
    reader2bytes(io::stdin().lock(), limit)
}

pub fn stdin2bytes2zip2sqlite2file<P>(limit: u64, filename: P) -> Result<(), ZipToSqliteErr>
where
    P: AsRef<Path>,
{
    let zbytes: Vec<u8> = stdin2bytes(limit)?;
    bytes2zip2sqlite2file(&zbytes, filename)
}