⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/virt/containers/
image.rs

1//! Container Image Format - layers, overlay composition, manifest, SHA-256 IDs.
2
3#[cfg(feature = "alloc")]
4use alloc::{collections::BTreeMap, string::String, vec::Vec};
5
6use super::simple_sha256;
7
8/// Image layer digest (SHA-256).
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct LayerDigest {
11    pub bytes: [u8; 32],
12}
13
14impl LayerDigest {
15    /// Compute a SHA-256 digest of the given data.
16    pub fn compute(data: &[u8]) -> Self {
17        Self {
18            bytes: simple_sha256(data),
19        }
20    }
21
22    /// Format as hex string.
23    #[cfg(feature = "alloc")]
24    pub fn to_hex(&self) -> String {
25        let mut s = String::with_capacity(64);
26        for b in &self.bytes {
27            let hi = HEX_CHARS[(b >> 4) as usize];
28            let lo = HEX_CHARS[(b & 0x0f) as usize];
29            s.push(hi as char);
30            s.push(lo as char);
31        }
32        s
33    }
34}
35
36const HEX_CHARS: &[u8; 16] = b"0123456789abcdef";
37
38/// A single layer in a container image.
39#[cfg(feature = "alloc")]
40#[derive(Debug, Clone)]
41pub struct ImageLayer {
42    /// SHA-256 digest of the layer content.
43    pub digest: LayerDigest,
44    /// Compressed size in bytes.
45    pub compressed_size: u64,
46    /// Uncompressed size in bytes.
47    pub uncompressed_size: u64,
48    /// Media type (e.g., "application/vnd.oci.image.layer.v1.tar+gzip").
49    pub media_type: String,
50}
51
52/// Gzip detection: check for gzip magic bytes (0x1f, 0x8b).
53pub fn is_gzip(data: &[u8]) -> bool {
54    data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b
55}
56
57/// TAR header: first 100 bytes are the filename, bytes 124-135 are octal size.
58#[cfg(feature = "alloc")]
59pub fn parse_tar_filename(header: &[u8; 512]) -> String {
60    let name_end = header[..100].iter().position(|&b| b == 0).unwrap_or(100);
61    let mut name = String::new();
62    for &b in &header[..name_end] {
63        if b.is_ascii() && b != 0 {
64            name.push(b as char);
65        }
66    }
67    name
68}
69
70/// Parse octal size from TAR header bytes 124..135.
71pub fn parse_tar_size(header: &[u8; 512]) -> u64 {
72    let mut size: u64 = 0;
73    for &b in &header[124..135] {
74        if (b'0'..=b'7').contains(&b) {
75            size = size.saturating_mul(8);
76            size = size.saturating_add((b - b'0') as u64);
77        }
78    }
79    size
80}
81
82/// Container image manifest.
83#[cfg(feature = "alloc")]
84#[derive(Debug, Clone)]
85pub struct ImageManifest {
86    /// Schema version (usually 2).
87    pub schema_version: u32,
88    /// Media type of the manifest.
89    pub media_type: String,
90    /// Config digest (SHA-256 of config JSON).
91    pub config_digest: LayerDigest,
92    /// Config size in bytes.
93    pub config_size: u64,
94    /// Ordered list of layer digests.
95    pub layer_digests: Vec<LayerDigest>,
96}
97
98/// Container image: manifest + layers + config.
99#[cfg(feature = "alloc")]
100#[derive(Debug, Clone)]
101pub struct ContainerImage {
102    /// Image ID (SHA-256 of the config blob).
103    pub image_id: LayerDigest,
104    /// Human-readable name (e.g., "alpine:3.19").
105    pub name: String,
106    /// Image manifest.
107    pub manifest: ImageManifest,
108    /// Layers in order (bottom to top).
109    pub layers: Vec<ImageLayer>,
110}
111
112/// Layer cache: stores extracted layers by their digest.
113#[cfg(feature = "alloc")]
114pub struct LayerCache {
115    /// Maps digest hex -> layer entry.
116    entries: BTreeMap<String, CachedLayer>,
117    /// Maximum number of cached layers.
118    max_entries: usize,
119}
120
121#[cfg(feature = "alloc")]
122#[derive(Debug, Clone)]
123pub struct CachedLayer {
124    pub digest: LayerDigest,
125    pub extracted_path: String,
126    pub size_bytes: u64,
127    pub reference_count: u32,
128}
129
130#[cfg(feature = "alloc")]
131impl LayerCache {
132    pub fn new(max_entries: usize) -> Self {
133        Self {
134            entries: BTreeMap::new(),
135            max_entries,
136        }
137    }
138
139    /// Get a cached layer by digest hex.
140    pub fn get(&self, digest_hex: &str) -> Option<&CachedLayer> {
141        self.entries.get(digest_hex)
142    }
143
144    /// Insert a layer into the cache. Returns false if cache is full.
145    pub fn insert(&mut self, layer: CachedLayer) -> bool {
146        if self.entries.len() >= self.max_entries {
147            return false;
148        }
149        let hex = layer.digest.to_hex();
150        self.entries.insert(hex, layer);
151        true
152    }
153
154    /// Increment reference count for a layer.
155    pub fn add_ref(&mut self, digest_hex: &str) -> bool {
156        if let Some(entry) = self.entries.get_mut(digest_hex) {
157            entry.reference_count = entry.reference_count.saturating_add(1);
158            true
159        } else {
160            false
161        }
162    }
163
164    /// Decrement reference count. Removes the entry if it reaches zero.
165    pub fn release(&mut self, digest_hex: &str) -> bool {
166        let should_remove = if let Some(entry) = self.entries.get_mut(digest_hex) {
167            entry.reference_count = entry.reference_count.saturating_sub(1);
168            entry.reference_count == 0
169        } else {
170            return false;
171        };
172        if should_remove {
173            self.entries.remove(digest_hex);
174        }
175        true
176    }
177
178    pub fn entry_count(&self) -> usize {
179        self.entries.len()
180    }
181
182    pub fn is_full(&self) -> bool {
183        self.entries.len() >= self.max_entries
184    }
185}
186
187#[cfg(feature = "alloc")]
188impl ContainerImage {
189    /// Compose an image from config data and a list of layer data blobs.
190    pub fn compose(name: &str, config_data: &[u8], layer_data: &[&[u8]]) -> Self {
191        let config_digest = LayerDigest::compute(config_data);
192        let image_id = config_digest.clone();
193
194        let mut layers = Vec::new();
195        let mut layer_digests = Vec::new();
196        for data in layer_data {
197            let digest = LayerDigest::compute(data);
198            let compressed = is_gzip(data);
199            layers.push(ImageLayer {
200                digest: digest.clone(),
201                compressed_size: if compressed { data.len() as u64 } else { 0 },
202                uncompressed_size: data.len() as u64,
203                media_type: if compressed {
204                    String::from("application/vnd.oci.image.layer.v1.tar+gzip")
205                } else {
206                    String::from("application/vnd.oci.image.layer.v1.tar")
207                },
208            });
209            layer_digests.push(digest);
210        }
211
212        let manifest = ImageManifest {
213            schema_version: 2,
214            media_type: String::from("application/vnd.oci.image.manifest.v1+json"),
215            config_digest,
216            config_size: config_data.len() as u64,
217            layer_digests,
218        };
219
220        Self {
221            image_id,
222            name: String::from(name),
223            manifest,
224            layers,
225        }
226    }
227}