use crate::ro_crate::read::read_crate;
use crate::ro_crate::rocrate::RoCrate;
use std::fmt;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use url::Url;
use walkdir::WalkDir;
use zip::{write::FileOptions, ZipWriter};
pub fn write_crate(rocrate: &RoCrate, name: String) {
match serde_json::to_string_pretty(&rocrate) {
Ok(json_ld) => match File::create(name) {
Ok(mut file) => {
if writeln!(file, "{}", json_ld).is_err() {
eprintln!("Failed to write to the file.");
}
}
Err(e) => eprintln!("Failed to create file: {}", e),
},
Err(e) => eprintln!("Serialization failed: {}", e),
}
}
fn write_crate_to_zip(
rocrate: &RoCrate,
name: String,
zip: &mut ZipWriter<File>,
options: FileOptions,
) -> Result<(), ZipError> {
let json_ld = serde_json::to_string_pretty(&rocrate)
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
zip.start_file(name, options)
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
zip.write_all(json_ld.as_bytes())
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
Ok(())
}
pub fn zip_crate(crate_path: &Path, external: bool, validation_level: i8) -> Result<(), ZipError> {
let crate_abs = get_absolute_path(crate_path).unwrap();
let root = crate_abs.parent().unwrap();
let zip_file_base_name = root
.file_name()
.ok_or(ZipError::FileNameNotFound)?
.to_str()
.ok_or(ZipError::FileNameConversionFailed)?;
let zip_file_name = root.join(format!("{}.zip", zip_file_base_name));
let file = File::create(&zip_file_name).map_err(ZipError::IoError)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut rocrate = read_crate(&crate_abs.to_path_buf(), validation_level).unwrap();
for entry in WalkDir::new(root)
.min_depth(0)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
{
let path = entry.path();
if path == zip_file_name {
continue;
}
let relative_path = path.strip_prefix(root).map_err(ZipError::from)?;
let relative_path_str = relative_path
.to_str()
.ok_or(ZipError::FileNameConversionFailed)?;
let mut file = fs::File::open(path).map_err(ZipError::IoError)?;
zip.start_file(relative_path_str, options)
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
io::copy(&mut file, &mut zip).map_err(ZipError::IoError)?;
let abs_path = get_absolute_path(path).unwrap();
update_zip_ids(&mut rocrate, abs_path, relative_path_str);
}
if external {
zip = zip_crate_external(&mut rocrate, &crate_abs, zip, options)?
}
let _ = write_crate_to_zip(
&rocrate,
"ro-crate-metadata.json".to_string(),
&mut zip,
options,
);
zip.finish()
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
Ok(())
}
#[derive(Debug)]
pub enum ZipError {
EmptyDirectoryVector,
FileNameNotFound,
FileNameConversionFailed,
PathError(std::path::StripPrefixError),
ZipOperationError(String),
IoError(io::Error),
}
impl fmt::Display for ZipError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ZipError::EmptyDirectoryVector => write!(f, "Directory vector is empty"),
ZipError::FileNameNotFound => write!(f, "File name not found"),
ZipError::FileNameConversionFailed => write!(f, "Failed to convert file name"),
ZipError::ZipOperationError(ref msg) => write!(f, "Zip operation Error: {}", msg),
ZipError::PathError(ref err) => write!(f, "Path error: {}", err),
ZipError::IoError(ref err) => write!(f, "IO error: {}", err),
}
}
}
impl std::error::Error for ZipError {}
impl From<io::Error> for ZipError {
fn from(err: io::Error) -> ZipError {
ZipError::IoError(err)
}
}
impl From<std::path::StripPrefixError> for ZipError {
fn from(err: std::path::StripPrefixError) -> ZipError {
ZipError::PathError(err)
}
}
pub fn zip_crate_external(
rocrate: &mut RoCrate,
crate_path: &Path,
mut zip: ZipWriter<File>,
options: FileOptions,
) -> Result<ZipWriter<File>, ZipError> {
let mut ids = rocrate.get_all_ids();
ids.retain(|id| is_not_url(id));
let nonrels = get_nonrelative_paths(&ids, crate_path);
if !nonrels.is_empty() {
for external in nonrels {
let file_name = external
.file_name()
.ok_or(ZipError::FileNameNotFound)?
.to_str()
.ok_or(ZipError::FileNameConversionFailed)?;
let zip_entry_name = format!("external/{}", file_name);
let mut file = fs::File::open(&external).map_err(ZipError::IoError)?;
zip.start_file(&zip_entry_name, options)
.map_err(|e| ZipError::ZipOperationError(e.to_string()))?;
let copy_result = io::copy(&mut file, &mut zip).map_err(ZipError::IoError);
match copy_result {
Ok(_) => {
update_zip_ids(rocrate, external, &zip_entry_name);
}
Err(e) => return Err(e),
}
}
}
Ok(zip)
}
fn update_zip_ids(rocrate: &mut RoCrate, id: PathBuf, zip_id: &str) {
let id_str = id.to_str().unwrap_or_default();
if rocrate.update_id_recursive(id_str, zip_id).is_some() {
return;
}
if id_str.starts_with(r"\\?\") {
let stripped_id = &id_str[4..];
if rocrate.update_id_recursive(stripped_id, zip_id).is_some() {
return;
}
if id_str.contains("\\\\") {
let normalized_id = stripped_id.replace("\\\\", "\\");
rocrate.update_id_recursive(&normalized_id, zip_id);
}
}
}
fn get_nonrelative_paths(ids: &Vec<&String>, crate_dir: &Path) -> Vec<PathBuf> {
let mut nonrels: Vec<PathBuf> = Vec::new();
let rocrate_path = get_absolute_path(crate_dir).unwrap();
for id in ids.iter() {
if id.starts_with('#') {
continue;
}
if let Some(path) = get_absolute_path(Path::new(id)) {
if path.exists() {
if is_outside_base_folder(&rocrate_path, &path) {
nonrels.push(path);
}
}
}
}
nonrels
}
fn get_absolute_path(relative_path: &Path) -> Option<PathBuf> {
match fs::canonicalize(relative_path) {
Ok(path) => Some(path),
Err(_e) => None,
}
}
fn is_not_url(path: &str) -> bool {
let is_extended_windows_path = path.starts_with(r"\\?\");
let is_normal_file_path = path.starts_with(r"\\") || path.chars().next().map(|c| c.is_alphabetic() && path.chars().nth(1) == Some(':')).unwrap_or(false) || path.starts_with('/') || path.starts_with('.');
if is_extended_windows_path || is_normal_file_path {
return true;
}
Url::parse(path).is_err()
}
fn is_outside_base_folder(base_folder: &Path, file_path: &Path) -> bool {
!file_path.starts_with(base_folder)
}
#[cfg(test)]
mod write_crate_tests {
use super::*;
use crate::ro_crate::read::read_crate;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
fn fixture_path(relative_path: &str) -> PathBuf {
Path::new("tests/fixtures").join(relative_path)
}
#[test]
fn test_write_crate_success() {
let path = fixture_path("_ro-crate-metadata-minimal.json");
let rocrate = read_crate(&path, 0).unwrap();
let file_name = "test_rocrate_output.json";
write_crate(&rocrate, file_name.to_string());
assert!(Path::new(file_name).exists());
let file_content = fs::read_to_string(file_name).expect("Failed to read file");
let expected_json = serde_json::to_string_pretty(&rocrate).expect("Failed to serialize");
assert_eq!(file_content.trim_end(), expected_json);
fs::remove_file(file_name).expect("Failed to remove test file");
}
}