use std::collections::BTreeSet;
pub const CANONICAL_MANIFEST_ENTRY: &str = "manifest.cbor";
pub const DW_MANIFEST_ENTRY: &str = "manifest.json";
pub const DW_METADATA_ENTRY: &str = "metadata.json";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PackArchiveShape {
Canonical,
DwAnswerDoc,
Unrecognised,
}
impl PackArchiveShape {
pub fn as_slug(&self) -> &'static str {
match self {
Self::Canonical => "canonical",
Self::DwAnswerDoc => "dw-application",
Self::Unrecognised => "unrecognised",
}
}
pub fn describe(&self) -> &'static str {
match self {
Self::Canonical => "canonical (manifest.cbor)",
Self::DwAnswerDoc => {
"DW application pack (manifest.json + metadata.json, written by greentic-designer)"
}
Self::Unrecognised => "unrecognised",
}
}
}
pub fn detect_archive_shape(entry_names: &BTreeSet<String>) -> PackArchiveShape {
if entry_names.contains(CANONICAL_MANIFEST_ENTRY) {
return PackArchiveShape::Canonical;
}
if entry_names.contains(DW_MANIFEST_ENTRY) {
return PackArchiveShape::DwAnswerDoc;
}
PackArchiveShape::Unrecognised
}
pub fn archive_shape_is_ambiguous(entry_names: &BTreeSet<String>) -> bool {
entry_names.contains(CANONICAL_MANIFEST_ENTRY) && entry_names.contains(DW_MANIFEST_ENTRY)
}
pub fn non_canonical_archive_message(entry_names: &BTreeSet<String>) -> String {
match detect_archive_shape(entry_names) {
PackArchiveShape::DwAnswerDoc => format!(
"this archive is a DW application pack (it carries `{DW_MANIFEST_ENTRY}`, not \
`{CANONICAL_MANIFEST_ENTRY}`); this command requires a canonical pack — run \
`greentic-pack doctor <pack>` to inspect it"
),
PackArchiveShape::Canonical | PackArchiveShape::Unrecognised => format!(
"`{CANONICAL_MANIFEST_ENTRY}` missing from archive, and no other known pack shape \
matched; run `greentic-pack doctor <pack>` for details"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entries(names: &[&str]) -> BTreeSet<String> {
names.iter().map(|name| (*name).to_string()).collect()
}
#[test]
fn manifest_cbor_yields_canonical() {
let names = entries(&["manifest.cbor", "sbom.cbor", "flows/main.ygtc"]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Canonical);
assert!(!archive_shape_is_ambiguous(&names));
}
#[test]
fn manifest_json_yields_dw_answer_doc() {
let names = entries(&["manifest.json", "metadata.json"]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::DwAnswerDoc);
assert!(!archive_shape_is_ambiguous(&names));
}
#[test]
fn both_manifests_yield_canonical_and_report_ambiguous() {
let names = entries(&["manifest.cbor", "manifest.json", "metadata.json"]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Canonical);
assert!(archive_shape_is_ambiguous(&names));
}
#[test]
fn neither_manifest_yields_unrecognised() {
let names = entries(&["metadata.json", "flows/main.ygtc", "assets/knowledge/a.txt"]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Unrecognised);
}
#[test]
fn nested_i18n_manifest_json_does_not_match() {
let names = entries(&["assets/i18n/_manifest.json", "assets/i18n/en.json"]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Unrecognised);
}
#[test]
fn nested_component_manifest_json_does_not_match() {
let names = entries(&[
"components/foo/component.manifest.json",
"components/foo.manifest.json",
]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Unrecognised);
}
#[test]
fn empty_archive_yields_unrecognised() {
let names = entries(&[]);
assert_eq!(detect_archive_shape(&names), PackArchiveShape::Unrecognised);
assert!(!archive_shape_is_ambiguous(&names));
}
#[test]
fn slugs_are_stable() {
assert_eq!(PackArchiveShape::Canonical.as_slug(), "canonical");
assert_eq!(PackArchiveShape::DwAnswerDoc.as_slug(), "dw-application");
assert_eq!(PackArchiveShape::Unrecognised.as_slug(), "unrecognised");
}
}