use super::model::Model;
use super::{FormatCapability, InterchangeError, ModelFormat, Xmi};
pub mod paths {
pub const MANIFEST: &str = "META-INF/manifest.xml";
pub const MODEL_DIR: &str = "model/";
pub const RESOURCES_DIR: &str = "resources/";
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Kpar;
impl ModelFormat for Kpar {
fn name(&self) -> &'static str {
"KPAR"
}
fn extensions(&self) -> &'static [&'static str] {
&["kpar"]
}
fn mime_type(&self) -> &'static str {
"application/kpar"
}
fn capabilities(&self) -> FormatCapability {
FormatCapability::FULL
}
fn read(&self, input: &[u8]) -> Result<Model, InterchangeError> {
#[cfg(feature = "interchange")]
{
KparReader::new().read(input)
}
#[cfg(not(feature = "interchange"))]
{
let _ = input;
Err(InterchangeError::Unsupported(
"KPAR reading requires the 'interchange' feature".to_string(),
))
}
}
fn write(&self, model: &Model) -> Result<Vec<u8>, InterchangeError> {
#[cfg(feature = "interchange")]
{
KparWriter::new().write(model)
}
#[cfg(not(feature = "interchange"))]
{
let _ = model;
Err(InterchangeError::Unsupported(
"KPAR writing requires the 'interchange' feature".to_string(),
))
}
}
fn validate(&self, input: &[u8]) -> Result<(), InterchangeError> {
if input.len() < 4 {
return Err(InterchangeError::archive("File too small"));
}
if &input[0..4] != b"PK\x03\x04" {
return Err(InterchangeError::archive("Not a valid ZIP archive"));
}
Ok(())
}
}
#[cfg(feature = "interchange")]
mod reader {
use super::*;
use std::io::{Cursor, Read};
use zip::ZipArchive;
pub struct KparReader {
xmi: Xmi,
}
impl KparReader {
pub fn new() -> Self {
Self { xmi: Xmi }
}
pub fn read(&self, input: &[u8]) -> Result<Model, InterchangeError> {
let cursor = Cursor::new(input);
let mut archive = ZipArchive::new(cursor)
.map_err(|e| InterchangeError::archive(format!("Failed to open archive: {e}")))?;
let mut combined_model = Model::new();
let xmi_files: Vec<String> = (0..archive.len())
.filter_map(|i| {
let file = archive.by_index(i).ok()?;
let name = file.name().to_string();
if name.starts_with(paths::MODEL_DIR) && name.ends_with(".xmi") {
Some(name)
} else {
None
}
})
.collect();
for xmi_path in xmi_files {
let mut file = archive.by_name(&xmi_path).map_err(|e| {
InterchangeError::archive(format!("Failed to read {xmi_path}: {e}"))
})?;
let mut xmi_content = Vec::new();
file.read_to_end(&mut xmi_content).map_err(|e| {
InterchangeError::archive(format!("Failed to read {xmi_path}: {e}"))
})?;
let model = self.xmi.read(&xmi_content)?;
merge_models(&mut combined_model, model);
}
Ok(combined_model)
}
}
fn merge_models(target: &mut Model, source: Model) {
for (id, element) in source.elements {
if !target.elements.contains_key(&id) {
target.elements.insert(id, element);
}
}
for rel in source.relationships {
target.relationships.push(rel);
}
for root in source.roots {
if !target.roots.contains(&root) {
target.roots.push(root);
}
}
}
}
#[cfg(feature = "interchange")]
use reader::KparReader;
#[cfg(feature = "interchange")]
mod writer {
use super::*;
use std::io::{Cursor, Write};
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
pub struct KparWriter {
xmi: Xmi,
}
impl KparWriter {
pub fn new() -> Self {
Self { xmi: Xmi }
}
pub fn write(&self, model: &Model) -> Result<Vec<u8>, InterchangeError> {
let mut buffer = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(&mut buffer);
let options =
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let manifest = generate_manifest(model);
zip.start_file(paths::MANIFEST, options).map_err(|e| {
InterchangeError::archive(format!("Failed to create manifest: {e}"))
})?;
zip.write_all(manifest.as_bytes())
.map_err(|e| InterchangeError::archive(format!("Failed to write manifest: {e}")))?;
let xmi_content = self.xmi.write(model)?;
zip.start_file(format!("{}main.xmi", paths::MODEL_DIR), options)
.map_err(|e| {
InterchangeError::archive(format!("Failed to create XMI file: {e}"))
})?;
zip.write_all(&xmi_content)
.map_err(|e| InterchangeError::archive(format!("Failed to write XMI: {e}")))?;
zip.finish().map_err(|e| {
InterchangeError::archive(format!("Failed to finalize archive: {e}"))
})?;
Ok(buffer.into_inner())
}
}
fn generate_manifest(model: &Model) -> String {
let name = model.metadata.name.as_deref().unwrap_or("unnamed");
let version = model.metadata.version.as_deref().unwrap_or("1.0.0");
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns="http://www.omg.org/spec/SysML/20230201/kpar">
<package name="{name}" version="{version}">
<model-files>
<file>model/main.xmi</file>
</model-files>
</package>
</manifest>
"#
)
}
}
#[cfg(feature = "interchange")]
use writer::KparWriter;
#[cfg(not(feature = "interchange"))]
struct KparReader;
#[cfg(not(feature = "interchange"))]
impl KparReader {
fn new() -> Self {
Self
}
fn read(&self, _input: &[u8]) -> Result<Model, InterchangeError> {
Err(InterchangeError::Unsupported(
"KPAR reading requires the 'interchange' feature".to_string(),
))
}
}
#[cfg(not(feature = "interchange"))]
struct KparWriter;
#[cfg(not(feature = "interchange"))]
impl KparWriter {
fn new() -> Self {
Self
}
fn write(&self, _model: &Model) -> Result<Vec<u8>, InterchangeError> {
Err(InterchangeError::Unsupported(
"KPAR writing requires the 'interchange' feature".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct KparManifest {
pub name: String,
pub version: Option<String>,
pub description: Option<String>,
pub model_files: Vec<String>,
pub dependencies: Vec<KparDependency>,
}
#[derive(Debug, Clone)]
pub struct KparDependency {
pub name: String,
pub version: Option<String>,
pub uri: Option<String>,
}
impl KparManifest {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
version: None,
description: None,
model_files: Vec::new(),
dependencies: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kpar_format_metadata() {
let kpar = Kpar;
assert_eq!(kpar.name(), "KPAR");
assert_eq!(kpar.extensions(), &["kpar"]);
assert_eq!(kpar.mime_type(), "application/kpar");
assert!(kpar.capabilities().read);
assert!(kpar.capabilities().write);
}
#[test]
fn test_kpar_validate_valid_zip() {
let kpar = Kpar;
let input = b"PK\x03\x04rest of zip...";
assert!(kpar.validate(input).is_ok());
}
#[test]
fn test_kpar_validate_invalid() {
let kpar = Kpar;
let input = b"not a zip file";
assert!(kpar.validate(input).is_err());
}
#[test]
fn test_kpar_manifest_new() {
let manifest = KparManifest::new("TestPackage");
assert_eq!(manifest.name, "TestPackage");
assert!(manifest.version.is_none());
assert!(manifest.model_files.is_empty());
}
#[cfg(feature = "interchange")]
mod interchange_tests {
use super::*;
use crate::interchange::model::{Element, ElementId, ElementKind};
#[test]
fn test_kpar_write_creates_valid_zip() {
let mut model = Model::new();
model.add_element(Element::new("pkg1", ElementKind::Package).with_name("TestPackage"));
let kpar_bytes = Kpar.write(&model).expect("Failed to write KPAR");
assert!(Kpar.validate(&kpar_bytes).is_ok());
assert!(kpar_bytes.starts_with(b"PK\x03\x04"));
}
#[test]
fn test_kpar_roundtrip() {
let mut model = Model::new();
model.metadata.name = Some("RoundtripTest".to_string());
model.metadata.version = Some("1.0.0".to_string());
let pkg = Element::new("pkg1", ElementKind::Package).with_name("Vehicles");
model.add_element(pkg);
let part = Element::new("part1", ElementKind::PartDefinition)
.with_name("Car")
.with_owner("pkg1");
model.add_element(part);
if let Some(pkg) = model.elements.get_mut(&ElementId::new("pkg1")) {
pkg.owned_elements.push(ElementId::new("part1"));
}
let kpar_bytes = Kpar.write(&model).expect("Write failed");
let model2 = Kpar.read(&kpar_bytes).expect("Read failed");
assert_eq!(model2.element_count(), 2);
let pkg2 = model2.get(&ElementId::new("pkg1")).unwrap();
assert_eq!(pkg2.name.as_deref(), Some("Vehicles"));
}
#[test]
fn test_kpar_contains_manifest() {
use std::io::Cursor;
use zip::ZipArchive;
let mut model = Model::new();
model.metadata.name = Some("ManifestTest".to_string());
model.add_element(Element::new("pkg1", ElementKind::Package).with_name("Test"));
let kpar_bytes = Kpar.write(&model).expect("Write failed");
let cursor = Cursor::new(kpar_bytes);
let mut archive = ZipArchive::new(cursor).expect("Failed to open archive");
assert!(
archive.by_name(paths::MANIFEST).is_ok(),
"Manifest not found in KPAR"
);
assert!(
archive.by_name("model/main.xmi").is_ok(),
"XMI file not found in KPAR"
);
}
}
}