use crate::dir_context::{PathResolver, find_to_run_pack_dir, resolve_pack_ref_base_path};
use crate::runtime::Runtime;
use crate::script::support::{get_value_prop_as_string, into_vec_of_strings};
use crate::types::{DestOptions, FileRecord, FileRef, PackRef};
use crate::{Error, Result};
use mlua::{FromLua as _, Lua, Value};
use simple_fs::SPath;
use std::borrow::Cow;
use std::path::Path;
use std::str::FromStr;
pub fn check_access_write(full_path: &SPath, wks_dir: &SPath) -> Result<()> {
if let Some(rel_path) = full_path.diff(wks_dir)
&& rel_path.as_str().starts_with("..")
{
if !full_path.as_str().contains(".aipack-base") {
return Err(Error::custom(format!(
"Save file protection - The path `{rel_path}` does not belong to the workspace dir `{wks_dir}` or to the .aipack-base.\nCannot save file out of workspace or aipack base at this point"
)));
}
}
Ok(())
}
pub fn check_access_delete(full_path: &SPath, wks_dir: &SPath) -> Result<()> {
if full_path.as_str().contains(".aipack-base") {
return Err(Error::custom(
"Delete file protection - Deleting files from the `.aipack-base` folder is not allowed".to_string(),
));
}
if let Some(rel_path) = full_path.diff(wks_dir)
&& rel_path.as_str().starts_with("..")
{
return Err(Error::custom(format!(
"Delete file protection - The path `{rel_path}` does not belong to the workspace dir `{wks_dir}`.\nCannot delete files outside the workspace"
)));
}
Ok(())
}
pub fn base_dir_and_globs(
runtime: &Runtime,
include_globs: Value,
options: Option<&Value>,
) -> Result<(Option<SPath>, Vec<String>)> {
let globs: Vec<String> = into_vec_of_strings(include_globs, "file::file_list globs argument")?;
let base_dir = compute_base_dir(runtime, options)?.or_else(|| runtime.dir_context().wks_dir().cloned());
let processed_globs = process_path_references(runtime, globs)?;
Ok((base_dir, processed_globs))
}
pub fn process_path_reference(runtime: &Runtime, path: &str) -> Result<SPath> {
let dir_context = runtime.dir_context();
let path: Cow<str> = if let Some(path_from_home) = path.strip_prefix("~/") {
format!("{}/{path_from_home}", dir_context.home_dir()).into()
} else {
path.into()
};
if let Some(pack_ref_str) = extract_pack_reference(&path) {
if let Ok(partial_pack) = PackRef::from_str(pack_ref_str) {
if let Ok(base_dir) = resolve_pack_ref_base_path(dir_context, &partial_pack) {
let sub_path = partial_pack.sub_path.unwrap_or_default();
let pack_path = base_dir.join(&sub_path);
let remaining_path = path.strip_prefix(pack_ref_str).unwrap_or("").trim_start_matches('/');
let resolved_path = if remaining_path.is_empty() {
pack_path.to_string()
} else {
pack_path.join(remaining_path).to_string()
};
return Ok(resolved_path.into());
}
}
}
let path = SPath::new(&path);
if dir_context.is_tmp_path(&path) {
dir_context.resolve_tmp_path(runtime.session(), &path)
}
else {
Ok(path)
}
}
pub fn process_path_references(runtime: &Runtime, globs: Vec<String>) -> Result<Vec<String>> {
let mut processed_globs = Vec::with_capacity(globs.len());
for glob in globs {
let glob = process_path_reference(runtime, &glob)?;
processed_globs.push(glob.to_string());
}
Ok(processed_globs)
}
fn extract_pack_reference(glob: &str) -> Option<&str> {
if !glob.contains('@') {
return None;
}
let wildcard_pos = glob.find(['*', '?', '[']);
let reference = match wildcard_pos {
Some(pos) => {
match glob[..pos].rfind('/') {
Some(sep_pos) => &glob[..=sep_pos],
None => glob, }
}
None => glob,
};
Some(reference)
}
pub fn compute_base_dir(runtime: &Runtime, options: Option<&Value>) -> Result<Option<SPath>> {
let dir_context = runtime.dir_context();
let workspace_path = dir_context.wks_dir().ok_or("Workspace dir is missing")?.clone();
let base_dir = get_value_prop_as_string(options, "base_dir", "aip.file... options fail")?;
let base_dir = match base_dir {
Some(base_dir) => {
if let Some(pack_ref_str) = extract_pack_reference(&base_dir) {
if let Ok(pack_ref) = PackRef::from_str(pack_ref_str) {
if let Ok(pack_dir) = find_to_run_pack_dir(dir_context, &pack_ref) {
let sub_path = pack_ref.sub_path.unwrap_or_default();
let remaining_path = base_dir.strip_prefix(pack_ref_str).unwrap_or("").trim_start_matches('/');
if remaining_path.is_empty() {
Some(pack_dir.path.join(sub_path))
} else {
Some(pack_dir.path.join(sub_path).join(remaining_path))
}
} else {
if crate::support::paths::is_relative(&base_dir) {
Some(workspace_path.join(&base_dir))
} else {
Some(SPath::from(base_dir))
}
}
} else {
if crate::support::paths::is_relative(&base_dir) {
Some(workspace_path.join(&base_dir))
} else {
Some(SPath::from(base_dir))
}
}
} else {
if crate::support::paths::is_relative(&base_dir) {
Some(workspace_path.join(&base_dir))
} else {
Some(SPath::from(base_dir))
}
}
}
None => None,
};
Ok(base_dir)
}
pub fn create_file_records(
runtime: &Runtime,
file_refs: Vec<FileRef>,
base_path: Option<&SPath>,
absolute: bool,
) -> Result<Vec<FileRecord>> {
let mut has_base_path = false;
let base_path = match base_path {
Some(base_path) => {
has_base_path = true;
base_path.clone()
}
None => runtime
.dir_context()
.wks_dir()
.ok_or("Cannot create file records, no workspace")?
.clone(),
};
file_refs
.into_iter()
.map(|file_ref| -> Result<FileRecord> {
if absolute {
let file_record =
FileRecord::load_from_full_path(runtime.dir_context(), file_ref.as_ref(), file_ref.clone().spath)?;
Ok(file_record)
} else {
let full_path = if has_base_path {
base_path.join(file_ref.as_ref())
} else {
let dir_context = runtime.dir_context();
dir_context.resolve_path(runtime.session(), file_ref.clone().spath, PathResolver::WksDir, None)?
};
let sfile_abs = full_path.canonicalize()?;
let diff = sfile_abs.try_diff(&base_path)?;
let (base_path, rel_path) = if diff.as_str().starts_with("..") {
(SPath::from(""), sfile_abs)
} else {
(base_path.clone(), diff)
};
let full_path = base_path.join(&rel_path);
let file_record = FileRecord::load_from_full_path(runtime.dir_context(), &full_path, rel_path)?;
Ok(file_record)
}
})
.collect()
}
pub fn resolve_dest_path(
lua: &Lua,
runtime: &Runtime,
src_rel_path: &SPath,
dest_value: Value,
target_ext: &str,
default_stem_suffix: Option<&str>,
) -> Result<(SPath, SPath)> {
let dir_context = runtime.dir_context();
let opts: DestOptions = DestOptions::from_lua(dest_value, lua)
.map_err(|e| Error::Custom(format!("Failed to parse destination options.\nCause: {e}")))?;
let src_stem = Path::new(src_rel_path.as_str())
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| Error::Custom(format!("Source path '{src_rel_path}' has no file stem.")))?;
let rel_dest_path: SPath = match opts {
DestOptions::Nil => {
let effective_stem = if let Some(def_suf) = default_stem_suffix {
format!("{src_stem}{def_suf}")
} else {
src_stem.to_string()
};
let filename_part = format!("{effective_stem}.{target_ext}");
if let Some(parent_dir) = src_rel_path.parent() {
parent_dir.join(filename_part)
} else {
SPath::new(filename_part)
}
}
DestOptions::Path(p) => p,
DestOptions::Custom(c) => {
let filename_part = if let Some(name_opt) = c.file_name {
name_opt
} else {
let effective_stem = if let Some(opt_suf) = c.suffix {
format!("{src_stem}{opt_suf}")
} else if let Some(def_suf) = default_stem_suffix {
if c.base_dir.is_some() {
src_stem.to_string()
} else {
format!("{src_stem}{def_suf}")
}
} else {
src_stem.to_string()
};
format!("{effective_stem}.{target_ext}")
};
if let Some(base_dir_spath) = c.base_dir {
if base_dir_spath.as_str().is_empty() {
SPath::new(filename_part)
} else {
base_dir_spath.join(filename_part)
}
} else if let Some(parent_dir) = src_rel_path.parent() {
parent_dir.join(filename_part)
} else {
SPath::new(filename_part)
}
}
};
let full_dest_path =
dir_context.resolve_path(runtime.session(), rel_dest_path.clone(), PathResolver::WksDir, None)?;
Ok((rel_dest_path, full_dest_path))
}
#[cfg(test)]
mod tests {
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::assert_contains;
use crate::runtime::Runtime;
use crate::script::aip_modules::support::{process_path_reference, process_path_references};
use crate::support::AsStrsExt;
#[tokio::test]
async fn test_lua_file_support_process_pack_references() -> Result<()> {
let runtime = Runtime::new_test_runtime_sandbox_01().await?;
let fx_globs: Vec<String> = ["ns_b@pack_b_2/main.aip", "no_ns@pack_b_2/main.aip", "**/*.txt"]
.into_iter()
.map(|v| v.to_string())
.collect();
let res = process_path_references(&runtime, fx_globs)?;
let res = res.x_as_strs();
assert_eq!(res.len(), 3, "Should have three globs");
let first = res.first().ok_or("Should have first")?;
assert_contains(*first, "ns_b/pack_b_2/main.aip");
assert_contains(&res, "**/*.txt");
Ok(())
}
#[tokio::test]
async fn test_lua_file_support_process_path_reference() -> Result<()> {
let runtime = Runtime::new_test_runtime_sandbox_01().await?;
let fx_path = "ns_b@pack_b_2/main.aip";
let res = process_path_reference(&runtime, fx_path)?;
assert_contains(res.as_str(), "ns_b/pack_b_2/main.aip");
Ok(())
}
}