use crate::hashing::normalize_line_endings;
use crate::manifest::ManifestPackage;
use crate::package_spec::PackageSpec;
use crate::{PrayError, PrayResult};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
pub(crate) fn select_exports(
declaration: &ManifestPackage,
spec: &PackageSpec,
) -> PrayResult<Vec<String>> {
if !declaration.exports.is_empty() {
for export in &declaration.exports {
if !spec.exports.contains_key(export) {
return Err(PrayError::Resolution(format!(
"package {} does not export {}",
declaration.name, export
)));
}
}
return Ok(declaration.exports.clone());
}
if declaration.roles.is_empty() && declaration.file.is_none() {
return Ok(spec.exports.keys().cloned().collect());
}
let mut roles = declaration.roles.clone();
if declaration.file.is_some() && !roles.contains(&crate::manifest::ExportRole::File) {
roles.push(crate::manifest::ExportRole::File);
}
let mut selected = Vec::new();
for role in roles {
let compatible: Vec<String> = spec
.exports
.iter()
.filter(|(_, export)| crate::destination::export_kind_matches_role(&export.kind, role))
.map(|(name, _)| name.clone())
.collect();
match compatible.as_slice() {
[name] => {
if !selected.contains(name) {
selected.push(name.clone());
}
}
[] => {
return Err(PrayError::Resolution(format!(
"package {} has no export compatible with {:?}",
declaration.name, role
)));
}
_ => {
return Err(PrayError::Resolution(format!(
"package {} has multiple exports compatible with {:?}; set export: \"name\"",
declaration.name, role
)));
}
}
}
Ok(selected)
}
pub(crate) fn read_text(path: &Path) -> PrayResult<String> {
let text = fs::read_to_string(path)?;
Ok(normalize_line_endings(&text))
}
pub(crate) fn load_package_file_bytes(
root: &Path,
spec: &PackageSpec,
) -> PrayResult<BTreeMap<String, Vec<u8>>> {
let mut file_bytes = BTreeMap::new();
for file in &spec.files {
let path = root.join(file);
if !path.exists() {
return Err(PrayError::Integrity(format!(
"package file missing: {}",
file
)));
}
if path.is_dir() {
return Err(PrayError::Integrity(format!(
"package file is a directory: {}",
file
)));
}
file_bytes.insert(file.clone(), fs::read(&path)?);
}
Ok(file_bytes)
}
pub(crate) fn load_export_bodies(
file_bytes: &BTreeMap<String, Vec<u8>>,
spec: &PackageSpec,
selected_exports: &[String],
) -> PrayResult<BTreeMap<String, String>> {
let mut export_bodies = BTreeMap::new();
for export_name in selected_exports {
let entry = spec.exports.get(export_name).ok_or_else(|| {
PrayError::Resolution(format!(
"package {} is missing export {}",
spec.name, export_name
))
})?;
if entry.kind != "fragment" {
continue;
}
let bytes = file_bytes.get(&entry.path).ok_or_else(|| {
PrayError::Integrity(format!(
"package file missing for export {}: {}",
export_name, entry.path
))
})?;
let text = std::str::from_utf8(bytes).map_err(|error| {
PrayError::Integrity(format!(
"package file is not valid utf-8 for export {}: {}",
export_name, error
))
})?;
export_bodies.insert(export_name.clone(), normalize_line_endings(text));
}
Ok(export_bodies)
}
pub(crate) fn build_skill_file_index(spec: &PackageSpec) -> BTreeMap<String, Vec<String>> {
let mut index = BTreeMap::new();
for (export_name, export) in &spec.exports {
if !matches!(export.kind.as_str(), "folder" | "skill") {
continue;
}
let folder_prefix = export.path.trim_end_matches('/');
let files = indexed_files_under_prefix(&spec.files, folder_prefix);
if !files.is_empty() {
index.insert(export_name.clone(), files);
}
}
for (skill_name, skill) in &spec.skills {
if index.contains_key(skill_name) {
continue;
}
let skill_prefix = skill.path.trim_end_matches('/');
let files = indexed_files_under_prefix(&spec.files, skill_prefix);
if !files.is_empty() {
index.insert(skill_name.clone(), files);
}
}
index
}
pub(crate) fn indexed_files_under_prefix(files: &[String], prefix: &str) -> Vec<String> {
let mut indexed = Vec::new();
for file in files {
if let Some(relative) = skill_relative_file(file, prefix) {
indexed.push(relative);
}
}
indexed
}
pub(crate) fn skill_relative_file(file: &str, skill_prefix: &str) -> Option<String> {
let relative = file.strip_prefix(skill_prefix)?.trim_start_matches('/');
if relative.is_empty() || file == skill_prefix {
None
} else {
Some(relative.to_string())
}
}