oci_wasm/
config.rs

1use std::collections::BTreeMap;
2
3use anyhow::Context;
4use chrono::{DateTime, Utc};
5use oci_client::client::{Config, ImageLayer};
6use serde::{Deserialize, Serialize};
7use sha2::Digest;
8
9use crate::{
10    Component, COMPONENT_OS, MODULE_OS, WASM_ARCHITECTURE, WASM_LAYER_MEDIA_TYPE,
11    WASM_MANIFEST_CONFIG_MEDIA_TYPE,
12};
13
14// A convenience trait that indicates a type can be converted into an OCI manifest config
15pub trait ToConfig {
16    /// Convert the type into an OCI manifest config
17    fn to_config(&self) -> anyhow::Result<Config>;
18}
19
20/// The config type struct for `application/wasm`
21#[derive(Serialize, Deserialize, Debug)]
22#[serde(rename_all = "camelCase")]
23pub struct WasmConfig {
24    /// The time when the config was created.
25    pub created: DateTime<Utc>,
26    /// The optional name of the author of the config.
27    pub author: Option<String>,
28    /// The architecture of the artifact. This is always `wasm`.
29    pub architecture: String,
30    /// The OS name of the artifact. Possible options: wasip1, wasip2. For plain wasm, this should
31    /// be wasip1 as this must match a GOOS value and it doesn’t have one for plain Wasm
32    ///
33    /// Eventually this will go away when we hit a 1.0 but we need it for now
34    pub os: String,
35    /// This field contains a list of digests of each of the layers from the manifest in the same
36    /// order as they are listed in the manfiest. This exists because we need to have a unique list
37    /// here so that the hash of the config (used as the ID) is unique every time
38    /// (https://github.com/opencontainers/image-spec/pull/1173)
39    pub layer_digests: Vec<String>,
40    /// Information about the component in the manifest. This is required when the `os` field is
41    /// `wasip2`
42    pub component: Option<Component>,
43}
44
45pub struct AnnotatedWasmConfig<'a> {
46    pub config: &'a WasmConfig,
47    pub annotations: BTreeMap<String, String>,
48}
49
50impl WasmConfig {
51    /// A helper for loading a component from a file and returning the proper config and
52    /// [`ImageLayer`]. The returned config will have the created time set to now and all other
53    /// fields set for a component.
54    pub async fn from_component(
55        path: impl AsRef<std::path::Path>,
56        author: Option<String>,
57    ) -> anyhow::Result<(Self, ImageLayer)> {
58        let raw = tokio::fs::read(path).await.context("Unable to read file")?;
59        Self::from_raw_component(raw, author)
60    }
61
62    /// Same as [`WasmConfig::from_component`] but for raw component bytes
63    pub fn from_raw_component(
64        raw: Vec<u8>,
65        author: Option<String>,
66    ) -> anyhow::Result<(Self, ImageLayer)> {
67        let component = Component::from_raw_component(&raw)?;
68        let config = Self {
69            created: Utc::now(),
70            author,
71            architecture: WASM_ARCHITECTURE.to_string(),
72            os: COMPONENT_OS.to_string(),
73            layer_digests: vec![sha256_digest(&raw)],
74            component: Some(component),
75        };
76        Ok((
77            config,
78            ImageLayer {
79                data: raw,
80                media_type: WASM_LAYER_MEDIA_TYPE.to_string(),
81                annotations: None,
82            },
83        ))
84    }
85
86    /// A helper for loading a plain wasm module and returning the proper config and [`ImageLayer`].
87    /// The returned config will have the created time set to now and all other fields set for a
88    /// plain wasm module.
89    pub async fn from_module(
90        path: impl AsRef<std::path::Path>,
91        author: Option<String>,
92    ) -> anyhow::Result<(Self, ImageLayer)> {
93        let raw = tokio::fs::read(path).await.context("Unable to read file")?;
94        Self::from_raw_module(raw, author)
95    }
96
97    /// Same as [`WasmConfig::from_module`] but for raw module bytes
98    pub fn from_raw_module(
99        raw: Vec<u8>,
100        author: Option<String>,
101    ) -> anyhow::Result<(Self, ImageLayer)> {
102        let config = Self {
103            created: Utc::now(),
104            author,
105            architecture: WASM_ARCHITECTURE.to_string(),
106            os: MODULE_OS.to_string(),
107            layer_digests: vec![sha256_digest(&raw)],
108            component: None,
109        };
110        Ok((
111            config,
112            ImageLayer {
113                data: raw,
114                media_type: WASM_LAYER_MEDIA_TYPE.to_string(),
115                annotations: None,
116            },
117        ))
118    }
119
120    /// Adds annotations to this [`WasmConfig`].
121    #[must_use]
122    pub fn with_annotations(
123        &'_ self,
124        annotations: BTreeMap<String, String>,
125    ) -> AnnotatedWasmConfig<'_> {
126        AnnotatedWasmConfig {
127            config: self,
128            annotations,
129        }
130    }
131}
132
133impl ToConfig for AnnotatedWasmConfig<'_> {
134    /// Generate a [`Config`] for this [`WasmConfig`]
135    fn to_config(&self) -> anyhow::Result<Config> {
136        let mut config = self.config.to_config()?;
137        config.annotations = Some(self.annotations.clone());
138        Ok(config)
139    }
140}
141
142impl ToConfig for WasmConfig {
143    /// Generate a [`Config`] for this [`WasmConfig`]
144    fn to_config(&self) -> anyhow::Result<Config> {
145        serde_json::to_vec(self)
146            .map(|data| Config {
147                data,
148                media_type: WASM_MANIFEST_CONFIG_MEDIA_TYPE.to_string(),
149                annotations: None,
150            })
151            .map_err(Into::into)
152    }
153}
154
155// NOTE: There are a bunch of implementations here because we can't do a generic implementation
156// across T for AsRef<[u8]>
157
158impl TryFrom<String> for WasmConfig {
159    type Error = anyhow::Error;
160
161    fn try_from(value: String) -> Result<Self, Self::Error> {
162        serde_json::from_str(&value).map_err(Into::into)
163    }
164}
165
166impl TryFrom<Vec<u8>> for WasmConfig {
167    type Error = anyhow::Error;
168
169    fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
170        serde_json::from_slice(&value).map_err(Into::into)
171    }
172}
173
174impl TryFrom<&str> for WasmConfig {
175    type Error = anyhow::Error;
176
177    fn try_from(value: &str) -> Result<Self, Self::Error> {
178        serde_json::from_str(value).map_err(Into::into)
179    }
180}
181
182impl TryFrom<&[u8]> for WasmConfig {
183    type Error = anyhow::Error;
184
185    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
186        serde_json::from_slice(value).map_err(Into::into)
187    }
188}
189
190fn sha256_digest(bytes: &[u8]) -> String {
191    format!("sha256:{:x}", sha2::Sha256::digest(bytes))
192}