use crate::binary::BinaryWriter;
use crate::{addon_metadata::AddonMetadata, result::Result, AddonTag, AddonType, Error, IDENT};
use crc::Crc;
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
use std::{
fs::File,
path::Path,
time::{Duration, SystemTime, UNIX_EPOCH},
};
const DEFAULT_VERSION: u8 = 3;
const DEFAULT_STEAMID: u64 = 0;
const DEFAULT_DESCRIPTION: &str = "";
const DEFAULT_AUTHOR: &str = "unknown";
const DEFAULT_COMPRESSION: bool = false;
enum BuilderFileReader {
FSFile(BufReader<File>),
Bytes(Vec<u8>),
Reader(Box<dyn Read>),
}
struct BuilderFile {
filename: String,
reader: BuilderFileReader,
}
struct FilePatchInfo {
filesize: u64,
crc: u32,
}
pub struct GMABuilder {
version: Option<u8>,
steamid: Option<u64>,
timestamp: Option<u64>,
name: Option<String>,
description: Option<String>,
author: Option<String>,
files: Vec<BuilderFile>,
addon_type: AddonType,
addon_tags: [Option<AddonTag>; 2],
compression: Option<bool>,
}
impl GMABuilder {
pub fn new() -> Self {
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| Duration::new(0, 0))
.as_secs() as u64;
Self {
version: Some(DEFAULT_VERSION),
steamid: Some(DEFAULT_STEAMID),
timestamp: Some(current_timestamp),
name: None,
description: Some(DEFAULT_DESCRIPTION.to_owned()),
author: Some(DEFAULT_AUTHOR.to_owned()),
files: Vec::new(),
addon_type: AddonType::Tool,
addon_tags: [None; 2],
compression: Some(DEFAULT_COMPRESSION),
}
}
pub fn version(&mut self, version: u8) -> &mut Self {
self.version = Some(version);
self
}
pub fn steamid(&mut self, steamid: u64) -> &mut Self {
self.steamid = Some(steamid);
self
}
pub fn timestamp(&mut self, timestamp: u64) -> &mut Self {
self.timestamp = Some(timestamp);
self
}
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
self.name = Some(name.into());
self
}
pub fn description<S: Into<String>>(&mut self, description: S) -> &mut Self {
self.description = Some(description.into());
self
}
pub fn author<S: Into<String>>(&mut self, author: S) -> &mut Self {
self.author = Some(author.into());
self
}
pub fn compression(&mut self, c: bool) -> &mut Self {
self.compression = Some(c);
self
}
pub fn addon_type(&mut self, addon_type: AddonType) -> &mut Self {
self.addon_type = addon_type;
self
}
pub fn addon_tag(&mut self, addon_tag: AddonTag) -> &mut Self {
let (avail1, avail2) = (self.addon_tags[0].is_none(), self.addon_tags[1].is_none());
match (avail1, avail2) {
(false, false) | (true, true) => {
self.addon_tags[1] = self.addon_tags[0];
self.addon_tags[0] = Some(addon_tag)
}
(true, false) | (false, true) => self.addon_tags[1] = Some(addon_tag),
};
self
}
pub fn file_from_path<S: AsRef<Path>>(
&mut self,
path: S,
) -> std::result::Result<&mut Self, std::io::Error> {
let file = File::open(&path)?;
self.files.push(BuilderFile {
filename: path.as_ref().to_string_lossy().as_ref().to_owned(),
reader: BuilderFileReader::FSFile(BufReader::new(file)),
});
Ok(self)
}
pub fn file_with_name<P: AsRef<Path>, N: Into<String>>(
&mut self,
path: P,
name: N,
) -> std::result::Result<&mut Self, std::io::Error> {
let file = File::open(&path)?;
self.files.push(BuilderFile {
filename: name.into(),
reader: BuilderFileReader::FSFile(BufReader::new(file)),
});
Ok(self)
}
pub fn file_from_bytes<S: Into<String>>(&mut self, filename: S, bytes: Vec<u8>) -> &mut Self {
self.files.push(BuilderFile {
filename: filename.into(),
reader: BuilderFileReader::Bytes(bytes),
});
self
}
pub fn file_from_reader<S: Into<String>, R: Read + 'static>(
&mut self,
filename: S,
reader: R,
) -> &mut Self {
self.files.push(BuilderFile {
filename: filename.into(),
reader: BuilderFileReader::Reader(Box::new(reader)),
});
self
}
pub fn write_to<WriterType>(self, mut writer: WriterType) -> Result<()>
where
WriterType: Write + Seek,
{
match self.compression.unwrap() {
true => {
let buffer = Vec::with_capacity(1024 * 1024 * 32);
let mut bufwriter = Cursor::new(buffer);
Self::write_to_gen(self, &mut bufwriter)?;
bufwriter.seek(SeekFrom::Start(0))?;
lzma_rs::lzma_compress(&mut bufwriter, &mut writer).unwrap();
Ok(())
}
false => Self::write_to_gen(self, writer),
}
}
fn write_to_gen<WriterType: Write + Seek>(self, mut writer: WriterType) -> Result<()> {
let name = self
.name
.expect("You need to provided a name for the addon file");
Self::write_ident(&mut writer)?;
writer.write_u8(self.version.unwrap())?;
writer.write_u64(self.steamid.unwrap())?;
writer.write_u64(self.timestamp.unwrap())?;
writer.write_u8(0)?;
writer.write_c_string(&name)?;
let tags: Vec<AddonTag> = self
.addon_tags
.iter()
.filter(|p| p.is_some())
.map(|p| p.unwrap())
.collect();
let metadata = AddonMetadata::new(
name.to_owned(),
self.description.unwrap(),
&self.addon_type,
&tags,
);
let metadata_json = metadata.to_json();
writer.write_c_string(&metadata_json)?;
writer.write_c_string(&self.author.unwrap())?;
writer.write_u32(1)?;
let mut patch_offsets = Vec::with_capacity(self.files.len());
let mut patch_info = Vec::with_capacity(self.files.len());
for (i, entry) in self.files.iter().enumerate() {
let file_number = (i + 1) as u32;
let (_, patch_offset) =
Self::write_incomplete_file_entry(&mut writer, file_number, &entry)?;
patch_offsets.push(patch_offset);
}
writer.write_u32(0)?;
for entry in self.files.into_iter() {
let (_, patch) = Self::write_file_contents(&mut writer, entry)?;
patch_info.push(patch)
}
assert_eq!(patch_info.len(), patch_offsets.len());
for (offset, info) in patch_offsets.into_iter().zip(patch_info.into_iter()) {
Self::apply_file_entry_patch(&mut writer, offset, info)?;
}
Ok(())
}
fn write_ident<WriterType: Write>(mut writer: WriterType) -> Result<usize> {
Ok(writer.write(&IDENT)?)
}
fn write_incomplete_file_entry<WriterType: Write + Seek>(
mut writer: WriterType,
file_number: u32,
bfile: &BuilderFile,
) -> Result<(usize, u64)> {
let mut bytes_written = 0;
bytes_written += writer.write_u32(file_number)?;
bytes_written += writer.write_c_string(&bfile.filename)?;
let offset_to_patch_start = writer.seek(SeekFrom::Current(0))?;
bytes_written += writer.write_u64(0)?;
bytes_written += writer.write_u32(0)?;
Ok((bytes_written, offset_to_patch_start))
}
fn write_file_contents<WriterType: Write + Seek>(
mut writer: WriterType,
bfile: BuilderFile,
) -> Result<(usize, FilePatchInfo)> {
let mut write_contents = |reader: &mut dyn Read| -> Result<(usize, FilePatchInfo)> {
const BLOCK_SIZE: usize = 8096;
let mut bytes_written: usize = 0;
let mut buffer: [u8; BLOCK_SIZE] = [0; BLOCK_SIZE];
let crc = Crc::<u32>::new(&crc::CRC_32_ISO_HDLC);
let mut digest = crc.digest();
loop {
let read_result = reader.read(&mut buffer);
match read_result {
Ok(0) => {
return Ok((
bytes_written,
FilePatchInfo {
filesize: bytes_written as u64,
crc: digest.finalize() as u32,
},
));
}
Ok(n) => {
let data_slice = &buffer[0..n];
digest.update(data_slice);
writer.write_all(data_slice)?;
bytes_written += n;
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(Error::IOError(e)),
}
}
};
match bfile.reader {
BuilderFileReader::FSFile(mut reader) => write_contents(&mut reader),
BuilderFileReader::Bytes(bytes) => write_contents(&mut bytes.as_slice()),
BuilderFileReader::Reader(mut reader) => write_contents(&mut reader),
}
}
fn apply_file_entry_patch<WriterType: Write + Seek>(
mut writer: WriterType,
patch_offset: u64,
patch_info: FilePatchInfo,
) -> Result<()> {
writer.seek(SeekFrom::Start(patch_offset))?;
writer.write_u64(patch_info.filesize)?;
writer.write_u32(patch_info.crc)?;
Ok(())
}
}