use std::{collections::HashSet, fs::{self, File}, io::{self, Read, Seek, Write}, path::{Path, PathBuf}};
use bytes::Bytes;
use nanoid::nanoid;
use regex::Regex;
use walkdir::{DirEntry, WalkDir};
use zip::{result::ZipResult, write::SimpleFileOptions};
use crate::{model::{Field, Format, Graph, NodeRef, Profile, SELF_STR_KEYWORD, SUPER_STR_KEYWORD}, parser::context::ParseContext, runtime::{Error, Val}};
#[derive(Debug, Clone)]
pub struct StofPackageFormat {
pub tmp: String,
}
impl Default for StofPackageFormat {
fn default() -> Self {
Self {
tmp: format!("{}/__stoftmp__", std::env::temp_dir().display()),
}
}
}
impl StofPackageFormat {
pub fn remove(path: &str) -> Result<(), std::io::Error> {
fs::remove_file(path)
}
pub fn create_package_file(dir_path: &str, dest_path: &str, included: &HashSet<String>, excluded: &HashSet<String>) -> Option<String> {
let mut path = dest_path.to_string();
if !path.ends_with(".pkg") { path = format!("{}.pkg", path); }
let mut dir_pth_buf = path.split('/').collect::<Vec<&str>>();
dir_pth_buf.pop();
let dir_pth = dir_pth_buf.join("/");
if dir_pth.len() > 0 { let _ = fs::create_dir_all(&dir_pth); }
let file = fs::File::create(&path).unwrap();
let walkdir = WalkDir::new(dir_path);
let iter = walkdir.into_iter();
let res = Self::zip_directory(&mut iter.filter_map(|e| e.ok()), dir_path, file, zip::CompressionMethod::Bzip2, included, excluded);
if res.is_err() {
return None;
}
return Some(path);
}
pub fn create_temp_package_file(&self, dir_path: &str, included: &HashSet<String>, excluded: &HashSet<String>) -> Option<String> {
let _ = fs::create_dir_all(&self.tmp);
let path = format!("{}/{}.pkg", self.tmp, nanoid!());
Self::create_package_file(dir_path, &path, included, excluded)
}
fn zip_directory<T: Write + Seek>(iter: &mut dyn Iterator<Item = DirEntry>, prefix: &str, writer: T, method: zip::CompressionMethod, included: &HashSet<String>, excluded: &HashSet<String>) -> ZipResult<()> {
let mut zip = zip::ZipWriter::new(writer);
let options = SimpleFileOptions::default().compression_method(method).unix_permissions(0o755);
let pref = Path::new(prefix);
let mut buffer = Vec::new();
'entries: for entry in iter {
let path = entry.path();
let name = path.strip_prefix(pref).unwrap();
let path_as_string = name
.to_str()
.map(str::to_owned)
.unwrap();
if included.len() > 0 {
let mut found_match = false;
for include in included {
if let Ok(re) = Regex::new(&include) {
if re.is_match(&path_as_string) {
found_match = true;
break;
}
}
}
if !found_match {
continue 'entries;
}
}
if excluded.len() > 0 {
for exclude in excluded {
if let Ok(re) = Regex::new(&exclude) {
if re.is_match(&path_as_string) {
continue 'entries;
}
}
}
}
if path.is_file() {
zip.start_file(path_as_string, options)?;
let mut f = File::open(path)?;
f.read_to_end(&mut buffer)?;
zip.write_all(&buffer)?;
buffer.clear();
} else if !name.as_os_str().is_empty() {
zip.add_directory(path_as_string, options)?;
}
}
zip.finish()?;
Ok(())
}
pub fn unzip_bytes(&self, output_dir_path: &str, bytes: &Bytes) {
let _ = fs::create_dir_all(&self.tmp);
let tmp_file_path = format!("{}/{}.pkg", &self.tmp, nanoid!());
let _ = fs::write(&tmp_file_path, bytes);
Self::unzip_file(&tmp_file_path, output_dir_path);
let _ = fs::remove_file(&tmp_file_path);
}
pub fn unzip_bytes_to_temp(&self, bytes: &Bytes) -> Option<String> {
let outdir = format!("{}/{}", &self.tmp, nanoid!());
let _ = fs::create_dir_all(&outdir);
let tmp_file_path = format!("{}/{}.pkg", &self.tmp, nanoid!());
let _ = fs::write(&tmp_file_path, bytes);
Self::unzip_file(&tmp_file_path, &outdir);
let _ = fs::remove_file(&tmp_file_path);
Some(outdir)
}
pub fn unzip_file(pkg_file_path: &str, output_dir_path: &str) {
let _ = fs::create_dir_all(output_dir_path);
if let Ok(file) = fs::File::open(pkg_file_path) {
if let Ok(mut archive) = zip::ZipArchive::new(file) {
for i in 0..archive.len() {
let mut file = archive.by_index(i).unwrap();
let outname = match file.enclosed_name() {
Some(path) => path,
None => continue,
};
let mut outpath = PathBuf::from(output_dir_path);
outpath.push(outname);
if file.is_dir() {
let _ = fs::create_dir_all(&outpath);
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
let _ = fs::create_dir_all(p);
}
}
if let Ok(mut outfile) = fs::File::create(&outpath) {
let _ = io::copy(&mut file, &mut outfile);
}
}
}
}
}
}
}
impl Format for StofPackageFormat {
fn identifiers(&self) -> Vec<String> {
vec!["pkg".into()]
}
fn content_type(&self) -> String {
"application/octet-stream+pkg".into()
}
fn binary_import(&self, graph: &mut Graph, format: &str, bytes: Bytes, node: Option<NodeRef>, profile: &Profile) -> Result<(), Error> {
if let Some(path) = self.unzip_bytes_to_temp(&bytes) {
let res = self.file_import(graph, format, &path, node, profile);
let _ = fs::remove_dir_all(&path);
res
} else {
Err(Error::PKGImport(format!("error unzipping bytes into temp PKG for binary import")))
}
}
fn parser_import(&self, _format: &str, path: &str, context: &mut ParseContext) -> Result<(), Error> {
let mut package_path = path.to_string();
let mut cleanup_dir = None;
if path.ends_with(".pkg") {
let tmp_dir = format!("{}/{}", &self.tmp, nanoid!());
Self::unzip_file(path, &tmp_dir);
package_path = format!("{tmp_dir}/pkg.stof");
cleanup_dir = Some(tmp_dir);
} else if path.ends_with(".stof") {
} else {
let mut buf = path.split('.').collect::<Vec<&str>>();
if buf.len() > 1 { buf.pop(); }
let cwd = buf.join(".");
package_path = format!("{}/pkg.stof", &cwd);
}
let cleanup = move || {
if let Some(cleanup_dir) = cleanup_dir {
let _ = fs::remove_dir_all(&cleanup_dir);
}
};
let mut pkg_graph = Graph::default();
let res = pkg_graph.parse_stof_file("stof", &package_path, None, context.profile.clone());
if res.is_err() {
cleanup();
return res;
}
if let Some(import_ref) = Field::field_from_path(&mut pkg_graph, "root.import", None) {
let mut import_value = None;
if let Some(import_field) = pkg_graph.get_stof_data::<Field>(&import_ref) {
import_value = Some(import_field.value.val.read().clone());
}
if let Some(import_val) = import_value {
context.push_relative_import_stack_file(&package_path);
let res = perform_imports(&pkg_graph, import_val, context);
context.pop_relative_import_stack();
if res.is_err() {
cleanup();
return res;
}
}
}
cleanup();
Ok(())
}
fn file_import(&self, graph: &mut Graph, _format: &str, path: &str, node: Option<NodeRef>, profile: &Profile) -> Result<(), Error> {
let mut package_path = path.to_string();
let mut cleanup_dir = None;
if path.ends_with(".pkg") {
let tmp_dir = format!("{}/{}", &self.tmp, nanoid!());
Self::unzip_file(path, &tmp_dir);
package_path = format!("{tmp_dir}/pkg.stof");
cleanup_dir = Some(tmp_dir);
} else if path.ends_with(".stof") {
} else {
let mut buf = path.split('.').collect::<Vec<&str>>();
if buf.len() > 1 { buf.pop(); }
let cwd = buf.join(".");
package_path = format!("{}/pkg.stof", &cwd);
}
let cleanup = move || {
if let Some(cleanup_dir) = cleanup_dir {
let _ = fs::remove_dir_all(&cleanup_dir);
}
};
let mut pkg_graph = Graph::default();
let res = pkg_graph.parse_stof_file("stof", &package_path, None, profile.clone());
if res.is_err() {
cleanup();
return res;
}
if let Some(import_ref) = Field::field_from_path(&mut pkg_graph, "root.import", None) {
let mut import_value = None;
if let Some(import_field) = pkg_graph.get_stof_data::<Field>(&import_ref) {
import_value = Some(import_field.value.val.read().clone());
}
if let Some(import_val) = import_value {
let mut context = ParseContext::new(graph, profile.clone());
if let Some(node) = node {
context.push_self_node(node);
}
context.push_relative_import_stack_file(&package_path);
let res = perform_imports(&pkg_graph, import_val, &mut context);
context.pop_relative_import_stack();
if res.is_err() {
cleanup();
return res;
}
}
}
cleanup();
Ok(())
}
}
fn perform_imports(pkg_graph: &Graph, import_val: Val, context: &mut ParseContext) -> Result<(), Error> {
match import_val {
Val::Str(path) => {
let mut res_format = "stof".to_string();
let path_list = path.trim_start_matches('.').split('.').collect::<Vec<_>>();
if path_list.len() > 1 {
res_format = path_list.last().unwrap().to_string();
}
context.parse_from_file(&res_format, &format!("./{}", path), None)?;
},
Val::Obj(nref) => {
let mut path = String::default();
if let Some(path_ref) = Field::direct_field(pkg_graph, &nref, "path") {
if let Some(path_field) = pkg_graph.get_stof_data::<Field>(&path_ref) {
path = path_field.value.val.read().print(pkg_graph);
}
}
let mut format = "stof".to_string();
if let Some(format_ref) = Field::direct_field(pkg_graph, &nref, "format") {
if let Some(format_field) = pkg_graph.get_stof_data::<Field>(&format_ref) {
format = format_field.value.val.read().print(pkg_graph);
}
} else if path.len() > 0 {
let path_list = path.trim_start_matches('.').split('.').collect::<Vec<_>>();
if path_list.len() > 1 {
format = path_list.last().unwrap().to_string();
}
}
let mut scope = "self".to_string();
if let Some(scope_ref) = Field::direct_field(pkg_graph, &nref, "as") {
if let Some(scope_field) = pkg_graph.get_stof_data::<Field>(&scope_ref) {
scope = scope_field.value.val.read().print(pkg_graph);
}
}
if let Some(scope_ref) = Field::direct_field(pkg_graph, &nref, "on") {
if let Some(scope_field) = pkg_graph.get_stof_data::<Field>(&scope_ref) {
scope = scope_field.value.val.read().print(pkg_graph);
}
}
if let Some(scope_ref) = Field::direct_field(pkg_graph, &nref, "scope") {
if let Some(scope_field) = pkg_graph.get_stof_data::<Field>(&scope_ref) {
scope = scope_field.value.val.read().print(pkg_graph);
}
}
if path.len() > 0 {
let mut start = None;
if scope.starts_with(SELF_STR_KEYWORD.as_str()) || scope.starts_with(SUPER_STR_KEYWORD.as_str()) {
start = Some(context.self_ptr());
}
let node = context.graph.ensure_named_nodes(&scope, start, true, None);
context.parse_from_file(&format, &format!("./{}", path), node)?;
}
},
Val::Tup(vals) |
Val::List(vals) => {
for val in vals {
perform_imports(pkg_graph, val.read().clone(), context)?;
}
},
Val::Set(vals) => {
for val in vals {
perform_imports(pkg_graph, val.read().clone(), context)?;
}
},
_ => {
return Err(Error::PKGImport(format!("invalid pkg.stof import field value")));
}
}
Ok(())
}