veridian_kernel/virt/containers/
image.rs1#[cfg(feature = "alloc")]
4use alloc::{collections::BTreeMap, string::String, vec::Vec};
5
6use super::simple_sha256;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct LayerDigest {
11 pub bytes: [u8; 32],
12}
13
14impl LayerDigest {
15 pub fn compute(data: &[u8]) -> Self {
17 Self {
18 bytes: simple_sha256(data),
19 }
20 }
21
22 #[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#[cfg(feature = "alloc")]
40#[derive(Debug, Clone)]
41pub struct ImageLayer {
42 pub digest: LayerDigest,
44 pub compressed_size: u64,
46 pub uncompressed_size: u64,
48 pub media_type: String,
50}
51
52pub fn is_gzip(data: &[u8]) -> bool {
54 data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b
55}
56
57#[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
70pub 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#[cfg(feature = "alloc")]
84#[derive(Debug, Clone)]
85pub struct ImageManifest {
86 pub schema_version: u32,
88 pub media_type: String,
90 pub config_digest: LayerDigest,
92 pub config_size: u64,
94 pub layer_digests: Vec<LayerDigest>,
96}
97
98#[cfg(feature = "alloc")]
100#[derive(Debug, Clone)]
101pub struct ContainerImage {
102 pub image_id: LayerDigest,
104 pub name: String,
106 pub manifest: ImageManifest,
108 pub layers: Vec<ImageLayer>,
110}
111
112#[cfg(feature = "alloc")]
114pub struct LayerCache {
115 entries: BTreeMap<String, CachedLayer>,
117 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 pub fn get(&self, digest_hex: &str) -> Option<&CachedLayer> {
141 self.entries.get(digest_hex)
142 }
143
144 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 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 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 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}