use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::crypto::EncryptionAlgorithm;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandSpec {
pub run: Vec<String>,
}
impl CommandSpec {
pub fn new(run: Vec<String>) -> Self {
Self { run }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EmbeddedEnv {
pub target_path: String,
pub nonce: String,
pub payload_hash_sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Manifest {
pub package_id: Uuid,
pub artifact_name: String,
pub artifact_size: u64,
pub created_at: DateTime<Utc>,
pub encryption_algorithm: EncryptionAlgorithm,
pub nonce: String,
pub artifact_hash_sha256: String,
pub payload_hash_sha256: String,
pub version: Option<String>,
pub profile: Option<String>,
pub artifact_kind: Option<String>,
pub archive_format: Option<String>,
pub runtime: Option<String>,
pub framework: Option<String>,
pub app_root: Option<String>,
pub start_command: Option<Vec<String>>,
pub setup_commands: Option<Vec<CommandSpec>>,
pub required_runtimes: Option<Vec<String>>,
pub writable_paths: Option<Vec<String>>,
pub entrypoint: Option<String>,
pub args: Option<Vec<String>>,
pub env: Option<Vec<(String, String)>>,
pub embedded_env: Option<EmbeddedEnv>,
pub signature_algorithm: Option<String>,
pub signing_pubkey_fingerprint: Option<String>,
pub signature_created_at: Option<String>,
pub require_signature: Option<bool>,
}
impl Manifest {
pub fn new(
artifact_name: String,
artifact_size: u64,
encryption_algorithm: EncryptionAlgorithm,
nonce: &[u8],
artifact_hash: &str,
payload_hash: &str,
) -> Self {
Self {
package_id: Uuid::new_v4(),
artifact_name,
artifact_size,
created_at: Utc::now(),
encryption_algorithm,
nonce: hex::encode(nonce),
artifact_hash_sha256: artifact_hash.to_string(),
payload_hash_sha256: payload_hash.to_string(),
version: None,
profile: None,
artifact_kind: None,
archive_format: None,
runtime: None,
framework: None,
app_root: None,
start_command: None,
setup_commands: None,
required_runtimes: None,
writable_paths: None,
entrypoint: None,
args: None,
env: None,
embedded_env: None,
signature_algorithm: None,
signing_pubkey_fingerprint: None,
signature_created_at: None,
require_signature: None,
}
}
pub fn with_version(mut self, version: String) -> Self {
self.version = Some(version);
self
}
pub fn with_profile(mut self, profile: String) -> Self {
self.profile = Some(profile);
self
}
pub fn with_artifact_kind(mut self, artifact_kind: String) -> Self {
self.artifact_kind = Some(artifact_kind);
self
}
pub fn with_archive_format(mut self, archive_format: String) -> Self {
self.archive_format = Some(archive_format);
self
}
pub fn with_runtime(mut self, runtime: String) -> Self {
self.runtime = Some(runtime);
self
}
pub fn with_framework(mut self, framework: String) -> Self {
self.framework = Some(framework);
self
}
pub fn with_app_root(mut self, app_root: String) -> Self {
self.app_root = Some(app_root);
self
}
pub fn with_start_command(mut self, start_command: Vec<String>) -> Self {
self.start_command = Some(start_command);
self
}
pub fn with_setup_commands(mut self, setup_commands: Vec<CommandSpec>) -> Self {
self.setup_commands = Some(setup_commands);
self
}
pub fn with_required_runtimes(mut self, required_runtimes: Vec<String>) -> Self {
self.required_runtimes = Some(required_runtimes);
self
}
pub fn with_writable_paths(mut self, writable_paths: Vec<String>) -> Self {
self.writable_paths = Some(writable_paths);
self
}
pub fn with_entrypoint(mut self, entrypoint: String) -> Self {
self.entrypoint = Some(entrypoint);
self
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args = Some(args);
self
}
pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
self.env = Some(env);
self
}
pub fn with_embedded_env(mut self, embedded_env: EmbeddedEnv) -> Self {
self.embedded_env = Some(embedded_env);
self
}
pub fn with_signature_metadata(
mut self,
algorithm: String,
fingerprint: String,
created_at: String,
) -> Self {
self.signature_algorithm = Some(algorithm);
self.signing_pubkey_fingerprint = Some(fingerprint);
self.signature_created_at = Some(created_at);
self
}
pub fn with_require_signature(mut self, require_signature: bool) -> Self {
self.require_signature = Some(require_signature);
self
}
pub fn to_json(&self) -> crate::error::Result<String> {
serde_json::to_string_pretty(self)
.map_err(|e| crate::error::CrablockError::Serialization(format!("JSON: {e}")))
}
pub fn from_json(json: &str) -> crate::error::Result<Self> {
serde_json::from_str(json)
.map_err(|e| crate::error::CrablockError::Serialization(format!("JSON: {e}")))
}
pub fn to_cbor(&self) -> crate::error::Result<Vec<u8>> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf)
.map_err(|e| crate::error::CrablockError::Serialization(format!("CBOR: {e}")))?;
Ok(buf)
}
pub fn from_cbor(data: &[u8]) -> crate::error::Result<Self> {
ciborium::from_reader(data)
.map_err(|e| crate::error::CrablockError::Serialization(format!("CBOR: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_manifest_serialization() {
let manifest = Manifest::new(
"test_app".to_string(),
1024,
EncryptionAlgorithm::Aes256Gcm,
&[0u8; 12],
"abc123",
"def456",
)
.with_version("1.0.0".to_string())
.with_profile("laravel".to_string())
.with_artifact_kind("directory_archive".to_string())
.with_archive_format("tar.gz".to_string())
.with_runtime("php".to_string())
.with_framework("laravel".to_string())
.with_app_root(".".to_string())
.with_start_command(vec![
"php".to_string(),
"artisan".to_string(),
"serve".to_string(),
])
.with_setup_commands(vec![CommandSpec::new(vec![
"composer".to_string(),
"install".to_string(),
])])
.with_required_runtimes(vec!["php".to_string()])
.with_writable_paths(vec!["storage".to_string(), "bootstrap/cache".to_string()])
.with_entrypoint("/bin/app".to_string())
.with_args(vec!["--port".to_string(), "3000".to_string()])
.with_env(vec![
("ENV".to_string(), "production".to_string()),
("DEBUG".to_string(), "false".to_string()),
])
.with_embedded_env(EmbeddedEnv {
target_path: ".env".to_string(),
nonce: "00112233445566778899aabb".to_string(),
payload_hash_sha256: "feedface".to_string(),
});
let json = manifest.to_json().unwrap();
let deserialized = Manifest::from_json(&json).unwrap();
assert_eq!(manifest.package_id, deserialized.package_id);
assert_eq!(manifest.artifact_name, deserialized.artifact_name);
assert_eq!(manifest.version, deserialized.version);
assert_eq!(manifest.profile, deserialized.profile);
assert_eq!(manifest.artifact_kind, deserialized.artifact_kind);
assert_eq!(manifest.runtime, deserialized.runtime);
assert_eq!(manifest.framework, deserialized.framework);
assert_eq!(manifest.app_root, deserialized.app_root);
assert_eq!(manifest.start_command, deserialized.start_command);
assert_eq!(manifest.setup_commands, deserialized.setup_commands);
assert_eq!(manifest.required_runtimes, deserialized.required_runtimes);
assert_eq!(manifest.writable_paths, deserialized.writable_paths);
assert_eq!(manifest.entrypoint, deserialized.entrypoint);
assert_eq!(manifest.embedded_env, deserialized.embedded_env);
}
#[test]
fn test_manifest_cbor() {
let manifest = Manifest::new(
"test_app".to_string(),
1024,
EncryptionAlgorithm::ChaCha20Poly1305,
&[1u8; 12],
"hash1",
"hash2",
);
let cbor = manifest.to_cbor().unwrap();
let deserialized = Manifest::from_cbor(&cbor).unwrap();
assert_eq!(manifest.package_id, deserialized.package_id);
assert_eq!(
manifest.encryption_algorithm,
deserialized.encryption_algorithm
);
}
}