use time::{OffsetDateTime, format_description};
use crate::raw::error::SdmpError;
use crate::raw::header::{CODEC_C2, CODEC_C4, CODEC_SDMP7, CODEC_SDMP8};
use std::collections::HashMap;
use std::path::PathBuf;
pub enum Codec {
Generic7,
Generic8,
Compact4,
Compact2,
}
impl Codec {
const fn to_byte(&self) -> u8 {
match self {
Self::Generic7 => CODEC_SDMP7,
Self::Generic8 => CODEC_SDMP8,
Self::Compact4 => CODEC_C4,
Self::Compact2 => CODEC_C2,
}
}
}
pub struct DumpBuilder {
directory: PathBuf,
codec: Codec,
ignores: Vec<String>,
use_gitignore: bool,
}
impl DumpBuilder {
#[must_use]
pub const fn codec(mut self, codec: Codec) -> Self {
self.codec = codec;
self
}
#[must_use]
pub fn ignore(mut self, pattern: &str) -> Self {
self.ignores.push(pattern.to_string());
self
}
#[must_use]
pub const fn gitignore(mut self) -> Self {
self.use_gitignore = true;
self
}
pub fn build(self) -> Result<Dump, SdmpError> {
use ignore::WalkBuilder;
let mut files = Vec::new();
let mut original_size = 0u32;
let mut walker = WalkBuilder::new(&self.directory);
walker.git_ignore(self.use_gitignore);
for result in walker.build() {
let entry =
result.map_err(|e| SdmpError::IoError(std::io::Error::other(e.to_string())))?;
if !entry.path().is_file() {
continue;
}
let content = std::fs::read(entry.path())?;
if is_binary(&content) {
continue;
}
let relative = entry
.path()
.strip_prefix(&self.directory)
.unwrap()
.to_string_lossy()
.to_string();
if self.ignores.iter().any(|pat| glob_match(pat, &relative)) {
continue;
}
if has_annotation(&content, b"@srcdmp:ignore") {
continue;
}
let _force = has_annotation(&content, b"@srcdmp:force");
let text = String::from_utf8(content).map_err(|_| SdmpError::InvalidUtf8)?;
original_size += text.len() as u32;
let encoded = match self.codec.to_byte() {
crate::raw::header::CODEC_SDMP8 => crate::raw::codec::sdmp8::encode(&text),
crate::raw::header::CODEC_SDMP7 => crate::raw::codec::sdmp7::encode(&text),
_ => crate::raw::codec::sdmpc4::encode(&text),
};
files.push((relative, encoded));
}
Ok(Dump {
files,
codec: self.codec.to_byte(),
original_size,
})
}
}
pub struct Dump {
pub(crate) files: Vec<(String, Vec<u8>)>,
pub(crate) codec: u8,
pub(crate) original_size: u32,
}
impl Dump {
fn decode_file(&self, encoded: &[u8]) -> Result<String, SdmpError> {
match self.codec {
crate::raw::header::CODEC_SDMP7 => crate::raw::codec::sdmp7::decode(encoded),
crate::raw::header::CODEC_SDMP8 => crate::raw::codec::sdmp8::decode(encoded),
crate::raw::header::CODEC_C4 => crate::raw::codec::sdmpc4::decode(encoded),
_ => Err(SdmpError::UnknownCodec(self.codec)),
}
}
#[must_use]
pub fn files(&self) -> impl Iterator<Item = &str> {
self.files.iter().map(|(path, _)| path.as_str())
}
#[must_use]
pub const fn codec_name(&self) -> &'static str {
match self.codec {
crate::raw::header::CODEC_SDMP7 => "SDMP-7",
crate::raw::header::CODEC_SDMP8 => "SDMP-8",
crate::raw::header::CODEC_C4 => "SDMP-C4",
crate::raw::header::CODEC_C2 => "SDMP-C2",
_ => "unknown",
}
}
#[must_use]
pub fn from_dir(path: &str) -> DumpBuilder {
DumpBuilder {
directory: PathBuf::from(path),
codec: Codec::Compact4,
ignores: Vec::new(),
use_gitignore: false,
}
}
pub fn open(path: &str) -> Result<Self, SdmpError> {
use crate::raw::entry::Entry;
use crate::raw::header::Header;
let data = std::fs::read(path)?;
let header = Header::read(&data)?;
let mut files = Vec::new();
let mut i = Header::SIZE;
for _ in 0..header.file_count {
let (entry, consumed) = Entry::read(&data[i..])?;
i += consumed;
let end = i + entry.file_size as usize;
if end > data.len() {
return Err(SdmpError::UnexpectedEof);
}
let encoded = data[i..end].to_vec();
i = end;
files.push((entry.path, encoded));
}
Ok(Self {
files,
codec: header.codec,
original_size: header.original_size,
})
}
#[must_use]
pub const fn file_count(&self) -> usize {
self.files.len()
}
#[must_use]
pub const fn original_size(&self) -> u32 {
self.original_size
}
pub fn write(&self, path: &str) -> Result<(), SdmpError> {
use crate::raw::entry::Entry;
use crate::raw::header::Header;
let mut body = Vec::new();
for (file_path, encoded) in &self.files {
let entry = Entry {
path: file_path.clone(),
file_size: encoded.len() as u32,
};
entry.write(&mut body);
body.extend_from_slice(encoded);
}
let header = Header {
codec: self.codec,
version: 1,
original_size: self.original_size,
file_count: self.files.len() as u32,
};
let mut file = Vec::new();
file.extend_from_slice(&header.write());
file.extend_from_slice(&body);
std::fs::write(path, file)?;
Ok(())
}
#[must_use]
pub fn named(&self, name: &str) -> DumpWriteBuilder<'_> {
DumpWriteBuilder {
dump: self,
name: name.to_string(),
version: None,
}
}
#[must_use]
pub fn from_cargo(&self) -> DumpWriteBuilder<'_> {
let cargo = std::fs::read_to_string("Cargo.toml")
.ok()
.and_then(|s| s.parse::<toml::Table>().ok());
let name = cargo
.as_ref()
.and_then(|t| t.get("package"))
.and_then(|p| p.as_table())
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("unnamed")
.to_string();
let version = cargo
.as_ref()
.and_then(|t| t.get("package"))
.and_then(|p| p.as_table())
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("0.1.0")
.to_string();
DumpWriteBuilder {
dump: self,
name,
version: Some(version),
}
}
pub fn unpack(&self) -> Result<HashMap<String, String>, SdmpError> {
let mut map = HashMap::new();
for (path, encoded) in &self.files {
let content = self.decode_file(encoded)?;
map.insert(path.clone(), content);
}
Ok(map)
}
pub fn unpack_to(&self, dir: &str) -> Result<(), SdmpError> {
for (path, encoded) in &self.files {
let content = self.decode_file(encoded)?;
let full_path = std::path::Path::new(dir).join(path);
if let Some(parent) = full_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(full_path, content)?;
}
Ok(())
}
pub fn to_log(&self, path: &str) -> Result<(), SdmpError> {
use std::fmt::Write as FmtWrite;
let mut log = String::new();
writeln!(log, "===> MANIFEST <===").unwrap();
writeln!(log, "codec: {}", self.codec).unwrap();
writeln!(log, "files: {}", self.files.len()).unwrap();
writeln!(log, "original_size: {}", self.original_size).unwrap();
for (file_path, encoded) in &self.files {
let content = self.decode_file(encoded)?;
writeln!(log, "\n===> {file_path}").unwrap();
log.push_str(&content);
writeln!(log, "===> EOF").unwrap();
}
std::fs::write(path, log)?;
Ok(())
}
}
pub struct DumpWriteBuilder<'a> {
dump: &'a Dump,
name: String,
version: Option<String>,
}
impl DumpWriteBuilder<'_> {
#[must_use]
pub fn version(mut self, version: &str) -> Self {
self.version = Some(version.to_string());
self
}
pub fn write(self) -> Result<(), SdmpError> {
let version = self.version.unwrap_or("0.1.0".to_string());
let format = format_description::parse("[year]-[month]-[day]").unwrap();
let date = OffsetDateTime::now_local()
.unwrap_or(OffsetDateTime::now_utc())
.format(&format)
.unwrap();
let filename = format!("{}-{}-{}.srcdmp", date, self.name, version);
self.dump.write(&filename)
}
pub fn write_to(self, dir: &str) -> Result<(), SdmpError> {
let version = self.version.unwrap_or("0.1.0".to_string());
let format = time::format_description::parse("[year]-[month]-[day]").unwrap();
let date = time::OffsetDateTime::now_local()
.unwrap_or(time::OffsetDateTime::now_utc())
.format(&format)
.unwrap();
let filename = format!("{}/{}-{}-{}.srcdmp", dir, date, self.name, version);
self.dump.write(&filename)
}
}
fn is_binary(content: &[u8]) -> bool {
content.iter().take(8000).any(|&b| b == 0x00)
}
fn has_annotation(content: &[u8], annotation: &[u8]) -> bool {
let search_area = &content[..content.len().min(512)];
search_area
.windows(annotation.len())
.any(|w| w == annotation)
}
fn glob_match(pattern: &str, path: &str) -> bool {
glob::Pattern::new(pattern)
.map(|p| p.matches(path))
.unwrap_or(false)
}