use super::foreign::ForeignFormat;
use indicatrix_formats::native::design::{DESIGN_EXTENSION, FileKind, detect_kind, is_design_path};
use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use tracing::warn;
pub(super) const MAX_RECURSE_DEPTH: usize = 32;
const SCHEDULE_EXTENSIONS: [&str; 6] = ["asc", "ASC", "gem", "GEM", "gcs", "GCS"];
#[must_use]
fn schedule_sibling(design_path: &Path) -> Option<PathBuf> {
SCHEDULE_EXTENSIONS
.iter()
.map(|extension| design_path.with_extension(extension))
.find(|candidate| candidate.is_file())
}
#[must_use]
fn is_importable_design(path: &Path) -> bool {
if is_design_path(path) {
return schedule_sibling(path).is_none();
}
path.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("asc"))
|| ForeignFormat::from_path(path).is_some()
}
pub(super) fn collect_design_files_recursive(
dir: &Path,
recurse: bool,
depth: usize,
max_depth: usize,
visited: &mut HashSet<PathBuf>,
out: &mut Vec<PathBuf>,
) {
if depth > max_depth {
warn!(
"Import: not descending into '{}' -- exceeded max recursion depth ({max_depth})",
dir.display()
);
return;
}
let canon = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf());
if !visited.insert(canon) {
warn!(
"Import: skipping '{}' -- already visited (symlink loop?)",
dir.display()
);
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && is_importable_design(&p) {
out.push(p);
} else if recurse && p.is_dir() {
collect_design_files_recursive(&p, recurse, depth + 1, max_depth, visited, out);
}
}
}
pub(super) fn collect_import_candidates(
path: &Path,
recurse: bool,
) -> Result<Vec<PathBuf>, String> {
let mut candidates: Vec<PathBuf> = Vec::new();
if path.is_dir() {
match std::fs::read_dir(path) {
Ok(entries) => {
let mut visited: HashSet<PathBuf> = HashSet::new();
for entry in entries.flatten() {
let p = entry.path();
if p.is_file() && is_importable_design(&p) {
candidates.push(p);
} else if recurse && p.is_dir() {
collect_design_files_recursive(
&p,
recurse,
1,
MAX_RECURSE_DEPTH,
&mut visited,
&mut candidates,
);
}
}
}
Err(e) => return Err(format!("Could not read folder '{}': {e}", path.display())),
}
} else if path.is_file() {
if indicatrix_cut_core::native::asc_path_for_native(path).is_some() {
return Err(format!(
"'{}' is an older Indicatrix sidecar, not a .asc -- open it with \"Open\" \
in the Edit tab instead of Import, or import the .asc beside it.",
path.display()
));
}
if is_design_path(path) {
candidates.push(schedule_sibling(path).unwrap_or_else(|| path.to_path_buf()));
} else {
candidates.push(path.to_path_buf());
}
} else {
return Err(format!("'{}' is not a file or folder.", path.display()));
}
if candidates.is_empty() {
return Err(format!(
"No .asc, .gem, .gcs or .indicatrix files found at '{}'.",
path.display()
));
}
Ok(candidates)
}
fn read_attachment(path: &Path) -> Option<(String, Vec<u8>)> {
let bytes = std::fs::read(path).ok()?;
let name = path.file_name()?.to_string_lossy().into_owned();
Some((name, bytes))
}
pub(super) fn find_design_file_beside(path: &Path) -> Option<(String, Vec<u8>)> {
let candidate = path.with_extension(DESIGN_EXTENSION);
let (name, bytes) = read_attachment(&candidate)?;
(detect_kind(&bytes) == FileKind::Design).then_some((name, bytes))
}
pub(super) fn find_native_sidecar(asc_path: &Path) -> Option<(String, Vec<u8>)> {
if let Some(design) = find_design_file_beside(asc_path) {
return Some(design);
}
let mut current = asc_path.to_path_buf();
current.set_extension(indicatrix_cut_core::native::NATIVE_EXTENSION_SUFFIX);
let mut legacy = asc_path.to_path_buf();
legacy.set_extension(indicatrix_cut_core::native::LEGACY_NATIVE_EXTENSION_SUFFIX);
[current, legacy]
.into_iter()
.find_map(|candidate| read_attachment(&candidate))
}