use crate::load_wide_string;
pub struct ProjectFile<'a> {
pub(crate) internal: *mut aviutl2_sys::plugin2::PROJECT_FILE,
_marker: std::marker::PhantomData<&'a ()>,
}
#[derive(thiserror::Error, Debug)]
pub enum ProjectFileError {
#[error("key contains null byte: {0}")]
KeyContainsNull(std::ffi::NulError),
#[error("data retrieval failed for key {0}")]
RetrievalFailed(String),
#[error("data length exceeds 4096 bytes, got {0} bytes")]
DataTooLarge(usize),
#[error("value contains null byte: {0}")]
ValueContainsNull(std::ffi::NulError),
}
impl<'a> ProjectFile<'a> {
pub unsafe fn from_raw(raw: *mut aviutl2_sys::plugin2::PROJECT_FILE) -> Self {
Self {
internal: raw,
_marker: std::marker::PhantomData,
}
}
pub fn get_param_string(&self, key: &str) -> Result<String, ProjectFileError> {
let c_key = std::ffi::CString::new(key).map_err(ProjectFileError::KeyContainsNull)?;
unsafe {
let raw_str = ((*self.internal).get_param_string)(c_key.as_ptr() as _);
if raw_str.is_null() {
return Err(ProjectFileError::RetrievalFailed(key.to_string()));
}
Ok(std::ffi::CStr::from_ptr(raw_str)
.to_string_lossy()
.into_owned())
}
}
pub fn get_param_binary(&self, key: &str, data: &mut [u8]) -> Result<(), ProjectFileError> {
let success = unsafe {
let key = std::ffi::CString::new(key).map_err(ProjectFileError::KeyContainsNull)?;
((*self.internal).get_param_binary)(
key.as_ptr() as _,
data.as_mut_ptr() as _,
data.len() as _,
)
};
if !success {
return Err(ProjectFileError::RetrievalFailed(key.to_string()));
}
Ok(())
}
pub fn set_param_string(&mut self, key: &str, value: &str) -> Result<(), ProjectFileError> {
let key_cstr = std::ffi::CString::new(key).map_err(ProjectFileError::KeyContainsNull)?;
let value_cstr =
std::ffi::CString::new(value).map_err(ProjectFileError::ValueContainsNull)?;
unsafe {
((*self.internal).set_param_string)(key_cstr.as_ptr() as _, value_cstr.as_ptr() as _);
}
Ok(())
}
pub fn set_param_binary(&mut self, key: &str, data: &[u8]) -> Result<(), ProjectFileError> {
if data.len() > 4096 {
return Err(ProjectFileError::DataTooLarge(data.len()));
}
unsafe {
let key = std::ffi::CString::new(key).map_err(ProjectFileError::KeyContainsNull)?;
((*self.internal).set_param_binary)(
key.as_ptr() as _,
data.as_ptr() as _,
data.len() as _,
);
}
Ok(())
}
pub fn clear_params(&mut self) {
unsafe { ((*self.internal).clear_params)() }
}
pub fn get_path(&self) -> Option<std::path::PathBuf> {
unsafe {
let raw_str = ((*self.internal).get_project_file_path)();
if raw_str.is_null() {
return None;
}
let path_str = load_wide_string(raw_str);
if path_str.is_empty() {
return None;
}
Some(std::path::PathBuf::from(path_str))
}
}
}
#[cfg(feature = "serde")]
const _: () = {
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use std::io::Read;
static NAMESPACE: &str = "--aviutl2-rs";
const BINARY_CHUNK_SIZE: usize = 4096;
const BASE64_CHUNK_RAW_SIZE: usize = 3072;
#[derive(thiserror::Error, Debug)]
pub enum ProjectFileSerdeError {
#[error("serialization error: {0}")]
Serialization(#[from] rmp_serde::encode::Error),
#[error("deserialization error: {0}")]
Deserialization(#[from] rmp_serde::decode::Error),
#[error("base64 decode error: {0}")]
Base64Decode(#[from] base64::DecodeError),
#[error("zstd dompression error: {0}")]
Decompression(#[from] std::io::Error),
#[error("zlib error: {0:?}")]
Zlib(zlib_rs::ReturnCode),
#[error("project file error: {0}")]
ProjectFile(#[from] ProjectFileError),
#[error("unsupported serialization format")]
UnsupportedFormat,
#[error("invalid header format: {0}")]
InvalidHeaderFormat(String),
#[error("incomplete data retrieved for key")]
IncompleteData,
}
impl<'a> ProjectFile<'a> {
pub fn serialize<T: serde::Serialize>(
&mut self,
key: &str,
value: &T,
) -> Result<(), ProjectFileSerdeError> {
self.delete_old_chunks(key)?;
let base_bytes = rmp_serde::to_vec_named(value)?;
let compressed_bound = zlib_rs::compress_bound(base_bytes.len());
let mut compressed_bytes = vec![0u8; compressed_bound];
let (compressed_bytes, result) = zlib_rs::compress_slice(
&mut compressed_bytes,
&base_bytes,
zlib_rs::DeflateConfig::default(),
);
if result != zlib_rs::ReturnCode::Ok {
return Err(ProjectFileSerdeError::Zlib(result));
}
let num_bytes = compressed_bytes.len();
if num_bytes > base_bytes.len() {
self.set_param_string(
key,
&format!("{NAMESPACE}:serde-rmp-base64-v1:{}", base_bytes.len()),
)?;
for (i, chunk) in base_bytes.chunks(BASE64_CHUNK_RAW_SIZE).enumerate() {
let chunk_key = format!("{NAMESPACE}:serde-base64-chunk:{}:{}", key, i);
self.set_param_string(&chunk_key, &BASE64.encode(chunk))?;
}
return Ok(());
}
self.set_param_string(
key,
&format!(
"{NAMESPACE}:serde-rmp-base64-zlib-v1:{},{}",
num_bytes,
base_bytes.len()
),
)?;
for (i, chunk) in compressed_bytes.chunks(BASE64_CHUNK_RAW_SIZE).enumerate() {
let chunk_key = format!("{NAMESPACE}:serde-base64-chunk:{}:{}", key, i);
self.set_param_string(&chunk_key, &BASE64.encode(chunk))?;
}
Ok(())
}
pub fn deserialize<T: serde::de::DeserializeOwned>(
&self,
key: &str,
) -> Result<T, ProjectFileSerdeError> {
let header = self.get_param_string(key)?;
if let Ok(value) = self.decode_serde_rmp_base64_v1(key, &header) {
return Ok(value);
}
if let Ok(value) = self.decode_serde_zstd_v1(key, &header) {
return Ok(value);
}
if let Ok(value) = self.decode_serde_rmp_v1(key, &header) {
return Ok(value);
}
if let Ok(value) = self.decode_serde_rmp_base64_zlib_v1(key, &header) {
return Ok(value);
}
Err(ProjectFileSerdeError::UnsupportedFormat)
}
fn decode_serde_rmp_base64_v1<T: serde::de::DeserializeOwned>(
&self,
key: &str,
header: &str,
) -> Result<T, ProjectFileSerdeError> {
let header_prefix = format!("{NAMESPACE}:serde-rmp-base64-v1:");
let num_bytes = header
.strip_prefix(&header_prefix)
.ok_or(ProjectFileSerdeError::UnsupportedFormat)?;
let num_bytes: usize = num_bytes
.parse()
.map_err(|_| ProjectFileSerdeError::InvalidHeaderFormat(header.to_string()))?;
if num_bytes == 0 {
return Err(ProjectFileSerdeError::InvalidHeaderFormat(
header.to_string(),
));
}
let chunks = self.collect_base64_chunks(num_bytes, key)?;
let value: T = rmp_serde::from_slice(&chunks)?;
Ok(value)
}
fn decode_serde_rmp_v1<T: serde::de::DeserializeOwned>(
&self,
key: &str,
header: &str,
) -> Result<T, ProjectFileSerdeError> {
let header_prefix = format!("{NAMESPACE}:serde-rmp-v1:");
let num_bytes = header
.strip_prefix(&header_prefix)
.ok_or(ProjectFileSerdeError::UnsupportedFormat)?;
let num_bytes: usize = num_bytes
.parse()
.map_err(|_| ProjectFileSerdeError::InvalidHeaderFormat(header.to_string()))?;
if num_bytes == 0 {
return Err(ProjectFileSerdeError::InvalidHeaderFormat(
header.to_string(),
));
}
let chunks = self.collect_chunks(num_bytes, key)?;
let value: T = rmp_serde::from_slice(&chunks)?;
Ok(value)
}
fn decode_serde_rmp_base64_zlib_v1<T: serde::de::DeserializeOwned>(
&self,
key: &str,
header: &str,
) -> Result<T, ProjectFileSerdeError> {
let header_prefix = format!("{NAMESPACE}:serde-rmp-base64-zlib-v1:");
let num_bytes = header
.strip_prefix(&header_prefix)
.ok_or(ProjectFileSerdeError::UnsupportedFormat)?;
let (compressed_size, uncompressed_size) =
num_bytes
.split_once(',')
.ok_or(ProjectFileSerdeError::InvalidHeaderFormat(
header.to_string(),
))?;
let compressed_size: usize = compressed_size
.parse()
.map_err(|_| ProjectFileSerdeError::InvalidHeaderFormat(header.to_string()))?;
let uncompressed_size: usize = uncompressed_size
.parse()
.map_err(|_| ProjectFileSerdeError::InvalidHeaderFormat(header.to_string()))?;
if compressed_size == 0 || uncompressed_size == 0 {
return Err(ProjectFileSerdeError::InvalidHeaderFormat(
header.to_string(),
));
}
let compressed_chunks = self.collect_base64_chunks(compressed_size, key)?;
let mut decompressed_bytes = vec![0u8; uncompressed_size];
let (decompressed_bytes, result) = zlib_rs::decompress_slice(
&mut decompressed_bytes,
&compressed_chunks,
zlib_rs::InflateConfig::default(),
);
if result != zlib_rs::ReturnCode::Ok {
return Err(ProjectFileSerdeError::Zlib(result));
}
let value: T = rmp_serde::from_slice(decompressed_bytes)?;
Ok(value)
}
fn collect_chunks(
&self,
num_bytes: usize,
key: &str,
) -> Result<Vec<u8>, ProjectFileSerdeError> {
let mut bytes = Vec::with_capacity(num_bytes);
let mut read_bytes = 0;
let mut chunk = vec![0u8; BINARY_CHUNK_SIZE];
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-chunk:{}:{}", key, i);
let to_read = std::cmp::min(BINARY_CHUNK_SIZE, num_bytes - read_bytes);
chunk.resize(to_read, 0);
match self.get_param_binary(&chunk_key, &mut chunk) {
Ok(()) => {
bytes.extend_from_slice(&chunk);
read_bytes += to_read;
if read_bytes >= num_bytes {
break;
}
}
Err(_) => break,
}
}
if read_bytes != num_bytes {
return Err(ProjectFileSerdeError::IncompleteData);
}
Ok(bytes)
}
fn collect_base64_chunks(
&self,
num_bytes: usize,
key: &str,
) -> Result<Vec<u8>, ProjectFileSerdeError> {
let mut bytes = Vec::with_capacity(num_bytes);
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-base64-chunk:{}:{}", key, i);
let chunk = match self.get_param_string(&chunk_key) {
Ok(chunk) => chunk,
Err(_) => break,
};
let chunk = BASE64.decode(chunk)?;
bytes.extend_from_slice(&chunk);
if bytes.len() >= num_bytes {
break;
}
}
if bytes.len() != num_bytes {
return Err(ProjectFileSerdeError::IncompleteData);
}
Ok(bytes)
}
fn decode_serde_zstd_v1<T: serde::de::DeserializeOwned>(
&self,
key: &str,
header: &str,
) -> Result<T, ProjectFileSerdeError> {
let header_prefix = format!("{NAMESPACE}:serde-zstd-v1:");
let num_bytes = header
.strip_prefix(&header_prefix)
.ok_or(ProjectFileSerdeError::UnsupportedFormat)?;
let num_bytes: usize = num_bytes
.parse()
.map_err(|_| ProjectFileSerdeError::InvalidHeaderFormat(header.to_string()))?;
if num_bytes == 0 {
return Err(ProjectFileSerdeError::InvalidHeaderFormat(
header.to_string(),
));
}
let mut bytes = Vec::with_capacity(num_bytes);
let mut read_bytes = 0;
let mut chunk = vec![0u8; BINARY_CHUNK_SIZE];
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-zstd-v1:chunk:{}:{}", key, i);
let to_read = std::cmp::min(BINARY_CHUNK_SIZE, num_bytes - read_bytes);
chunk.resize(to_read, 0);
match self.get_param_binary(&chunk_key, &mut chunk) {
Ok(()) => {
bytes.extend_from_slice(&chunk);
read_bytes += to_read;
if read_bytes >= num_bytes {
break;
}
}
Err(_) => break,
}
}
if read_bytes != num_bytes {
return Err(ProjectFileSerdeError::IncompleteData);
}
let mut decoder = ruzstd::decoding::StreamingDecoder::new(&bytes[..])
.map_err(|e| ProjectFileSerdeError::Decompression(std::io::Error::other(e)))?;
let mut decompressed_bytes = vec![];
decoder.read_to_end(&mut decompressed_bytes)?;
let value: T = rmp_serde::from_slice(&decompressed_bytes)?;
Ok(value)
}
fn delete_old_chunks(&mut self, key: &str) -> Result<(), ProjectFileSerdeError> {
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-base64-chunk:{}:{}", key, i);
match self.get_param_string(&chunk_key) {
Ok(_) => {
self.set_param_string(&chunk_key, "")?;
}
Err(_) => break,
}
}
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-chunk:{}:{}", key, i);
match self.get_param_binary(&chunk_key, &mut [0u8; 1]) {
Ok(_) => {
self.set_param_string(&chunk_key, "")?;
}
Err(_) => break,
}
}
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-zstd-v1:chunk:{}:{}", key, i);
match self.get_param_binary(&chunk_key, &mut [0u8; 1]) {
Ok(_) => {
self.set_param_string(&chunk_key, "")?;
}
Err(_) => break,
}
}
for i in 0.. {
let chunk_key = format!("{NAMESPACE}:serde-base64-chunk:{}:{}", key, i);
match self.get_param_string(&chunk_key) {
Ok(_) => {
self.set_param_string(&chunk_key, "")?;
}
Err(_) => break,
}
}
Ok(())
}
}
};