1use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use anyhow::{Context as _, Result};
25use composefs::fsverity::{Algorithm, FsVerityHashValue, Sha256HashValue, Sha512HashValue};
26use composefs::repository::{FsckResult, Repository, RepositoryConfig, system_path, user_path};
27use rustix::fs::CWD;
28use serde::{Deserialize, Serialize};
29
30use crate::{App, HashType, open_repo_at, resolve_hash_type};
31
32#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
39pub struct FsckReply {
40 pub ok: bool,
42 pub has_metadata: bool,
44 pub objects_checked: u64,
46 pub objects_corrupted: u64,
48 pub streams_checked: u64,
50 pub streams_corrupted: u64,
52 pub images_checked: u64,
54 pub images_corrupted: u64,
56 pub broken_links: u64,
58 pub missing_objects: u64,
60 pub errors: Vec<String>,
68}
69
70impl From<&FsckResult> for FsckReply {
71 fn from(result: &FsckResult) -> Self {
72 Self {
73 ok: result.is_ok(),
74 has_metadata: result.has_metadata(),
75 objects_checked: result.objects_checked(),
76 objects_corrupted: result.objects_corrupted(),
77 streams_checked: result.streams_checked(),
78 streams_corrupted: result.streams_corrupted(),
79 images_checked: result.images_checked(),
80 images_corrupted: result.images_corrupted(),
81 broken_links: result.broken_links(),
82 missing_objects: result.missing_objects(),
83 errors: result.errors().iter().map(|e| e.to_string()).collect(),
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
93pub struct GcReply {
94 pub result: composefs::repository::GcResult,
96 pub dry_run: bool,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
102pub struct ImageObjectsReply {
103 pub object_ids: Vec<String>,
106}
107
108#[derive(Debug, zlink::ReplyError, zlink::introspect::ReplyError)]
110#[zlink(interface = "org.composefs.Repository")]
111pub enum RepositoryError {
112 RepoNotFound {
114 message: String,
116 },
117 InvalidHandle {
119 handle: u64,
121 },
122 InvalidSpec {
124 message: String,
126 },
127 NoSuchRef {
129 reference: String,
131 },
132 InternalError {
134 message: String,
136 },
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
141pub struct OpenRepositoryReply {
142 pub handle: u64,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
148pub struct InitRepositoryReply {
149 pub created: bool,
152}
153
154#[derive(Debug, Clone)]
159pub(crate) enum OpenRepo {
160 Sha256(Arc<Repository<Sha256HashValue>>),
162 Sha512(Arc<Repository<Sha512HashValue>>),
164}
165
166#[derive(Debug)]
168struct HandleEntry {
169 repo: OpenRepo,
171 #[allow(dead_code)]
175 owner: Option<usize>,
176}
177
178#[derive(Debug, Clone)]
180struct OpenOptions {
181 insecure: bool,
183 require_verity: bool,
185 no_upgrade: bool,
187}
188
189impl OpenOptions {
190 fn from_app(args: &App) -> Self {
192 Self {
193 insecure: args.insecure,
194 require_verity: args.require_verity,
195 no_upgrade: args.no_upgrade,
196 }
197 }
198}
199
200impl Default for OpenOptions {
201 fn default() -> Self {
204 Self {
205 insecure: false,
206 require_verity: false,
207 no_upgrade: false,
208 }
209 }
210}
211
212#[derive(Debug)]
219pub(crate) struct CfsctlService {
220 repos: HashMap<u64, HandleEntry>,
222 next_handle: u64,
224 open_opts: OpenOptions,
226}
227
228impl CfsctlService {
229 fn with_open_opts(open_opts: OpenOptions) -> Self {
235 Self {
236 repos: HashMap::new(),
237 next_handle: 0,
238 open_opts,
239 }
240 }
241
242 pub(crate) fn from_app(args: &App) -> Self {
249 Self::with_open_opts(OpenOptions::from_app(args))
250 }
251
252 pub(crate) fn activated() -> Self {
256 Self::with_open_opts(OpenOptions::default())
257 }
258
259 fn next_handle(&mut self) -> u64 {
261 self.next_handle += 1;
262 self.next_handle
263 }
264
265 fn lookup_repo(&self, handle: u64) -> std::result::Result<OpenRepo, RepositoryError> {
270 self.repos
271 .get(&handle)
272 .map(|entry| entry.repo.clone())
273 .ok_or(RepositoryError::InvalidHandle { handle })
274 }
275
276 #[cfg(feature = "oci")]
281 fn lookup_oci(&self, handle: u64) -> std::result::Result<OpenRepo, oci::OciError> {
282 self.repos
283 .get(&handle)
284 .map(|entry| entry.repo.clone())
285 .ok_or(oci::OciError::InvalidHandle { handle })
286 }
287
288 fn do_open(
294 &mut self,
295 path: &Path,
296 owner: Option<usize>,
297 ) -> std::result::Result<u64, RepositoryError> {
298 let hash_type = resolve_hash_type(path, None, !self.open_opts.no_upgrade).map_err(|e| {
299 RepositoryError::RepoNotFound {
300 message: format!("{e:#}"),
301 }
302 })?;
303 let repo = match hash_type {
304 HashType::Sha256 => OpenRepo::Sha256(Arc::new(
305 open_repo_at::<Sha256HashValue>(
306 path,
307 self.open_opts.insecure,
308 self.open_opts.require_verity,
309 self.open_opts.no_upgrade,
310 )
311 .map_err(|e| RepositoryError::RepoNotFound {
312 message: format!("{e:#}"),
313 })?,
314 )),
315 HashType::Sha512 => OpenRepo::Sha512(Arc::new(
316 open_repo_at::<Sha512HashValue>(
317 path,
318 self.open_opts.insecure,
319 self.open_opts.require_verity,
320 self.open_opts.no_upgrade,
321 )
322 .map_err(|e| RepositoryError::RepoNotFound {
323 message: format!("{e:#}"),
324 })?,
325 )),
326 };
327 let handle = self.next_handle();
328 self.repos.insert(handle, HandleEntry { repo, owner });
329 Ok(handle)
330 }
331
332 fn resolve_selector(
337 path: Option<String>,
338 user: Option<bool>,
339 system: Option<bool>,
340 ) -> std::result::Result<PathBuf, RepositoryError> {
341 let user = user.unwrap_or(false);
342 let system = system.unwrap_or(false);
343 match (path, user, system) {
344 (Some(p), false, false) => Ok(PathBuf::from(p)),
345 (None, true, false) => user_path().map_err(|e| RepositoryError::InvalidSpec {
346 message: format!("{e:#}"),
347 }),
348 (None, false, true) => Ok(system_path()),
349 _ => Err(RepositoryError::InvalidSpec {
350 message: "exactly one of `path`, `user`, `system` must be set".into(),
351 }),
352 }
353 }
354}
355
356async fn run_fsck<ObjectID: FsVerityHashValue>(
358 repo: &Repository<ObjectID>,
359 metadata_only: bool,
360) -> std::result::Result<FsckResult, RepositoryError> {
361 let result = if metadata_only {
362 repo.fsck_metadata_only().await
363 } else {
364 repo.fsck().await
365 };
366 result.map_err(|e| RepositoryError::InternalError {
367 message: format!("{e:#}"),
368 })
369}
370
371async fn run_gc<ObjectID: FsVerityHashValue>(
373 repo: &Repository<ObjectID>,
374 dry_run: bool,
375 roots: Vec<String>,
376) -> std::result::Result<GcReply, RepositoryError> {
377 let root_refs: Vec<&str> = roots.iter().map(String::as_str).collect();
378 let result = if dry_run {
379 repo.gc_dry_run(&root_refs)
380 } else {
381 repo.gc(&root_refs)
382 }
383 .map_err(|e| RepositoryError::InternalError {
384 message: format!("{e:#}"),
385 })?;
386 Ok(GcReply { result, dry_run })
387}
388
389async fn run_image_objects<ObjectID: FsVerityHashValue>(
391 repo: &Repository<ObjectID>,
392 name: String,
393) -> std::result::Result<ImageObjectsReply, RepositoryError> {
394 let objects = repo.objects_for_image(&name).map_err(|e| {
395 if let Some(nf) = e.downcast_ref::<composefs::ImageNotFound>() {
396 RepositoryError::NoSuchRef {
397 reference: nf.name.clone(),
398 }
399 } else {
400 RepositoryError::InternalError {
401 message: format!("{e:#}"),
402 }
403 }
404 })?;
405 let mut object_ids: Vec<String> = objects.iter().map(|id| id.to_id()).collect();
406 object_ids.sort();
407 Ok(ImageObjectsReply { object_ids })
408}
409
410#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, zlink::introspect::Type)]
414pub struct MountParams {
415 pub overlay: Option<bool>,
418 pub read_write: Option<bool>,
420}
421
422impl MountParams {
423 fn to_mount_options(
425 &self,
426 fds: Vec<std::os::fd::OwnedFd>,
427 ) -> std::result::Result<composefs::mount::MountOptions, RepositoryError> {
428 let overlay = self.overlay.unwrap_or(false);
429
430 let mut expected_fds = 0;
431 if overlay {
432 expected_fds += 2;
433 }
434
435 if fds.len() != expected_fds {
436 return Err(RepositoryError::InvalidSpec {
437 message: format!(
438 "Mount expects {expected_fds} fds for the requested options, got {}",
439 fds.len()
440 ),
441 });
442 }
443
444 let mut options = composefs::mount::MountOptions::default();
445 let mut fd_iter = fds.into_iter();
446 if overlay {
447 let upperdir = fd_iter.next().unwrap();
448 let workdir = fd_iter.next().unwrap();
449 options.set_overlay(upperdir, workdir);
450 }
451 options.set_read_write(self.read_write.unwrap_or(false));
452
453 Ok(options)
454 }
455}
456
457#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, zlink::introspect::Type)]
459pub struct MountReply {
460 pub fd_index: u32,
462}
463
464fn run_mount<ObjectID: FsVerityHashValue>(
465 repo: &Repository<ObjectID>,
466 name: &str,
467 params: &MountParams,
468 fds: Vec<std::os::fd::OwnedFd>,
469) -> std::result::Result<(MountReply, Vec<std::os::fd::OwnedFd>), RepositoryError> {
470 let options = params.to_mount_options(fds)?;
471
472 let mount_fd =
473 repo.mount_with_options(name, &options)
474 .map_err(|e| RepositoryError::InternalError {
475 message: format!("{e:#}"),
476 })?;
477
478 Ok((MountReply { fd_index: 0 }, vec![mount_fd]))
479}
480
481#[cfg(feature = "oci")]
482fn run_oci_mount<ObjectID: composefs::fsverity::FsVerityHashValue>(
483 repo: &Repository<ObjectID>,
484 image: &str,
485 bootable: bool,
486 params: &MountParams,
487 fds: Vec<std::os::fd::OwnedFd>,
488) -> std::result::Result<(MountReply, Vec<std::os::fd::OwnedFd>), oci::OciError> {
489 let img = if image.starts_with("sha256:") {
490 let digest: composefs_oci::OciDigest =
491 image.parse().map_err(|e| oci::OciError::InternalError {
492 message: format!("Invalid manifest digest: {e}"),
493 })?;
494 composefs_oci::OciImage::open(repo, &digest, None)
495 } else {
496 composefs_oci::OciImage::open_ref(repo, image)
497 }
498 .map_err(|e| oci::OciError::NoSuchImage {
499 image: format!("{image}: {e:#}"),
500 })?;
501
502 let erofs_id = if bootable {
503 img.boot_image_ref(repo.erofs_version())
504 } else {
505 img.image_ref(repo.erofs_version())
506 }
507 .ok_or_else(|| oci::OciError::InternalError {
508 message: if bootable {
509 "No boot EROFS image linked".into()
510 } else {
511 "No composefs EROFS image linked".into()
512 },
513 })?;
514
515 let options = params
516 .to_mount_options(fds)
517 .map_err(|e| oci::OciError::InternalError {
518 message: format!("{e:?}"),
519 })?;
520 let mount_fd = repo
521 .mount_with_options(&erofs_id.to_hex(), &options)
522 .map_err(|e| oci::OciError::InternalError {
523 message: format!("{e:#}"),
524 })?;
525
526 Ok((MountReply { fd_index: 0 }, vec![mount_fd]))
527}
528
529fn run_init_repository(
536 path: &Path,
537 algorithm: Algorithm,
538 insecure: bool,
539) -> std::result::Result<InitRepositoryReply, RepositoryError> {
540 if let Some(parent) = path.parent() {
542 std::fs::create_dir_all(parent).map_err(|e| RepositoryError::InternalError {
543 message: format!("creating parent directories for {}: {e:#}", path.display()),
544 })?;
545 }
546
547 let created = match algorithm {
548 Algorithm::Sha256 { .. } => {
549 let config = if insecure {
550 RepositoryConfig::new(algorithm).set_insecure()
551 } else {
552 RepositoryConfig::new(algorithm)
553 };
554 Repository::<Sha256HashValue>::init_path(CWD, path, config)
555 .map_err(|e| RepositoryError::InternalError {
556 message: format!("{e:#}"),
557 })?
558 .1
559 }
560 Algorithm::Sha512 { .. } => {
561 let config = if insecure {
562 RepositoryConfig::new(algorithm).set_insecure()
563 } else {
564 RepositoryConfig::new(algorithm)
565 };
566 Repository::<Sha512HashValue>::init_path(CWD, path, config)
567 .map_err(|e| RepositoryError::InternalError {
568 message: format!("{e:#}"),
569 })?
570 .1
571 }
572 };
573 Ok(InitRepositoryReply { created })
574}
575
576#[cfg(feature = "oci")]
579async fn run_list_images<ObjectID: FsVerityHashValue>(
580 repo: &Repository<ObjectID>,
581 filter: Option<String>,
582) -> std::result::Result<Vec<oci::ImageEntry>, oci::OciError> {
583 composefs_oci::oci_image::list_images(repo)
584 .map(|imgs| {
585 imgs.iter()
586 .filter(|img| match &filter {
587 Some(needle) => img.name.contains(needle.as_str()),
588 None => true,
589 })
590 .map(oci::ImageEntry::from)
591 .collect()
592 })
593 .map_err(|e| oci::OciError::InternalError {
594 message: format!("{e:#}"),
595 })
596}
597
598#[cfg(feature = "oci")]
603async fn run_oci_fsck<ObjectID: FsVerityHashValue>(
604 repo: &Repository<ObjectID>,
605 image: Option<String>,
606) -> std::result::Result<oci::OciFsckReply, oci::OciError> {
607 let result = match image {
608 Some(name) => composefs_oci::oci_fsck_image(repo, &name).await,
609 None => composefs_oci::oci_fsck(repo).await,
610 }
611 .map_err(|e| oci::OciError::InternalError {
612 message: format!("{e:#}"),
613 })?;
614 Ok(oci::OciFsckReply::from(&result))
615}
616
617#[cfg(feature = "oci")]
619async fn run_inspect<ObjectID: FsVerityHashValue>(
620 repo: &Repository<ObjectID>,
621 image: String,
622) -> std::result::Result<oci::OciInspectReply, oci::OciError> {
623 let reference: crate::OciReference =
624 image.parse().map_err(|e| oci::OciError::InternalError {
625 message: format!("invalid image reference: {e:#}"),
626 })?;
627 let img = crate::resolve_oci_image(repo, &reference).map_err(|e| {
628 if let Some(nf) = e.downcast_ref::<composefs_oci::OciRefNotFound>() {
629 oci::OciError::NoSuchImage {
630 image: nf.name.clone(),
631 }
632 } else if let Some(nf) = e.downcast_ref::<composefs_oci::OciImageNotFound>() {
633 oci::OciError::NoSuchImage {
634 image: nf.digest.clone(),
635 }
636 } else {
637 oci::OciError::InternalError {
638 message: format!("{e:#}"),
639 }
640 }
641 })?;
642
643 oci::OciInspectReply::from_image(repo, &img).map_err(|e| oci::OciError::InternalError {
644 message: format!("{e:#}"),
645 })
646}
647
648#[cfg(feature = "oci")]
650async fn run_tag<ObjectID: FsVerityHashValue>(
651 repo: &Repository<ObjectID>,
652 manifest_digest: String,
653 name: String,
654) -> std::result::Result<(), oci::OciError> {
655 let digest: composefs_oci::OciDigest =
656 manifest_digest
657 .parse()
658 .map_err(|e| oci::OciError::InternalError {
659 message: format!("invalid digest: {e}"),
660 })?;
661 composefs_oci::oci_image::tag_image(repo, &digest, &name).map_err(|e| {
662 oci::OciError::InternalError {
663 message: format!("{e:#}"),
664 }
665 })
666}
667
668#[cfg(feature = "oci")]
670async fn run_untag<ObjectID: FsVerityHashValue>(
671 repo: &Repository<ObjectID>,
672 name: String,
673) -> std::result::Result<(), oci::OciError> {
674 composefs_oci::oci_image::untag_image(repo, &name).map_err(|e| oci::OciError::InternalError {
675 message: format!("{e:#}"),
676 })
677}
678
679#[cfg(feature = "oci")]
685async fn run_compute_id<ObjectID: FsVerityHashValue>(
686 repo: &Repository<ObjectID>,
687 image: String,
688 verity: Option<String>,
689 bootable: bool,
690) -> std::result::Result<oci::OciComputeIdReply, oci::OciError> {
691 let reference: crate::OciReference =
692 image.parse().map_err(|e| oci::OciError::InternalError {
693 message: format!("invalid image reference: {e:#}"),
694 })?;
695 let verity_override =
696 crate::verity_opt::<ObjectID>(&verity).map_err(|e| oci::OciError::InternalError {
697 message: format!("invalid verity: {e:#}"),
698 })?;
699 let (config_digest, config_verity) =
700 crate::resolve_oci_config(repo, &reference, verity_override).map_err(|e| {
701 oci::OciError::InternalError {
702 message: format!("{e:#}"),
703 }
704 })?;
705
706 let mut fs =
707 composefs_oci::image::create_filesystem(repo, &config_digest, config_verity.as_ref())
708 .map_err(|e| oci::OciError::InternalError {
709 message: format!("{e:#}"),
710 })?;
711 if bootable {
712 use composefs_boot::BootOps as _;
713 fs.transform_for_boot(repo)
714 .map_err(|e| oci::OciError::InternalError {
715 message: format!("{e:#}"),
716 })?;
717 }
718 let id = fs.compute_image_id(repo.erofs_version());
719 Ok(oci::OciComputeIdReply {
720 image_id: id.to_hex(),
721 })
722}
723
724#[cfg(not(feature = "oci"))]
742mod service_impl {
743 #![allow(missing_docs)]
744
745 use super::{
746 CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, MountParams,
747 MountReply, OpenRepo, OpenRepositoryReply, RepositoryError, run_fsck, run_gc,
748 run_image_objects, run_init_repository, run_mount,
749 };
750 use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue};
751
752 #[zlink::service(
753 interface = "org.composefs.Repository",
754 vendor = "org.composefs",
755 product = "cfsctl",
756 version = env!("CARGO_PKG_VERSION"),
757 url = "https://github.com/composefs/composefs-rs"
758 )]
759 impl<Sock> CfsctlService {
760 async fn init_repository(
770 &mut self,
771 path: String,
772 algorithm: Option<String>,
773 insecure: Option<bool>,
774 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
775 let algorithm: Algorithm = algorithm
776 .as_deref()
777 .unwrap_or("fsverity-sha512-12")
778 .parse()
779 .map_err(|e| RepositoryError::InvalidSpec {
780 message: format!("invalid algorithm: {e}"),
781 })?;
782 let insecure = insecure.unwrap_or(self.open_opts.insecure);
783 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
784 }
785
786 async fn open_repository(
790 &mut self,
791 path: Option<String>,
792 user: Option<bool>,
793 system: Option<bool>,
794 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
795 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
796 let selected = Self::resolve_selector(path, user, system)?;
797 let handle = self.do_open(&selected, Some(conn.id()))?;
798 Ok(OpenRepositoryReply { handle })
799 }
800
801 async fn close_repository(
803 &mut self,
804 handle: u64,
805 ) -> std::result::Result<(), RepositoryError> {
806 self.repos
807 .remove(&handle)
808 .map(|_| ())
809 .ok_or(RepositoryError::InvalidHandle { handle })
810 }
811
812 async fn fsck(
818 &self,
819 handle: u64,
820 metadata_only: Option<bool>,
821 ) -> std::result::Result<FsckReply, RepositoryError> {
822 let metadata_only = metadata_only.unwrap_or(false);
823 let result = match self.lookup_repo(handle)? {
824 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
825 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
826 }?;
827 Ok(FsckReply::from(&result))
828 }
829
830 async fn gc(
832 &self,
833 handle: u64,
834 dry_run: bool,
835 roots: Vec<String>,
836 ) -> std::result::Result<GcReply, RepositoryError> {
837 match self.lookup_repo(handle)? {
838 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
839 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
840 }
841 }
842
843 async fn image_objects(
845 &self,
846 handle: u64,
847 name: String,
848 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
849 match self.lookup_repo(handle)? {
850 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
851 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
852 }
853 }
854
855 #[zlink(return_fds)]
861 async fn mount(
862 &self,
863 handle: u64,
864 name: String,
865 options: MountParams,
866 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
867 ) -> (
868 std::result::Result<MountReply, RepositoryError>,
869 Vec<std::os::fd::OwnedFd>,
870 ) {
871 let result = match self.lookup_repo(handle) {
872 Ok(OpenRepo::Sha256(ref r)) => {
873 run_mount::<Sha256HashValue>(r, &name, &options, fds)
874 }
875 Ok(OpenRepo::Sha512(ref r)) => {
876 run_mount::<Sha512HashValue>(r, &name, &options, fds)
877 }
878 Err(e) => Err(e),
879 };
880 match result {
881 Ok((reply, fds)) => (Ok(reply), fds),
882 Err(e) => (Err(e), vec![]),
883 }
884 }
885 }
886}
887
888#[cfg(feature = "oci")]
893mod service_impl {
894 #![allow(missing_docs)]
895
896 use super::oci::{
897 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
898 parse_local_fetch, pull_stream,
899 };
900 use super::{
901 CfsctlService, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, MountParams,
902 MountReply, OpenRepo, OpenRepositoryReply, RepositoryError, run_compute_id, run_fsck,
903 run_gc, run_image_objects, run_init_repository, run_inspect, run_list_images, run_mount,
904 run_oci_fsck, run_oci_mount, run_tag, run_untag,
905 };
906 use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue};
907
908 #[zlink::service(
909 interface = "org.composefs.Repository",
910 vendor = "org.composefs",
911 product = "cfsctl",
912 version = env!("CARGO_PKG_VERSION"),
913 url = "https://github.com/composefs/composefs-rs"
914 )]
915 impl<Sock> CfsctlService {
916 async fn init_repository(
928 &mut self,
929 path: String,
930 algorithm: Option<String>,
931 insecure: Option<bool>,
932 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
933 let algorithm: Algorithm = algorithm
934 .as_deref()
935 .unwrap_or("fsverity-sha512-12")
936 .parse()
937 .map_err(|e| RepositoryError::InvalidSpec {
938 message: format!("invalid algorithm: {e}"),
939 })?;
940 let insecure = insecure.unwrap_or(self.open_opts.insecure);
941 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
942 }
943
944 async fn open_repository(
948 &mut self,
949 path: Option<String>,
950 user: Option<bool>,
951 system: Option<bool>,
952 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
953 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
954 let selected = Self::resolve_selector(path, user, system)?;
955 let handle = self.do_open(&selected, Some(conn.id()))?;
956 Ok(OpenRepositoryReply { handle })
957 }
958
959 async fn close_repository(
961 &mut self,
962 handle: u64,
963 ) -> std::result::Result<(), RepositoryError> {
964 self.repos
965 .remove(&handle)
966 .map(|_| ())
967 .ok_or(RepositoryError::InvalidHandle { handle })
968 }
969
970 async fn fsck(
976 &self,
977 handle: u64,
978 metadata_only: Option<bool>,
979 ) -> std::result::Result<FsckReply, RepositoryError> {
980 let metadata_only = metadata_only.unwrap_or(false);
981 let result = match self.lookup_repo(handle)? {
982 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
983 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
984 }?;
985 Ok(FsckReply::from(&result))
986 }
987
988 async fn gc(
990 &self,
991 handle: u64,
992 dry_run: bool,
993 roots: Vec<String>,
994 ) -> std::result::Result<GcReply, RepositoryError> {
995 match self.lookup_repo(handle)? {
996 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
997 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
998 }
999 }
1000
1001 async fn image_objects(
1003 &self,
1004 handle: u64,
1005 name: String,
1006 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
1007 match self.lookup_repo(handle)? {
1008 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
1009 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
1010 }
1011 }
1012
1013 #[zlink(return_fds)]
1019 async fn mount(
1020 &self,
1021 handle: u64,
1022 name: String,
1023 options: MountParams,
1024 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1025 ) -> (
1026 std::result::Result<MountReply, RepositoryError>,
1027 Vec<std::os::fd::OwnedFd>,
1028 ) {
1029 let result = match self.lookup_repo(handle) {
1030 Ok(OpenRepo::Sha256(ref r)) => {
1031 run_mount::<Sha256HashValue>(r, &name, &options, fds)
1032 }
1033 Ok(OpenRepo::Sha512(ref r)) => {
1034 run_mount::<Sha512HashValue>(r, &name, &options, fds)
1035 }
1036 Err(e) => Err(e),
1037 };
1038 match result {
1039 Ok((reply, fds)) => (Ok(reply), fds),
1040 Err(e) => (Err(e), vec![]),
1041 }
1042 }
1043
1044 #[zlink(interface = "org.composefs.Oci")]
1055 async fn list_images(
1056 &self,
1057 handle: u64,
1058 filter: Option<String>,
1059 ) -> std::result::Result<ListImagesReply, OciError> {
1060 let images = match self.lookup_oci(handle)? {
1061 OpenRepo::Sha256(ref r) => run_list_images::<Sha256HashValue>(r, filter).await,
1062 OpenRepo::Sha512(ref r) => run_list_images::<Sha512HashValue>(r, filter).await,
1063 }?;
1064 Ok(ListImagesReply { images })
1065 }
1066
1067 #[zlink(interface = "org.composefs.Oci", rename = "Check")]
1073 async fn oci_fsck(
1074 &self,
1075 handle: u64,
1076 image: Option<String>,
1077 ) -> std::result::Result<OciFsckReply, OciError> {
1078 match self.lookup_oci(handle)? {
1079 OpenRepo::Sha256(ref r) => run_oci_fsck::<Sha256HashValue>(r, image).await,
1080 OpenRepo::Sha512(ref r) => run_oci_fsck::<Sha512HashValue>(r, image).await,
1081 }
1082 }
1083
1084 #[zlink(interface = "org.composefs.Oci")]
1086 async fn inspect(
1087 &self,
1088 handle: u64,
1089 image: String,
1090 ) -> std::result::Result<OciInspectReply, OciError> {
1091 match self.lookup_oci(handle)? {
1092 OpenRepo::Sha256(ref r) => run_inspect::<Sha256HashValue>(r, image).await,
1093 OpenRepo::Sha512(ref r) => run_inspect::<Sha512HashValue>(r, image).await,
1094 }
1095 }
1096
1097 #[zlink(interface = "org.composefs.Oci")]
1099 async fn tag(
1100 &self,
1101 handle: u64,
1102 manifest_digest: String,
1103 name: String,
1104 ) -> std::result::Result<(), OciError> {
1105 match self.lookup_oci(handle)? {
1106 OpenRepo::Sha256(ref r) => {
1107 run_tag::<Sha256HashValue>(r, manifest_digest, name).await
1108 }
1109 OpenRepo::Sha512(ref r) => {
1110 run_tag::<Sha512HashValue>(r, manifest_digest, name).await
1111 }
1112 }
1113 }
1114
1115 #[zlink(interface = "org.composefs.Oci")]
1117 async fn untag(&self, handle: u64, name: String) -> std::result::Result<(), OciError> {
1118 match self.lookup_oci(handle)? {
1119 OpenRepo::Sha256(ref r) => run_untag::<Sha256HashValue>(r, name).await,
1120 OpenRepo::Sha512(ref r) => run_untag::<Sha512HashValue>(r, name).await,
1121 }
1122 }
1123
1124 #[zlink(interface = "org.composefs.Oci")]
1126 async fn compute_id(
1127 &self,
1128 handle: u64,
1129 image: String,
1130 verity: Option<String>,
1131 bootable: bool,
1132 ) -> std::result::Result<OciComputeIdReply, OciError> {
1133 match self.lookup_oci(handle)? {
1134 OpenRepo::Sha256(ref r) => {
1135 run_compute_id::<Sha256HashValue>(r, image, verity, bootable).await
1136 }
1137 OpenRepo::Sha512(ref r) => {
1138 run_compute_id::<Sha512HashValue>(r, image, verity, bootable).await
1139 }
1140 }
1141 }
1142
1143 #[zlink(interface = "org.composefs.Oci", more)]
1149 #[allow(clippy::too_many_arguments)]
1150 async fn pull(
1151 &self,
1152 more: bool,
1153 handle: u64,
1154 image: String,
1155 name: Option<String>,
1156 local_fetch: String,
1157 storage_root: Option<String>,
1158 bootable: bool,
1159 ) -> impl zlink::futures_util::Stream<
1160 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
1161 > + Send {
1162 let lf = parse_local_fetch(&local_fetch);
1163 let sr = storage_root.map(std::path::PathBuf::from);
1164 match self.repos.get(&handle).map(|entry| &entry.repo) {
1169 Some(OpenRepo::Sha256(r)) => {
1170 pull_stream::<Sha256HashValue>(r.clone(), image, name, lf, sr, bootable, more)
1171 }
1172 Some(OpenRepo::Sha512(r)) => {
1173 pull_stream::<Sha512HashValue>(r.clone(), image, name, lf, sr, bootable, more)
1174 }
1175 None => {
1176 use zlink::futures_util::stream;
1177 Box::pin(stream::once(async move {
1178 Err(OciError::InvalidHandle { handle })
1179 }))
1180 }
1181 }
1182 }
1183
1184 #[zlink(interface = "org.composefs.Oci", return_fds)]
1191 async fn oci_mount(
1192 &self,
1193 handle: u64,
1194 image: String,
1195 bootable: bool,
1196 options: MountParams,
1197 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1198 ) -> (
1199 std::result::Result<MountReply, OciError>,
1200 Vec<std::os::fd::OwnedFd>,
1201 ) {
1202 let result = match self.lookup_oci(handle) {
1203 Ok(OpenRepo::Sha256(ref r)) => {
1204 run_oci_mount::<Sha256HashValue>(r, &image, bootable, &options, fds)
1205 }
1206 Ok(OpenRepo::Sha512(ref r)) => {
1207 run_oci_mount::<Sha512HashValue>(r, &image, bootable, &options, fds)
1208 }
1209 Err(e) => Err(e),
1210 };
1211 match result {
1212 Ok((reply, fds)) => (Ok(reply), fds),
1213 Err(e) => (Err(e), vec![]),
1214 }
1215 }
1216 }
1217}
1218
1219#[derive(Debug)]
1226pub(crate) struct ActivatedListener {
1227 conn: Option<zlink::Connection<zlink::unix::Stream>>,
1229}
1230
1231impl zlink::Listener for ActivatedListener {
1232 type Socket = zlink::unix::Stream;
1233
1234 async fn accept(&mut self) -> zlink::Result<zlink::Connection<Self::Socket>> {
1235 match self.conn.take() {
1236 Some(conn) => Ok(conn),
1237 None => std::future::pending().await,
1238 }
1239 }
1240}
1241
1242#[allow(unsafe_code)]
1248pub(crate) fn try_activated_listener() -> Result<Option<ActivatedListener>> {
1249 use std::os::fd::{FromRawFd as _, IntoRawFd as _};
1250
1251 let fds = libsystemd::activation::receive_descriptors(true)
1252 .map_err(|e| anyhow::anyhow!("Failed to receive activation fds: {e}"))?;
1253
1254 let fd = match fds.into_iter().next() {
1255 Some(fd) => fd,
1256 None => return Ok(None),
1257 };
1258
1259 let std_stream = unsafe { std::os::unix::net::UnixStream::from_raw_fd(fd.into_raw_fd()) };
1263 std_stream
1264 .set_nonblocking(true)
1265 .context("setting systemd socket to non-blocking")?;
1266 let tokio_stream = tokio::net::UnixStream::from_std(std_stream)
1267 .context("converting systemd UnixStream to tokio")?;
1268 let zlink_stream = zlink::unix::Stream::from(tokio_stream);
1269 let conn = zlink::Connection::from(zlink_stream);
1270 Ok(Some(ActivatedListener { conn: Some(conn) }))
1271}
1272
1273pub(crate) async fn serve_activated<S>(service: S, listener: ActivatedListener) -> Result<()>
1279where
1280 S: zlink::Service<zlink::unix::Stream>,
1281{
1282 log::info!("Listening on systemd-activated socket");
1283 let server = zlink::Server::new(listener, service);
1284 server
1285 .run()
1286 .await
1287 .context("running varlink server (activated)")
1288}
1289
1290pub(crate) async fn serve<S>(service: S, address: Option<&Path>) -> Result<()>
1297where
1298 S: zlink::Service<zlink::unix::Stream>,
1299{
1300 if let Some(activated) = try_activated_listener()? {
1301 return serve_activated(service, activated).await;
1302 }
1303 let address = address.context("no --address given and not socket-activated")?;
1304 let listener = zlink::unix::bind(address)
1305 .with_context(|| format!("binding varlink socket at {}", address.display()))?;
1306 log::info!("Listening on {}", address.display());
1307 let server = zlink::Server::new(listener, service);
1308 server.run().await.context("running varlink server")
1309}
1310
1311#[cfg(feature = "oci")]
1316pub mod oci {
1317 use super::*;
1318
1319 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1321 pub struct ImageEntry {
1322 pub name: String,
1324 pub manifest_digest: String,
1326 pub is_container: bool,
1328 pub architecture: String,
1330 pub os: String,
1332 pub created: Option<String>,
1334 pub layer_count: u64,
1336 pub referrer_count: u64,
1338 }
1339
1340 impl From<&composefs_oci::oci_image::ImageInfo> for ImageEntry {
1341 fn from(info: &composefs_oci::oci_image::ImageInfo) -> Self {
1342 Self {
1343 name: info.name.clone(),
1344 manifest_digest: info.manifest_digest.to_string(),
1345 is_container: info.is_container,
1346 architecture: info.architecture.clone(),
1347 os: info.os.clone(),
1348 created: info.created.clone(),
1349 layer_count: info.layer_count as u64,
1350 referrer_count: info.referrer_count as u64,
1351 }
1352 }
1353 }
1354
1355 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1357 pub struct ListImagesReply {
1358 pub images: Vec<ImageEntry>,
1360 }
1361
1362 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1367 pub struct OciFsckReply {
1368 pub ok: bool,
1370 pub images_checked: u64,
1372 pub images_corrupted: u64,
1374 pub errors: Vec<String>,
1376 pub repo: FsckReply,
1378 }
1379
1380 impl From<&composefs_oci::OciFsckResult> for OciFsckReply {
1381 fn from(result: &composefs_oci::OciFsckResult) -> Self {
1382 Self {
1383 ok: result.is_ok(),
1384 images_checked: result.images_checked(),
1385 images_corrupted: result.images_corrupted(),
1386 errors: result.errors().iter().map(|e| e.to_string()).collect(),
1387 repo: FsckReply::from(result.repo_result()),
1388 }
1389 }
1390 }
1391
1392 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1394 pub struct OciInspectReply {
1395 pub manifest: String,
1397 pub config: String,
1399 pub referrers: Vec<String>,
1401 pub composefs_erofs: Option<String>,
1403 pub composefs_boot_erofs: Option<String>,
1409 }
1410
1411 impl OciInspectReply {
1412 pub fn from_image<ObjectID: FsVerityHashValue>(
1415 repo: &Repository<ObjectID>,
1416 img: &composefs_oci::oci_image::OciImage<ObjectID>,
1417 ) -> anyhow::Result<Self> {
1418 let manifest = String::from_utf8(img.read_manifest_json(repo)?)
1419 .context("manifest is not valid UTF-8")?;
1420 let config = String::from_utf8(img.read_config_json(repo)?)
1421 .context("config is not valid UTF-8")?;
1422 let referrers = composefs_oci::oci_image::list_referrers(repo, img.manifest_digest())?
1423 .iter()
1424 .map(|(digest, _verity)| digest.to_string())
1425 .collect();
1426 Ok(Self {
1427 manifest,
1428 config,
1429 referrers,
1430 composefs_erofs: img.image_ref(repo.erofs_version()).map(|id| id.to_hex()),
1431 composefs_boot_erofs: img
1432 .boot_image_ref(repo.erofs_version())
1433 .map(|id| id.to_hex()),
1434 })
1435 }
1436 }
1437
1438 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1440 pub struct OciComputeIdReply {
1441 pub image_id: String,
1443 }
1444
1445 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1458 pub struct PullProgress {
1459 #[serde(skip_serializing_if = "Option::is_none", default)]
1461 pub started: Option<Started>,
1462 #[serde(skip_serializing_if = "Option::is_none", default)]
1464 pub progress: Option<Progress>,
1465 #[serde(skip_serializing_if = "Option::is_none", default)]
1467 pub skipped: Option<Skipped>,
1468 #[serde(skip_serializing_if = "Option::is_none", default)]
1470 pub done: Option<Done>,
1471 #[serde(skip_serializing_if = "Option::is_none", default)]
1473 pub message: Option<String>,
1474 #[serde(skip_serializing_if = "Option::is_none", default)]
1477 pub completed: Option<Completed>,
1478 }
1479
1480 #[derive(Debug, Clone, Copy, Serialize, Deserialize, zlink::introspect::Type)]
1482 pub enum ProgressUnit {
1483 Bytes,
1485 Items,
1487 }
1488
1489 impl From<composefs::progress::ProgressUnit> for ProgressUnit {
1490 fn from(unit: composefs::progress::ProgressUnit) -> Self {
1491 use composefs::progress::ProgressUnit as U;
1492 match unit {
1493 U::Bytes => ProgressUnit::Bytes,
1494 U::Items => ProgressUnit::Items,
1495 _ => ProgressUnit::Items,
1497 }
1498 }
1499 }
1500
1501 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1503 pub struct Started {
1504 pub id: String,
1506 pub total: Option<u64>,
1508 pub unit: ProgressUnit,
1510 }
1511
1512 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1514 pub struct Progress {
1515 pub id: String,
1517 pub fetched: u64,
1519 pub total: Option<u64>,
1521 }
1522
1523 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1525 pub struct Skipped {
1526 pub id: String,
1528 }
1529
1530 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1532 pub struct Done {
1533 pub id: String,
1535 pub transferred: u64,
1537 }
1538
1539 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
1541 pub struct Completed {
1542 pub manifest_digest: String,
1544 pub config_digest: String,
1546 pub manifest_verity: String,
1548 pub config_verity: String,
1550 pub stats: String,
1552 pub boot_image: Option<String>,
1555 }
1556
1557 impl PullProgress {
1558 fn empty() -> Self {
1561 PullProgress {
1562 started: None,
1563 progress: None,
1564 skipped: None,
1565 done: None,
1566 message: None,
1567 completed: None,
1568 }
1569 }
1570 }
1571
1572 impl From<composefs::progress::ProgressEvent> for PullProgress {
1573 fn from(event: composefs::progress::ProgressEvent) -> Self {
1577 use composefs::progress::ProgressEvent;
1578
1579 let mut p = PullProgress::empty();
1580 match event {
1581 ProgressEvent::Started { id, total, unit } => {
1582 p.started = Some(Started {
1583 id: id.into_inner(),
1584 total,
1585 unit: unit.into(),
1586 });
1587 }
1588 ProgressEvent::Progress { id, fetched, total } => {
1589 p.progress = Some(Progress {
1590 id: id.into_inner(),
1591 fetched,
1592 total,
1593 });
1594 }
1595 ProgressEvent::Skipped { id } => {
1596 p.skipped = Some(Skipped {
1597 id: id.into_inner(),
1598 });
1599 }
1600 ProgressEvent::Done { id, transferred } => {
1601 p.done = Some(Done {
1602 id: id.into_inner(),
1603 transferred,
1604 });
1605 }
1606 ProgressEvent::Message(s) => {
1607 p.message = Some(s);
1608 }
1609 other => {
1612 p.message = Some(format!("{other:?}"));
1613 }
1614 }
1615 p
1616 }
1617 }
1618
1619 struct ChannelReporter {
1622 tx: tokio::sync::mpsc::UnboundedSender<PullProgress>,
1623 }
1624
1625 impl std::fmt::Debug for ChannelReporter {
1626 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1627 f.debug_struct("ChannelReporter").finish_non_exhaustive()
1628 }
1629 }
1630
1631 impl composefs::progress::ProgressReporter for ChannelReporter {
1632 fn report(&self, event: composefs::progress::ProgressEvent) {
1633 let _ = self.tx.send(PullProgress::from(event));
1636 }
1637 }
1638
1639 struct AbortOnDrop {
1645 handle: Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>>,
1646 }
1647
1648 impl AbortOnDrop {
1649 fn take(&mut self) -> Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>> {
1651 self.handle.take()
1652 }
1653 }
1654
1655 impl std::fmt::Debug for AbortOnDrop {
1656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1657 f.debug_struct("AbortOnDrop").finish_non_exhaustive()
1658 }
1659 }
1660
1661 impl Drop for AbortOnDrop {
1662 fn drop(&mut self) {
1663 if let Some(handle) = &self.handle {
1664 handle.abort();
1665 }
1666 }
1667 }
1668
1669 pub(crate) fn parse_local_fetch(value: &str) -> composefs_oci::LocalFetchOpt {
1673 use composefs_oci::LocalFetchOpt;
1674 match value {
1675 "auto" | "if-possible" => LocalFetchOpt::IfPossible,
1676 "zerocopy" | "zero-copy" => LocalFetchOpt::ZeroCopy,
1677 _ => LocalFetchOpt::Disabled,
1678 }
1679 }
1680
1681 #[allow(clippy::too_many_arguments)]
1693 pub(crate) fn pull_stream<ObjectID: FsVerityHashValue>(
1694 repo: Arc<Repository<ObjectID>>,
1695 image: String,
1696 name: Option<String>,
1697 local_fetch: composefs_oci::LocalFetchOpt,
1698 storage_root: Option<PathBuf>,
1699 bootable: bool,
1700 more: bool,
1701 ) -> std::pin::Pin<
1702 Box<
1703 dyn zlink::futures_util::Stream<
1704 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
1705 > + Send,
1706 >,
1707 > {
1708 use zlink::futures_util::stream;
1709
1710 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<PullProgress>();
1711 let reporter: Option<composefs::progress::SharedReporter> = if more {
1713 Some(std::sync::Arc::new(ChannelReporter { tx: tx.clone() }))
1714 } else {
1715 None
1716 };
1717
1718 let task_tx = tx.clone();
1723 let handle = tokio::spawn(async move {
1724 let opts = composefs_oci::PullOptions {
1725 local_fetch,
1726 storage_root: storage_root.as_deref(),
1727 progress: reporter,
1728 ..Default::default()
1729 };
1730 let result = composefs_oci::pull(&repo, &image, name.as_deref(), opts)
1731 .await
1732 .map_err(|e| OciError::InternalError {
1733 message: format!("{e:#}"),
1734 })?;
1735
1736 let boot_image = if bootable {
1737 let id = composefs_oci::generate_boot_image(&repo, &result.manifest_digest)
1738 .map_err(|e| OciError::InternalError {
1739 message: format!("{e:#}"),
1740 })?;
1741 Some(id.to_hex())
1742 } else {
1743 None
1744 };
1745
1746 let completed = PullProgress {
1747 completed: Some(Completed {
1748 manifest_digest: result.manifest_digest.to_string(),
1749 config_digest: result.config_digest.to_string(),
1750 manifest_verity: result.manifest_verity.to_hex(),
1751 config_verity: result.config_verity.to_hex(),
1752 stats: result.stats.to_string(),
1753 boot_image,
1754 }),
1755 ..PullProgress::empty()
1756 };
1757 let _ = task_tx.send(completed);
1759 Ok(())
1760 });
1761
1762 drop(tx);
1765
1766 struct State {
1767 rx: tokio::sync::mpsc::UnboundedReceiver<PullProgress>,
1768 handle: Option<AbortOnDrop>,
1769 done: bool,
1770 }
1771
1772 let state = State {
1773 rx,
1774 handle: Some(AbortOnDrop {
1775 handle: Some(handle),
1776 }),
1777 done: false,
1778 };
1779
1780 let stream = stream::unfold(state, |mut state| async move {
1781 if state.done {
1782 return None;
1783 }
1784 match state.rx.recv().await {
1785 Some(frame) => {
1786 let is_completed = frame.completed.is_some();
1787 if is_completed {
1788 state.done = true;
1789 if let Some(guard) = state.handle.as_mut() {
1792 let _ = guard.take();
1793 }
1794 }
1795 let reply = zlink::Reply::new(Some(frame)).set_continues(Some(!is_completed));
1796 Some((Ok(reply), state))
1797 }
1798 None => {
1799 state.done = true;
1802 let join = state.handle.as_mut().and_then(AbortOnDrop::take);
1805 let err = match join {
1806 Some(join) => match join.await {
1807 Ok(Ok(())) => OciError::InternalError {
1808 message: "pull completed without a result frame".to_string(),
1809 },
1810 Ok(Err(e)) => e,
1811 Err(_) => OciError::InternalError {
1812 message: "pull task panicked".to_string(),
1813 },
1814 },
1815 None => OciError::InternalError {
1816 message: "pull task panicked".to_string(),
1817 },
1818 };
1819 Some((Err(err), state))
1820 }
1821 }
1822 });
1823
1824 Box::pin(stream)
1825 }
1826
1827 #[derive(Debug, zlink::ReplyError, zlink::introspect::ReplyError)]
1829 #[zlink(interface = "org.composefs.Oci")]
1830 pub enum OciError {
1831 RepoNotFound {
1833 message: String,
1835 },
1836 InvalidHandle {
1838 handle: u64,
1840 },
1841 NoSuchImage {
1843 image: String,
1845 },
1846 InternalError {
1848 message: String,
1850 },
1851 }
1852}
1853
1854pub mod proxy {
1860 #![allow(missing_docs)]
1861
1862 #[cfg(feature = "oci")]
1863 use super::oci::{
1864 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
1865 };
1866 use super::{
1867 FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply, OpenRepositoryReply,
1868 RepositoryError,
1869 };
1870 #[cfg(feature = "oci")]
1871 use zlink::futures_util::Stream;
1872
1873 #[zlink::proxy(interface = "org.composefs.Repository")]
1875 pub trait RepositoryProxy {
1876 async fn init_repository(
1878 &mut self,
1879 path: &str,
1880 algorithm: Option<&str>,
1881 insecure: Option<bool>,
1882 ) -> zlink::Result<Result<InitRepositoryReply, RepositoryError>>;
1883
1884 async fn open_repository(
1886 &mut self,
1887 path: Option<&str>,
1888 user: Option<bool>,
1889 system: Option<bool>,
1890 ) -> zlink::Result<Result<OpenRepositoryReply, RepositoryError>>;
1891
1892 async fn close_repository(
1894 &mut self,
1895 handle: u64,
1896 ) -> zlink::Result<Result<(), RepositoryError>>;
1897
1898 async fn fsck(
1900 &mut self,
1901 handle: u64,
1902 metadata_only: Option<bool>,
1903 ) -> zlink::Result<Result<FsckReply, RepositoryError>>;
1904
1905 async fn gc(
1907 &mut self,
1908 handle: u64,
1909 dry_run: bool,
1910 roots: Vec<String>,
1911 ) -> zlink::Result<Result<GcReply, RepositoryError>>;
1912
1913 async fn image_objects(
1915 &mut self,
1916 handle: u64,
1917 name: &str,
1918 ) -> zlink::Result<Result<ImageObjectsReply, RepositoryError>>;
1919 }
1920
1921 #[cfg(feature = "oci")]
1923 #[zlink::proxy(interface = "org.composefs.Oci")]
1924 pub trait OciProxy {
1925 async fn list_images(
1927 &mut self,
1928 handle: u64,
1929 filter: Option<&str>,
1930 ) -> zlink::Result<Result<ListImagesReply, OciError>>;
1931
1932 #[zlink(rename = "Check")]
1934 async fn oci_fsck(
1935 &mut self,
1936 handle: u64,
1937 image: Option<&str>,
1938 ) -> zlink::Result<Result<OciFsckReply, OciError>>;
1939
1940 async fn inspect(
1942 &mut self,
1943 handle: u64,
1944 image: &str,
1945 ) -> zlink::Result<Result<OciInspectReply, OciError>>;
1946
1947 async fn tag(
1949 &mut self,
1950 handle: u64,
1951 manifest_digest: &str,
1952 name: &str,
1953 ) -> zlink::Result<Result<(), OciError>>;
1954
1955 async fn untag(&mut self, handle: u64, name: &str) -> zlink::Result<Result<(), OciError>>;
1957
1958 async fn compute_id(
1960 &mut self,
1961 handle: u64,
1962 image: &str,
1963 verity: Option<&str>,
1964 bootable: bool,
1965 ) -> zlink::Result<Result<OciComputeIdReply, OciError>>;
1966
1967 #[zlink(more, rename = "Pull")]
1969 async fn pull(
1970 &mut self,
1971 handle: u64,
1972 image: &str,
1973 name: Option<&str>,
1974 local_fetch: &str,
1975 storage_root: Option<&str>,
1976 bootable: bool,
1977 ) -> zlink::Result<impl Stream<Item = zlink::Result<Result<PullProgress, OciError>>>>;
1978 }
1979}
1980
1981#[cfg(feature = "oci")]
1982pub(crate) use oci::*;