Skip to main content

wfe_core/models/
artifact.rs

1//! Artifact model for OCI-compatible workflow inputs.
2
3use serde::{Deserialize, Serialize};
4
5/// A reference to an artifact stored in a content-addressed store.
6///
7/// The digest is a content hash (typically `sha256:<hex>`) that uniquely
8/// identifies the artifact blob.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct ArtifactRef {
11    /// Content digest, e.g. `sha256:abc123...`.
12    pub digest: String,
13}
14
15impl ArtifactRef {
16    /// Create a new artifact reference from a digest string.
17    pub fn new(digest: impl Into<String>) -> Self {
18        Self {
19            digest: digest.into(),
20        }
21    }
22
23    /// Extract the algorithm portion (e.g. `sha256`).
24    pub fn algorithm(&self) -> Option<&str> {
25        self.digest.split_once(':').map(|(algo, _)| algo)
26    }
27
28    /// Extract the hex-encoded hash portion.
29    pub fn hash(&self) -> Option<&str> {
30        self.digest.split_once(':').map(|(_, hash)| hash)
31    }
32}
33
34/// An opaque blob stored in an artifact store.
35///
36/// This represents a single OCI image layer: a tar archive (usually
37/// gzip-compressed) containing a filesystem tree.
38#[derive(Debug, Clone)]
39pub struct ArtifactBlob {
40    /// Content digest (`sha256:...`).
41    pub digest: String,
42    /// Size in bytes.
43    pub size: u64,
44    /// OCI media type.
45    pub media_type: String,
46    /// Raw blob bytes.
47    pub data: bytes::Bytes,
48}
49
50/// The well-known media type for a gzip-compressed OCI layer.
51pub const MEDIA_TYPE_OCI_LAYER_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip";
52
53/// The well-known media type for an uncompressed OCI layer.
54pub const MEDIA_TYPE_OCI_LAYER_TAR: &str = "application/vnd.oci.image.layer.v1.tar";
55
56/// Reserved key used inside workflow data to mark an artifact reference.
57pub const ARTIFACT_REF_KEY: &str = "__wfe_artifact";
58
59/// Build an artifact-reference JSON value for insertion into workflow data.
60pub fn artifact_ref_value(digest: &str) -> serde_json::Value {
61    serde_json::json!({ ARTIFACT_REF_KEY: digest })
62}
63
64/// Check whether a JSON value is an artifact reference.
65pub fn is_artifact_ref(value: &serde_json::Value) -> bool {
66    value
67        .as_object()
68        .map(|obj| obj.contains_key(ARTIFACT_REF_KEY))
69        .unwrap_or(false)
70}
71
72/// Extract the digest from an artifact-reference JSON value.
73pub fn parse_artifact_ref(value: &serde_json::Value) -> Option<String> {
74    value
75        .as_object()
76        .and_then(|obj| obj.get(ARTIFACT_REF_KEY))
77        .and_then(|v| v.as_str())
78        .map(|s| s.to_string())
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn artifact_ref_roundtrip() {
87        let r = ArtifactRef::new("sha256:abc123");
88        assert_eq!(r.algorithm(), Some("sha256"));
89        assert_eq!(r.hash(), Some("abc123"));
90    }
91
92    #[test]
93    fn artifact_ref_value_is_artifact_ref() {
94        let v = artifact_ref_value("sha256:def456");
95        assert!(is_artifact_ref(&v));
96        assert_eq!(parse_artifact_ref(&v), Some("sha256:def456".to_string()));
97    }
98
99    #[test]
100    fn plain_string_is_not_artifact_ref() {
101        assert!(!is_artifact_ref(&serde_json::Value::String("hello".into())));
102    }
103}