Coverage Report

Created: 2026-02-13 21:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/lyquor/lyquor/oci/src/oci.rs
Line
Count
Source
1
use crate::registry::PulledContent;
2
3
use super::{
4
    pack::*,
5
    registry::{Error, Registry, RegistryOptions},
6
};
7
use async_trait::async_trait;
8
use bytes::Bytes;
9
use docker_credential::{self, DockerCredential};
10
pub use oci_client::Reference as OCIReference;
11
pub use oci_client::secrets::RegistryAuth as OCIRegistryAuth;
12
use oci_client::{
13
    Client, Reference,
14
    client::{ClientConfig, ClientProtocol, Config, ImageLayer},
15
    manifest,
16
    secrets::RegistryAuth,
17
};
18
use std::{collections::BTreeMap, net::IpAddr};
19
20
const LYQUID_PACK_ASSET_NAME_KEY: &str = "assetName";
21
const LYQUID_PACK_ASSET_TYPE_KEY: &str = "assetType";
22
pub const LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY: &str = "solidity";
23
pub const LYQUID_PACK_ASSET_TYPE_VALUE_LYQUID: &str = "lyquid";
24
pub const LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS: &str = "assets";
25
26
#[derive(Clone)]
27
pub struct OCIRegistry {
28
    auth: RegistryAuth,
29
    repository_address: String,
30
    client: Client,
31
}
32
33
impl OCIRegistry {
34
0
    pub fn new(auth: OCIRegistryAuth, repository_address: &str) -> Self {
35
0
        let config = ClientConfig {
36
0
            protocol: match Self::check_localhost(repository_address) {
37
0
                true => ClientProtocol::Http,
38
0
                _ => ClientProtocol::Https,
39
            },
40
0
            ..Default::default()
41
        };
42
43
0
        OCIRegistry {
44
0
            auth,
45
0
            repository_address: repository_address.to_owned(),
46
0
            client: Client::new(config),
47
0
        }
48
0
    }
49
50
1
    pub fn new_with_config(auth: OCIRegistryAuth, repository_address: &str, config: ClientConfig) -> Self {
51
1
        OCIRegistry {
52
1
            auth,
53
1
            repository_address: repository_address.to_owned(),
54
1
            client: Client::new(config),
55
1
        }
56
1
    }
57
58
0
    fn check_localhost(addr: &str) -> bool {
59
        // Get host+port body
60
0
        let host = addr.split('/').next().unwrap_or(addr);
61
62
        // Remove port get actual host
63
0
        let host = if host.starts_with('[') {
64
0
            host.trim_start_matches('[').split(']').next().unwrap_or(host)
65
        } else {
66
0
            host.split(':').next().unwrap_or(host)
67
        };
68
        // Check hostname
69
0
        if host.eq_ignore_ascii_case("localhost") {
70
0
            return true;
71
0
        }
72
73
        // Check IP
74
0
        if let Ok(ip) = host.parse::<IpAddr>() {
75
0
            return ip.is_loopback();
76
0
        }
77
78
0
        false
79
0
    }
80
81
    #[inline]
82
6
    fn build_reference(
83
6
        &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>,
84
6
    ) -> Result<Reference, Error> {
85
6
        let mut address = format!("{}/{}", self.repository_address, name);
86
87
6
        if let Some(
t1
) = tag {
88
1
            address.push(':');
89
1
            address.push_str(t);
90
5
        }
91
6
        if let Some(
d1
) = digest {
92
1
            address.push('@');
93
1
            address.push_str(&d.to_oci_digest());
94
5
        }
95
96
6
        Reference::try_from(address).map_err(|_| Error::InvalidRegistryAddress)
97
6
    }
98
99
0
    pub fn get_docker_credential(repository_address: &str) -> Result<OCIRegistryAuth, Error> {
100
0
        match docker_credential::get_credential(repository_address) {
101
0
            Ok(DockerCredential::UsernamePassword(user, passwd)) => Ok(OCIRegistryAuth::Basic(user, passwd)),
102
0
            Ok(DockerCredential::IdentityToken(token)) => Ok(OCIRegistryAuth::Bearer(token)),
103
0
            Err(e) => Err(Error::DockerCredentialNotFound(e)),
104
        }
105
0
    }
106
107
0
    pub async fn resolve_digest(&self, name: &str, tag: Option<&str>) -> Result<LyquidPackDigest, Error> {
108
0
        let reference = self.build_reference(name, tag, None)?;
109
0
        let digest = self
110
0
            .client
111
0
            .fetch_manifest_digest(&reference, &self.auth)
112
0
            .await
113
0
            .map_err(|e| Error::OciDistributionError(e.to_string()))?;
114
0
        LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest)
115
0
    }
116
117
    #[inline]
118
1
    async fn push_pack(&self, pack: LyquidPack, _options: Option<RegistryOptions>) -> Result<LyquidPackDigest, Error> {
119
1
        let reference =
120
1
            self.build_reference(&pack.metadata.name, Some(pack.metadata.tag.to_string().as_str()), None)
?0
;
121
122
        // Add wasm
123
1
        let layer_lyquid = ImageLayer {
124
1
            data: pack.wasm,
125
1
            media_type: manifest::WASM_LAYER_MEDIA_TYPE.to_owned(),
126
1
            annotations: Some(BTreeMap::from([(
127
1
                LYQUID_PACK_ASSET_TYPE_KEY.to_owned(),
128
1
                LYQUID_PACK_ASSET_TYPE_VALUE_LYQUID.to_owned(),
129
1
            )])),
130
1
        };
131
1
        let mut layers = vec![layer_lyquid];
132
133
        // Add solidity
134
1
        let layer_solidity = ImageLayer {
135
1
            data: pack.solidity,
136
1
            media_type: manifest::IMAGE_LAYER_MEDIA_TYPE.to_owned(),
137
1
            annotations: Some(BTreeMap::from([(
138
1
                LYQUID_PACK_ASSET_TYPE_KEY.to_owned(),
139
1
                LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY.to_owned(),
140
1
            )])),
141
1
        };
142
1
        layers.push(layer_solidity);
143
144
        // Add assets
145
2
        for (key, val) in 
pack.assets1
.
iter1
().
flat_map1
(|a|
a1
.
iter1
()) {
146
2
            let asset_layer = ImageLayer {
147
2
                data: val.clone(),
148
2
                media_type: manifest::IMAGE_LAYER_MEDIA_TYPE.to_owned(),
149
2
                annotations: Some(BTreeMap::from([
150
2
                    (
151
2
                        LYQUID_PACK_ASSET_TYPE_KEY.to_owned(),
152
2
                        LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS.to_owned(),
153
2
                    ),
154
2
                    (LYQUID_PACK_ASSET_NAME_KEY.to_owned(), key.to_owned()),
155
2
                ])),
156
2
            };
157
2
            layers.push(asset_layer);
158
2
        }
159
160
        // Add config
161
1
        let config_data = pack.metadata.to_json().map_err(|_| Error::BadMetadata)
?0
;
162
163
1
        let config = Config {
164
1
            data: config_data.into(),
165
1
            media_type: manifest::WASM_CONFIG_MEDIA_TYPE.to_owned(),
166
1
            annotations: None,
167
1
        };
168
169
1
        let mut manifest = manifest::OciImageManifest::build(&layers, &config, None);
170
1
        manifest.media_type = Some(manifest::OCI_IMAGE_MEDIA_TYPE.to_owned());
171
172
1
        let _ = self
173
1
            .client
174
1
            .push(&reference, &layers, config, &self.auth, Some(manifest))
175
1
            .await
176
1
            .map_err(|e| Error::PushError(
e0
.
to_string0
()))
?0
;
177
178
        // the only way the get the image digest in oci_client is to fetch it from the remote.
179
1
        let digest = self
180
1
            .client
181
1
            .fetch_manifest_digest(&reference, &self.auth)
182
1
            .await
183
1
            .map_err(|e| Error::OciDistributionError(
e0
.
to_string0
()))
?0
;
184
1
        LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest)
185
1
    }
186
187
    #[inline]
188
2
    async fn pull_pack(
189
2
        &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, _options: Option<RegistryOptions>,
190
2
    ) -> Result<super::pack::LyquidPack, Error> {
191
2
        let reference = self.build_reference(name, tag, digest)
?0
;
192
2
        let accepted_media_types = vec![
193
            manifest::IMAGE_MANIFEST_MEDIA_TYPE,
194
2
            manifest::OCI_IMAGE_MEDIA_TYPE,
195
2
            manifest::WASM_LAYER_MEDIA_TYPE,
196
2
            manifest::IMAGE_LAYER_MEDIA_TYPE,
197
        ];
198
2
        let image = self
199
2
            .client
200
2
            .pull(&reference, &self.auth, accepted_media_types)
201
2
            .await
202
2
            .map_err(|e| Error::PullError(
e0
.
to_string0
()))
?0
;
203
204
2
        let mut wasm = Bytes::new();
205
2
        let mut solidity = Bytes::new();
206
2
        let mut assets = BTreeMap::new();
207
8
        for layer in 
image.layers2
.
into_iter2
() {
208
            // Find wasm
209
8
            if layer.media_type == manifest::WASM_LAYER_MEDIA_TYPE {
210
2
                wasm = Bytes::from(layer.data);
211
2
                continue;
212
6
            }
213
214
            // Other data should be in image layer
215
6
            if layer.media_type != manifest::IMAGE_LAYER_MEDIA_TYPE {
216
0
                continue;
217
6
            }
218
219
6
            let Some(annotations) = layer.annotations else {
220
0
                continue;
221
            };
222
223
            // Find solidity / assets
224
6
            match annotations.get(LYQUID_PACK_ASSET_TYPE_KEY) {
225
6
                Some(v) => match v.as_str() {
226
6
                    LYQUID_PACK_ASSET_TYPE_VALUE_SOLIDITY => {
227
2
                        solidity = Bytes::from(layer.data);
228
2
                    }
229
4
                    LYQUID_PACK_ASSET_TYPE_VALUE_ASSETS => {
230
4
                        match annotations.get(LYQUID_PACK_ASSET_NAME_KEY) {
231
4
                            Some(an) => {
232
4
                                assets.insert(an.to_owned(), Bytes::from(layer.data));
233
4
                            }
234
0
                            _ => continue,
235
                        };
236
                    }
237
0
                    _ => continue,
238
                },
239
0
                _ => continue,
240
            }
241
        }
242
243
2
        if wasm.is_empty() {
244
0
            return Err(Error::BadImage("Missing wasm binary.".into()));
245
2
        }
246
247
2
        let assets = if assets.is_empty() { 
None0
} else { Some(assets) };
248
249
        // metadata
250
2
        let metadata = LyquidPackMetadata::from_json(image.config.data.to_vec()).map_err(|_| Error::BadMetadata)
?0
;
251
252
        // digest
253
2
        let digest = image.digest.ok_or(Error::BadDigest)
?0
;
254
2
        let digest = LyquidPackDigest::from_oci_digest(&digest).map_err(|_| Error::BadDigest)
?0
;
255
256
2
        Ok(LyquidPack {
257
2
            wasm,
258
2
            solidity,
259
2
            assets,
260
2
            metadata,
261
2
            digest: Some(digest),
262
2
        })
263
2
    }
264
265
    #[inline]
266
2
    async fn pull_layer(
267
2
        &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, layer_type: &str,
268
2
        layer_name: Option<&str>, _options: Option<RegistryOptions>,
269
2
    ) -> Result<Bytes, Error> {
270
2
        let mut layer_bytes = None;
271
2
        let reference = self.build_reference(name, tag, digest)
?0
;
272
273
        // Get manifest
274
2
        let (manifest, _) = self
275
2
            .client
276
2
            .pull_manifest(&reference, &self.auth)
277
2
            .await
278
2
            .map_err(|e| Error::PullError(
e0
.
to_string0
()))
?0
;
279
2
        let layers = match manifest {
280
2
            manifest::OciManifest::Image(oim) => oim.layers,
281
0
            manifest::OciManifest::ImageIndex(oii) => {
282
0
                let mut found = None;
283
                // This part of code is for future-proof.
284
                // Currently we use os to identify major lyquor versions.
285
                // Refer: https://docs.rs/oci-client/latest/oci_client/manifest/struct.Platform.html
286
0
                for manifest in oii.manifests {
287
0
                    if let Some(platform) = manifest.platform {
288
                        // Found the platform we want
289
0
                        if platform.architecture.to_string() == LYQUID_PACK_METADATA_ARCHITECTURE_VALUE &&
290
0
                            platform.os == oci_client::config::Os::Other(LYQUID_PACK_METADATA_OS_DEFAULT.to_owned()) &&
291
0
                            check_lyquid_os_version_compat(
292
0
                                LYQUID_PACK_METADATA_OS_VERSION_DEFAULT,
293
0
                                &platform.os_version.unwrap_or_default(),
294
                            )
295
                        {
296
0
                            let reference = reference.clone_with_digest(manifest.digest);
297
                            // Pull manifest for this platform
298
0
                            let (manifest, _) = self
299
0
                                .client
300
0
                                .pull_manifest(&reference, &self.auth)
301
0
                                .await
302
0
                                .map_err(|e| Error::PullError(e.to_string()))?;
303
0
                            match manifest {
304
0
                                manifest::OciManifest::Image(oim) => found = Some(oim),
305
                                manifest::OciManifest::ImageIndex(_) => {
306
0
                                    return Err(Error::PullError("Unexpected nested index.".to_owned()));
307
                                }
308
                            };
309
0
                            break;
310
0
                        }
311
                    } else {
312
                        // A multi-arch manifest must have a platform field
313
0
                        continue;
314
                    }
315
                }
316
317
0
                match found {
318
0
                    Some(manifest) => manifest.layers,
319
0
                    None => return Err(Error::PullError("Layer not found. No matching platform.".to_owned())),
320
                }
321
            }
322
        };
323
324
        // Find layer digest and pull it
325
5
        for od in 
layers2
{
326
5
            if let Some(ref annotation) = od.annotations {
327
5
                let Some(l_type) = annotation.get(LYQUID_PACK_ASSET_TYPE_KEY) else {
328
0
                    continue;
329
                };
330
5
                let l_name = annotation.get(LYQUID_PACK_ASSET_NAME_KEY);
331
332
5
                if l_type != layer_type {
333
3
                    continue;
334
2
                }
335
336
2
                if let Some(
layer_name1
) = layer_name {
337
1
                    let Some(l_name) = l_name else {
338
0
                        continue;
339
                    };
340
1
                    if l_name != layer_name {
341
0
                        continue;
342
1
                    }
343
1
                }
344
345
                // pull it
346
2
                let mut out = Vec::with_capacity(od.size.clone().try_into().unwrap_or_default());
347
2
                self.client
348
2
                    .pull_blob(&reference, &od, &mut out)
349
2
                    .await
350
2
                    .map_err(|e| Error::PullError(
e0
.
to_string0
()))
?0
;
351
352
2
                layer_bytes = Some(Bytes::from(out));
353
2
                break;
354
            } else {
355
0
                continue;
356
            }
357
        }
358
359
2
        layer_bytes.ok_or(Error::PullError("Layer not found.".to_owned()))
360
2
    }
361
}
362
363
#[async_trait]
364
impl Registry for OCIRegistry {
365
1
    async fn push(&self, pack: LyquidPack, options: Option<RegistryOptions>) -> Result<LyquidPackDigest, Error> {
366
        self.push_pack(pack, options).await
367
1
    }
368
369
    async fn pull(
370
        &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>, layer_type: Option<&str>,
371
        layer_name: Option<&str>, options: Option<RegistryOptions>,
372
4
    ) -> Result<PulledContent, Error> {
373
        if let Some(layer_type) = layer_type {
374
            let layer_byte = self
375
                .pull_layer(name, tag, digest, layer_type, layer_name, options)
376
                .await?;
377
            Ok(PulledContent::Layer(layer_byte))
378
        } else {
379
            let pack = self.pull_pack(name, tag, digest, options).await?;
380
            Ok(PulledContent::LyquidPack(pack))
381
        }
382
4
    }
383
384
    async fn query(
385
        &self, name: &str, tag: Option<&str>, digest: Option<&LyquidPackDigest>,
386
1
    ) -> Result<LyquidPackMetadata, Error> {
387
        let reference = self.build_reference(name, tag, digest)?;
388
        let (_, _, config) = self
389
            .client
390
            .pull_manifest_and_config(&reference, &self.auth)
391
            .await
392
0
            .map_err(|e| Error::OciDistributionError(e.to_string()))?;
393
394
        let config = config.into_bytes();
395
        LyquidPackMetadata::from_json(config).map_err(|_| Error::BadMetadata)
396
1
    }
397
}