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)]
159pub struct OpenRepositoryReply {
160 pub handle: u64,
162
163 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub hash_algorithm: Option<String>,
169
170 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub objects_device_id: Option<u64>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
181pub struct InitRepositoryReply {
182 pub created: bool,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
189pub struct EnsureRepositoryReply {
190 pub status: composefs::repository::EnsureStatus,
192}
193
194#[derive(Debug, Clone)]
199pub(crate) enum OpenRepo {
200 Sha256(Arc<Repository<Sha256HashValue>>),
202 Sha512(Arc<Repository<Sha512HashValue>>),
204}
205
206impl OpenRepo {
207 fn hash_algorithm(&self) -> &'static str {
209 match self {
210 OpenRepo::Sha256(_) => "sha256",
211 OpenRepo::Sha512(_) => "sha512",
212 }
213 }
214
215 fn objects_device_id(&self) -> Option<u64> {
217 let stat_it = |fd: &std::os::fd::OwnedFd| -> Option<u64> {
218 rustix::fs::fstat(fd).ok().map(|s| s.st_dev)
219 };
220 match self {
221 OpenRepo::Sha256(r) => r.objects_dir().ok().and_then(stat_it),
222 OpenRepo::Sha512(r) => r.objects_dir().ok().and_then(stat_it),
223 }
224 }
225}
226
227#[derive(Debug)]
229struct HandleEntry {
230 repo: OpenRepo,
232 #[allow(dead_code)]
236 owner: Option<usize>,
237}
238
239#[derive(Debug, Clone)]
241struct OpenOptions {
242 insecure: bool,
244 require_verity: bool,
246 no_upgrade: bool,
248}
249
250impl OpenOptions {
251 fn from_app(args: &App) -> Self {
253 Self {
254 insecure: args.insecure,
255 require_verity: args.require_verity,
256 no_upgrade: args.no_upgrade,
257 }
258 }
259}
260
261impl Default for OpenOptions {
262 fn default() -> Self {
265 Self {
266 insecure: false,
267 require_verity: false,
268 no_upgrade: false,
269 }
270 }
271}
272
273#[derive(Debug)]
280pub(crate) struct CfsctlService {
281 repos: HashMap<u64, HandleEntry>,
283 next_handle: u64,
285 open_opts: OpenOptions,
287}
288
289impl Default for CfsctlService {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295impl CfsctlService {
296 fn with_open_opts(open_opts: OpenOptions) -> Self {
302 Self {
303 repos: HashMap::new(),
304 next_handle: 0,
305 open_opts,
306 }
307 }
308
309 pub(crate) fn from_app(args: &App) -> Self {
316 Self::with_open_opts(OpenOptions::from_app(args))
317 }
318
319 pub(crate) fn activated() -> Self {
323 Self::with_open_opts(OpenOptions::default())
324 }
325
326 pub(crate) fn new() -> Self {
328 Self::with_open_opts(OpenOptions::default())
329 }
330
331 #[cfg(test)]
336 pub(crate) fn insecure_for_test() -> Self {
337 Self::with_open_opts(OpenOptions {
338 insecure: true,
339 require_verity: false,
340 no_upgrade: false,
341 })
342 }
343
344 fn next_handle(&mut self) -> u64 {
346 self.next_handle += 1;
347 self.next_handle
348 }
349
350 fn lookup_repo(&self, handle: u64) -> std::result::Result<OpenRepo, RepositoryError> {
355 self.repos
356 .get(&handle)
357 .map(|entry| entry.repo.clone())
358 .ok_or(RepositoryError::InvalidHandle { handle })
359 }
360
361 #[cfg(feature = "oci")]
366 fn lookup_oci(&self, handle: u64) -> std::result::Result<OpenRepo, oci::OciError> {
367 self.repos
368 .get(&handle)
369 .map(|entry| entry.repo.clone())
370 .ok_or(oci::OciError::InvalidHandle { handle })
371 }
372
373 fn do_open(
380 &mut self,
381 path: &Path,
382 owner: Option<usize>,
383 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
384 let hash_type = resolve_hash_type(path, None, !self.open_opts.no_upgrade).map_err(|e| {
385 RepositoryError::RepoNotFound {
386 message: format!("{e:#}"),
387 }
388 })?;
389 let repo = match hash_type {
390 HashType::Sha256 => OpenRepo::Sha256(Arc::new(
391 open_repo_at::<Sha256HashValue>(
392 path,
393 self.open_opts.insecure,
394 self.open_opts.require_verity,
395 self.open_opts.no_upgrade,
396 )
397 .map_err(|e| RepositoryError::RepoNotFound {
398 message: format!("{e:#}"),
399 })?,
400 )),
401 HashType::Sha512 => OpenRepo::Sha512(Arc::new(
402 open_repo_at::<Sha512HashValue>(
403 path,
404 self.open_opts.insecure,
405 self.open_opts.require_verity,
406 self.open_opts.no_upgrade,
407 )
408 .map_err(|e| RepositoryError::RepoNotFound {
409 message: format!("{e:#}"),
410 })?,
411 )),
412 };
413 let handle = self.next_handle();
414 let hash_algorithm = Some(repo.hash_algorithm().to_string());
415 let objects_device_id = repo.objects_device_id();
416 self.repos.insert(handle, HandleEntry { repo, owner });
417 Ok(OpenRepositoryReply {
418 handle,
419 hash_algorithm,
420 objects_device_id,
421 })
422 }
423
424 fn resolve_selector(
429 path: Option<String>,
430 user: Option<bool>,
431 system: Option<bool>,
432 ) -> std::result::Result<PathBuf, RepositoryError> {
433 let user = user.unwrap_or(false);
434 let system = system.unwrap_or(false);
435 match (path, user, system) {
436 (Some(p), false, false) => Ok(PathBuf::from(p)),
437 (None, true, false) => user_path().map_err(|e| RepositoryError::InvalidSpec {
438 message: format!("{e:#}"),
439 }),
440 (None, false, true) => Ok(system_path()),
441 _ => Err(RepositoryError::InvalidSpec {
442 message: "exactly one of `path`, `user`, `system` must be set".into(),
443 }),
444 }
445 }
446}
447
448async fn run_fsck<ObjectID: FsVerityHashValue>(
450 repo: &Repository<ObjectID>,
451 metadata_only: bool,
452) -> std::result::Result<FsckResult, RepositoryError> {
453 let result = if metadata_only {
454 repo.fsck_metadata_only().await
455 } else {
456 repo.fsck().await
457 };
458 result.map_err(|e| RepositoryError::InternalError {
459 message: format!("{e:#}"),
460 })
461}
462
463async fn run_gc<ObjectID: FsVerityHashValue>(
465 repo: &Repository<ObjectID>,
466 dry_run: bool,
467 roots: Vec<String>,
468) -> std::result::Result<GcReply, RepositoryError> {
469 let root_refs: Vec<&str> = roots.iter().map(String::as_str).collect();
470 let result = if dry_run {
471 repo.gc_dry_run(&root_refs)
472 } else {
473 repo.gc(&root_refs)
474 }
475 .map_err(|e| RepositoryError::InternalError {
476 message: format!("{e:#}"),
477 })?;
478 Ok(GcReply { result, dry_run })
479}
480
481async fn run_image_objects<ObjectID: FsVerityHashValue>(
483 repo: &Repository<ObjectID>,
484 name: String,
485) -> std::result::Result<ImageObjectsReply, RepositoryError> {
486 let objects = repo.objects_for_image(&name).map_err(|e| {
487 if let Some(nf) = e.downcast_ref::<composefs::ImageNotFound>() {
488 RepositoryError::NoSuchRef {
489 reference: nf.name.clone(),
490 }
491 } else {
492 RepositoryError::InternalError {
493 message: format!("{e:#}"),
494 }
495 }
496 })?;
497 let mut object_ids: Vec<String> = objects.iter().map(|id| id.to_id()).collect();
498 object_ids.sort();
499 Ok(ImageObjectsReply { object_ids })
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
504pub struct ImageRefEntry {
505 pub name: String,
507 pub digest: String,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
513pub struct ListImageRefsReply {
514 pub images: Vec<ImageRefEntry>,
516}
517
518pub fn run_list_image_refs<ObjectID: FsVerityHashValue>(
520 repo: &Repository<ObjectID>,
521) -> std::result::Result<ListImageRefsReply, RepositoryError> {
522 let refs = repo
523 .list_image_refs("")
524 .map_err(|e| RepositoryError::InternalError {
525 message: format!("{e:#}"),
526 })?;
527 let images = refs
528 .into_iter()
529 .map(|(name, target)| {
530 let digest = target.rsplit('/').next().unwrap_or(&target).to_string();
531 ImageRefEntry { name, digest }
532 })
533 .collect();
534 Ok(ListImageRefsReply { images })
535}
536
537#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, zlink::introspect::Type)]
541pub struct MountParams {
542 pub overlay: Option<bool>,
545 pub read_write: Option<bool>,
547}
548
549impl MountParams {
550 fn to_mount_options(
552 &self,
553 fds: Vec<std::os::fd::OwnedFd>,
554 ) -> std::result::Result<composefs::mount::MountOptions, RepositoryError> {
555 let overlay = self.overlay.unwrap_or(false);
556
557 let mut expected_fds = 0;
558 if overlay {
559 expected_fds += 2;
560 }
561
562 if fds.len() != expected_fds {
563 return Err(RepositoryError::InvalidSpec {
564 message: format!(
565 "Mount expects {expected_fds} fds for the requested options, got {}",
566 fds.len()
567 ),
568 });
569 }
570
571 let mut options = composefs::mount::MountOptions::default();
572 let mut fd_iter = fds.into_iter();
573 if overlay {
574 let upperdir = fd_iter.next().unwrap();
575 let workdir = fd_iter.next().unwrap();
576 options.set_overlay(upperdir, workdir);
577 }
578 options.set_read_write(self.read_write.unwrap_or(false));
579
580 Ok(options)
581 }
582}
583
584#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, zlink::introspect::Type)]
586pub struct MountReply {
587 pub fd_index: u32,
589}
590
591fn run_mount<ObjectID: FsVerityHashValue>(
592 repo: &Repository<ObjectID>,
593 name: &str,
594 params: &MountParams,
595 fds: Vec<std::os::fd::OwnedFd>,
596) -> std::result::Result<(MountReply, Vec<std::os::fd::OwnedFd>), RepositoryError> {
597 let options = params.to_mount_options(fds)?;
598
599 let mount_fd =
600 repo.mount_with_options(name, &options)
601 .map_err(|e| RepositoryError::InternalError {
602 message: format!("{e:#}"),
603 })?;
604
605 Ok((MountReply { fd_index: 0 }, vec![mount_fd]))
606}
607
608#[cfg(feature = "oci")]
609fn run_oci_mount<ObjectID: composefs::fsverity::FsVerityHashValue>(
610 repo: &Repository<ObjectID>,
611 image: &str,
612 bootable: bool,
613 params: &MountParams,
614 fds: Vec<std::os::fd::OwnedFd>,
615) -> std::result::Result<(MountReply, Vec<std::os::fd::OwnedFd>), oci::OciError> {
616 let img = if image.starts_with("sha256:") {
617 let digest: composefs_oci::OciDigest =
618 image.parse().map_err(|e| oci::OciError::InternalError {
619 message: format!("Invalid manifest digest: {e}"),
620 })?;
621 composefs_oci::OciImage::open(repo, &digest, None)
622 } else {
623 composefs_oci::OciImage::open_ref(repo, image)
624 }
625 .map_err(|e| oci::OciError::NoSuchImage {
626 image: format!("{image}: {e:#}"),
627 })?;
628
629 let erofs_id = if bootable {
630 img.boot_image_ref(repo.erofs_version())
631 } else {
632 img.image_ref(repo.erofs_version())
633 }
634 .ok_or_else(|| oci::OciError::InternalError {
635 message: if bootable {
636 "No boot EROFS image linked".into()
637 } else {
638 "No composefs EROFS image linked".into()
639 },
640 })?;
641
642 let options = params
643 .to_mount_options(fds)
644 .map_err(|e| oci::OciError::InternalError {
645 message: format!("{e:?}"),
646 })?;
647 let mount_fd = repo
648 .mount_with_options(&erofs_id.to_hex(), &options)
649 .map_err(|e| oci::OciError::InternalError {
650 message: format!("{e:#}"),
651 })?;
652
653 Ok((MountReply { fd_index: 0 }, vec![mount_fd]))
654}
655
656fn parse_algorithm(algorithm: Option<&str>) -> std::result::Result<Algorithm, RepositoryError> {
659 match algorithm {
660 Some(s) => s.parse().map_err(|e| RepositoryError::InvalidSpec {
661 message: format!("invalid algorithm: {e}"),
662 }),
663 None => Ok(Algorithm::SHA512),
664 }
665}
666
667fn run_init_repository(
674 path: &Path,
675 algorithm: Algorithm,
676 insecure: bool,
677) -> std::result::Result<InitRepositoryReply, RepositoryError> {
678 if let Some(parent) = path.parent() {
680 std::fs::create_dir_all(parent).map_err(|e| RepositoryError::InternalError {
681 message: format!("creating parent directories for {}: {e:#}", path.display()),
682 })?;
683 }
684
685 let created = match algorithm {
686 Algorithm::Sha256 { .. } => {
687 let config = if insecure {
688 RepositoryConfig::new(algorithm).set_insecure()
689 } else {
690 RepositoryConfig::new(algorithm)
691 };
692 Repository::<Sha256HashValue>::init_path(CWD, path, config)
693 .map_err(|e| RepositoryError::InternalError {
694 message: format!("{e:#}"),
695 })?
696 .1
697 }
698 Algorithm::Sha512 { .. } => {
699 let config = if insecure {
700 RepositoryConfig::new(algorithm).set_insecure()
701 } else {
702 RepositoryConfig::new(algorithm)
703 };
704 Repository::<Sha512HashValue>::init_path(CWD, path, config)
705 .map_err(|e| RepositoryError::InternalError {
706 message: format!("{e:#}"),
707 })?
708 .1
709 }
710 };
711 Ok(InitRepositoryReply { created })
712}
713
714pub(crate) fn run_ensure_repository(
722 path: &Path,
723 algorithm: Algorithm,
724 insecure: bool,
725 erofs_formats: Option<composefs::erofs::format::FormatConfig>,
726) -> anyhow::Result<composefs::repository::EnsureStatus> {
727 if let Some(parent) = path.parent() {
728 std::fs::create_dir_all(parent)
729 .with_context(|| format!("creating parent directories for {}", path.display()))?;
730 }
731
732 let mut config = RepositoryConfig::new(algorithm);
733 if insecure {
734 config = config.set_insecure();
735 }
736 if let Some(formats) = erofs_formats {
737 config.erofs_formats = formats;
738 }
739
740 let status = match algorithm {
741 Algorithm::Sha256 { .. } => {
742 Repository::<Sha256HashValue>::ensure_path(CWD, path, config)?.1
743 }
744 Algorithm::Sha512 { .. } => {
745 Repository::<Sha512HashValue>::ensure_path(CWD, path, config)?.1
746 }
747 };
748 Ok(status)
749}
750
751#[cfg(feature = "oci")]
754async fn run_list_images<ObjectID: FsVerityHashValue>(
755 repo: &Repository<ObjectID>,
756 filter: Option<String>,
757) -> std::result::Result<Vec<oci::ImageEntry>, oci::OciError> {
758 composefs_oci::oci_image::list_images(repo)
759 .map(|imgs| {
760 imgs.iter()
761 .filter(|img| match &filter {
762 Some(needle) => img.name.contains(needle.as_str()),
763 None => true,
764 })
765 .map(oci::ImageEntry::from)
766 .collect()
767 })
768 .map_err(|e| oci::OciError::InternalError {
769 message: format!("{e:#}"),
770 })
771}
772
773#[cfg(feature = "oci")]
778async fn run_oci_fsck<ObjectID: FsVerityHashValue>(
779 repo: &Repository<ObjectID>,
780 image: Option<String>,
781) -> std::result::Result<oci::OciFsckReply, oci::OciError> {
782 let result = match image {
783 Some(name) => composefs_oci::oci_fsck_image(repo, &name).await,
784 None => composefs_oci::oci_fsck(repo).await,
785 }
786 .map_err(|e| oci::OciError::InternalError {
787 message: format!("{e:#}"),
788 })?;
789 Ok(oci::OciFsckReply::from(&result))
790}
791
792#[cfg(feature = "oci")]
794async fn run_inspect<ObjectID: FsVerityHashValue>(
795 repo: &Repository<ObjectID>,
796 image: String,
797) -> std::result::Result<oci::OciInspectReply, oci::OciError> {
798 let reference: crate::OciReference =
799 image.parse().map_err(|e| oci::OciError::InternalError {
800 message: format!("invalid image reference: {e:#}"),
801 })?;
802 let img = crate::resolve_oci_image(repo, &reference).map_err(|e| {
803 if let Some(nf) = e.downcast_ref::<composefs_oci::OciRefNotFound>() {
804 oci::OciError::NoSuchImage {
805 image: nf.name.clone(),
806 }
807 } else if let Some(nf) = e.downcast_ref::<composefs_oci::OciImageNotFound>() {
808 oci::OciError::NoSuchImage {
809 image: nf.digest.clone(),
810 }
811 } else {
812 oci::OciError::InternalError {
813 message: format!("{e:#}"),
814 }
815 }
816 })?;
817
818 oci::OciInspectReply::from_image(repo, &img).map_err(|e| oci::OciError::InternalError {
819 message: format!("{e:#}"),
820 })
821}
822
823#[cfg(feature = "oci")]
825async fn run_tag<ObjectID: FsVerityHashValue>(
826 repo: &Repository<ObjectID>,
827 manifest_digest: String,
828 name: String,
829) -> std::result::Result<(), oci::OciError> {
830 let digest: composefs_oci::OciDigest =
831 manifest_digest
832 .parse()
833 .map_err(|e| oci::OciError::InternalError {
834 message: format!("invalid digest: {e}"),
835 })?;
836 composefs_oci::oci_image::tag_image(repo, &digest, &name).map_err(|e| {
837 oci::OciError::InternalError {
838 message: format!("{e:#}"),
839 }
840 })
841}
842
843#[cfg(feature = "oci")]
845async fn run_untag<ObjectID: FsVerityHashValue>(
846 repo: &Repository<ObjectID>,
847 name: String,
848) -> std::result::Result<(), oci::OciError> {
849 composefs_oci::oci_image::untag_image(repo, &name).map_err(|e| oci::OciError::InternalError {
850 message: format!("{e:#}"),
851 })
852}
853
854#[cfg(feature = "oci")]
860async fn run_compute_id<ObjectID: FsVerityHashValue>(
861 repo: &Repository<ObjectID>,
862 image: String,
863 verity: Option<String>,
864 bootable: bool,
865 xattrs: Option<composefs_oci::XattrFiltering>,
866) -> std::result::Result<oci::OciComputeIdReply, oci::OciError> {
867 let reference: crate::OciReference =
868 image.parse().map_err(|e| oci::OciError::InternalError {
869 message: format!("invalid image reference: {e:#}"),
870 })?;
871 let verity_override =
872 crate::verity_opt::<ObjectID>(&verity).map_err(|e| oci::OciError::InternalError {
873 message: format!("invalid verity: {e:#}"),
874 })?;
875 let (config_digest, config_verity) =
876 crate::resolve_oci_config(repo, &reference, verity_override).map_err(|e| {
877 oci::OciError::InternalError {
878 message: format!("{e:#}"),
879 }
880 })?;
881
882 let transform_opts = composefs_oci::OciTransformOptions {
883 xattrs: xattrs.unwrap_or_default(),
884 };
885 let mut fs = composefs_oci::image::create_filesystem(
886 repo,
887 &config_digest,
888 config_verity.as_ref(),
889 &transform_opts,
890 )
891 .map_err(|e| oci::OciError::InternalError {
892 message: format!("{e:#}"),
893 })?;
894 if bootable {
895 use composefs_boot::BootOps as _;
896 fs.transform_for_boot(repo)
897 .map_err(|e| oci::OciError::InternalError {
898 message: format!("{e:#}"),
899 })?;
900 }
901 let id = fs.compute_image_id(repo.erofs_version());
902 Ok(oci::OciComputeIdReply {
903 image_id: id.to_hex(),
904 })
905}
906
907#[cfg(not(feature = "oci"))]
925mod service_impl {
926 #![allow(missing_docs)]
927
928 use super::{
929 CfsctlService, EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply,
930 InitRepositoryReply, ListImageRefsReply, MountParams, MountReply, OpenRepo,
931 OpenRepositoryReply, RepositoryError, parse_algorithm, run_ensure_repository, run_fsck,
932 run_gc, run_image_objects, run_init_repository, run_list_image_refs, run_mount,
933 };
934 use composefs::fsverity::{Sha256HashValue, Sha512HashValue};
935
936 #[zlink::service(
937 interface = "org.composefs.Repository",
938 vendor = "org.composefs",
939 product = "cfsctl",
940 version = env!("CARGO_PKG_VERSION"),
941 url = "https://github.com/composefs/composefs-rs"
942 )]
943 impl<Sock> CfsctlService {
944 async fn init_repository(
954 &mut self,
955 path: String,
956 algorithm: Option<String>,
957 insecure: Option<bool>,
958 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
959 let algorithm = parse_algorithm(algorithm.as_deref())?;
960 let insecure = insecure.unwrap_or(self.open_opts.insecure);
961 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
962 }
963
964 async fn ensure_repository(
976 &mut self,
977 path: String,
978 algorithm: Option<String>,
979 insecure: Option<bool>,
980 ) -> std::result::Result<EnsureRepositoryReply, RepositoryError> {
981 let algorithm = parse_algorithm(algorithm.as_deref())?;
982 let insecure = insecure.unwrap_or(self.open_opts.insecure);
983 let status =
984 run_ensure_repository(std::path::Path::new(&path), algorithm, insecure, None)
985 .map_err(|e| RepositoryError::InternalError {
986 message: format!("{e:#}"),
987 })?;
988 Ok(EnsureRepositoryReply { status })
989 }
990
991 async fn open_repository(
995 &mut self,
996 path: Option<String>,
997 user: Option<bool>,
998 system: Option<bool>,
999 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
1000 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
1001 let selected = Self::resolve_selector(path, user, system)?;
1002 self.do_open(&selected, Some(conn.id()))
1003 }
1004
1005 async fn close_repository(
1007 &mut self,
1008 handle: u64,
1009 ) -> std::result::Result<(), RepositoryError> {
1010 self.repos
1011 .remove(&handle)
1012 .map(|_| ())
1013 .ok_or(RepositoryError::InvalidHandle { handle })
1014 }
1015
1016 async fn fsck(
1022 &self,
1023 handle: u64,
1024 metadata_only: Option<bool>,
1025 ) -> std::result::Result<FsckReply, RepositoryError> {
1026 let metadata_only = metadata_only.unwrap_or(false);
1027 let result = match self.lookup_repo(handle)? {
1028 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
1029 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
1030 }?;
1031 Ok(FsckReply::from(&result))
1032 }
1033
1034 async fn gc(
1036 &self,
1037 handle: u64,
1038 dry_run: bool,
1039 roots: Vec<String>,
1040 ) -> std::result::Result<GcReply, RepositoryError> {
1041 match self.lookup_repo(handle)? {
1042 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
1043 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
1044 }
1045 }
1046
1047 async fn image_objects(
1049 &self,
1050 handle: u64,
1051 name: String,
1052 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
1053 match self.lookup_repo(handle)? {
1054 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
1055 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
1056 }
1057 }
1058
1059 async fn list_image_refs(
1061 &self,
1062 handle: u64,
1063 ) -> std::result::Result<ListImageRefsReply, RepositoryError> {
1064 match self.lookup_repo(handle)? {
1065 OpenRepo::Sha256(ref r) => run_list_image_refs::<Sha256HashValue>(r),
1066 OpenRepo::Sha512(ref r) => run_list_image_refs::<Sha512HashValue>(r),
1067 }
1068 }
1069
1070 #[zlink(return_fds)]
1076 async fn mount(
1077 &self,
1078 handle: u64,
1079 name: String,
1080 options: MountParams,
1081 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1082 ) -> (
1083 std::result::Result<MountReply, RepositoryError>,
1084 Vec<std::os::fd::OwnedFd>,
1085 ) {
1086 let result = match self.lookup_repo(handle) {
1087 Ok(OpenRepo::Sha256(ref r)) => {
1088 run_mount::<Sha256HashValue>(r, &name, &options, fds)
1089 }
1090 Ok(OpenRepo::Sha512(ref r)) => {
1091 run_mount::<Sha512HashValue>(r, &name, &options, fds)
1092 }
1093 Err(e) => Err(e),
1094 };
1095 match result {
1096 Ok((reply, fds)) => (Ok(reply), fds),
1097 Err(e) => (Err(e), vec![]),
1098 }
1099 }
1100 }
1101}
1102
1103#[cfg(feature = "oci")]
1108mod service_impl {
1109 #![allow(missing_docs)]
1110
1111 use super::layer_sync::{
1112 FinalizeImageReply, GetInfoReply, GetLayerReply, HasLayerReply, LayerRef, PutLayerReply,
1113 };
1114 use super::oci::{
1115 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
1116 parse_local_fetch, pull_stream,
1117 };
1118 use super::{
1119 CfsctlService, EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply,
1120 InitRepositoryReply, ListImageRefsReply, MountParams, MountReply, OpenRepo,
1121 OpenRepositoryReply, RepositoryError, parse_algorithm, run_compute_id,
1122 run_ensure_repository, run_fsck, run_gc, run_image_objects, run_init_repository,
1123 run_inspect, run_list_image_refs, run_list_images, run_mount, run_oci_fsck, run_oci_mount,
1124 run_tag, run_untag,
1125 };
1126 use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
1127 use composefs_oci::layer_transport::{RepoLayerSource, serve_get_layer};
1128 use composefs_oci::varlink_types::GetLayerParams;
1129 use composefs_splitdirfdstream::seed_from_id;
1130
1131 #[zlink::service(
1132 interface = "org.composefs.Repository",
1133 vendor = "org.composefs",
1134 product = "cfsctl",
1135 version = env!("CARGO_PKG_VERSION"),
1136 url = "https://github.com/composefs/composefs-rs"
1137 )]
1138 impl<Sock> CfsctlService {
1139 async fn init_repository(
1151 &mut self,
1152 path: String,
1153 algorithm: Option<String>,
1154 insecure: Option<bool>,
1155 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
1156 let algorithm = parse_algorithm(algorithm.as_deref())?;
1157 let insecure = insecure.unwrap_or(self.open_opts.insecure);
1158 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
1159 }
1160
1161 async fn ensure_repository(
1173 &mut self,
1174 path: String,
1175 algorithm: Option<String>,
1176 insecure: Option<bool>,
1177 ) -> std::result::Result<EnsureRepositoryReply, RepositoryError> {
1178 let algorithm = parse_algorithm(algorithm.as_deref())?;
1179 let insecure = insecure.unwrap_or(self.open_opts.insecure);
1180 let status =
1181 run_ensure_repository(std::path::Path::new(&path), algorithm, insecure, None)
1182 .map_err(|e| RepositoryError::InternalError {
1183 message: format!("{e:#}"),
1184 })?;
1185 Ok(EnsureRepositoryReply { status })
1186 }
1187
1188 async fn open_repository(
1192 &mut self,
1193 path: Option<String>,
1194 user: Option<bool>,
1195 system: Option<bool>,
1196 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
1197 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
1198 let selected = Self::resolve_selector(path, user, system)?;
1199 self.do_open(&selected, Some(conn.id()))
1200 }
1201
1202 async fn close_repository(
1204 &mut self,
1205 handle: u64,
1206 ) -> std::result::Result<(), RepositoryError> {
1207 self.repos
1208 .remove(&handle)
1209 .map(|_| ())
1210 .ok_or(RepositoryError::InvalidHandle { handle })
1211 }
1212
1213 async fn fsck(
1219 &self,
1220 handle: u64,
1221 metadata_only: Option<bool>,
1222 ) -> std::result::Result<FsckReply, RepositoryError> {
1223 let metadata_only = metadata_only.unwrap_or(false);
1224 let result = match self.lookup_repo(handle)? {
1225 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
1226 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
1227 }?;
1228 Ok(FsckReply::from(&result))
1229 }
1230
1231 async fn gc(
1233 &self,
1234 handle: u64,
1235 dry_run: bool,
1236 roots: Vec<String>,
1237 ) -> std::result::Result<GcReply, RepositoryError> {
1238 match self.lookup_repo(handle)? {
1239 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
1240 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
1241 }
1242 }
1243
1244 async fn image_objects(
1246 &self,
1247 handle: u64,
1248 name: String,
1249 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
1250 match self.lookup_repo(handle)? {
1251 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
1252 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
1253 }
1254 }
1255
1256 async fn list_image_refs(
1258 &self,
1259 handle: u64,
1260 ) -> std::result::Result<ListImageRefsReply, RepositoryError> {
1261 match self.lookup_repo(handle)? {
1262 OpenRepo::Sha256(ref r) => run_list_image_refs::<Sha256HashValue>(r),
1263 OpenRepo::Sha512(ref r) => run_list_image_refs::<Sha512HashValue>(r),
1264 }
1265 }
1266
1267 #[zlink(return_fds)]
1273 async fn mount(
1274 &self,
1275 handle: u64,
1276 name: String,
1277 options: MountParams,
1278 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1279 ) -> (
1280 std::result::Result<MountReply, RepositoryError>,
1281 Vec<std::os::fd::OwnedFd>,
1282 ) {
1283 let result = match self.lookup_repo(handle) {
1284 Ok(OpenRepo::Sha256(ref r)) => {
1285 run_mount::<Sha256HashValue>(r, &name, &options, fds)
1286 }
1287 Ok(OpenRepo::Sha512(ref r)) => {
1288 run_mount::<Sha512HashValue>(r, &name, &options, fds)
1289 }
1290 Err(e) => Err(e),
1291 };
1292 match result {
1293 Ok((reply, fds)) => (Ok(reply), fds),
1294 Err(e) => (Err(e), vec![]),
1295 }
1296 }
1297
1298 #[zlink(interface = "org.composefs.Oci")]
1309 async fn list_images(
1310 &self,
1311 handle: u64,
1312 filter: Option<String>,
1313 ) -> std::result::Result<ListImagesReply, OciError> {
1314 let images = match self.lookup_oci(handle)? {
1315 OpenRepo::Sha256(ref r) => run_list_images::<Sha256HashValue>(r, filter).await,
1316 OpenRepo::Sha512(ref r) => run_list_images::<Sha512HashValue>(r, filter).await,
1317 }?;
1318 Ok(ListImagesReply { images })
1319 }
1320
1321 #[zlink(interface = "org.composefs.Oci", rename = "Check")]
1327 async fn oci_fsck(
1328 &self,
1329 handle: u64,
1330 image: Option<String>,
1331 ) -> std::result::Result<OciFsckReply, OciError> {
1332 match self.lookup_oci(handle)? {
1333 OpenRepo::Sha256(ref r) => run_oci_fsck::<Sha256HashValue>(r, image).await,
1334 OpenRepo::Sha512(ref r) => run_oci_fsck::<Sha512HashValue>(r, image).await,
1335 }
1336 }
1337
1338 #[zlink(interface = "org.composefs.Oci")]
1340 async fn inspect(
1341 &self,
1342 handle: u64,
1343 image: String,
1344 ) -> std::result::Result<OciInspectReply, OciError> {
1345 match self.lookup_oci(handle)? {
1346 OpenRepo::Sha256(ref r) => run_inspect::<Sha256HashValue>(r, image).await,
1347 OpenRepo::Sha512(ref r) => run_inspect::<Sha512HashValue>(r, image).await,
1348 }
1349 }
1350
1351 #[zlink(interface = "org.composefs.Oci")]
1353 async fn tag(
1354 &self,
1355 handle: u64,
1356 manifest_digest: String,
1357 name: String,
1358 ) -> std::result::Result<(), OciError> {
1359 match self.lookup_oci(handle)? {
1360 OpenRepo::Sha256(ref r) => {
1361 run_tag::<Sha256HashValue>(r, manifest_digest, name).await
1362 }
1363 OpenRepo::Sha512(ref r) => {
1364 run_tag::<Sha512HashValue>(r, manifest_digest, name).await
1365 }
1366 }
1367 }
1368
1369 #[zlink(interface = "org.composefs.Oci")]
1371 async fn untag(&self, handle: u64, name: String) -> std::result::Result<(), OciError> {
1372 match self.lookup_oci(handle)? {
1373 OpenRepo::Sha256(ref r) => run_untag::<Sha256HashValue>(r, name).await,
1374 OpenRepo::Sha512(ref r) => run_untag::<Sha512HashValue>(r, name).await,
1375 }
1376 }
1377
1378 #[zlink(interface = "org.composefs.Oci")]
1380 async fn compute_id(
1381 &self,
1382 handle: u64,
1383 image: String,
1384 verity: Option<String>,
1385 bootable: bool,
1386 xattrs: Option<composefs_oci::XattrFiltering>,
1387 ) -> std::result::Result<OciComputeIdReply, OciError> {
1388 match self.lookup_oci(handle)? {
1389 OpenRepo::Sha256(ref r) => {
1390 run_compute_id::<Sha256HashValue>(r, image, verity, bootable, xattrs).await
1391 }
1392 OpenRepo::Sha512(ref r) => {
1393 run_compute_id::<Sha512HashValue>(r, image, verity, bootable, xattrs).await
1394 }
1395 }
1396 }
1397
1398 #[zlink(interface = "org.composefs.Oci", more)]
1413 #[allow(clippy::too_many_arguments)]
1414 async fn pull(
1415 &self,
1416 more: bool,
1417 handle: u64,
1418 image: String,
1419 name: Option<String>,
1420 local_fetch: String,
1421 storage_root: Option<String>,
1422 bootable: bool,
1423 xattrs: Option<composefs_oci::XattrFiltering>,
1424 expected_digest: Option<String>,
1425 ) -> impl zlink::futures_util::Stream<
1426 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
1427 > {
1428 let lf = parse_local_fetch(&local_fetch);
1429 let sr = storage_root.map(std::path::PathBuf::from);
1430 match self.repos.get(&handle).map(|entry| &entry.repo) {
1435 Some(OpenRepo::Sha256(r)) => pull_stream::<Sha256HashValue>(
1436 r.clone(),
1437 image,
1438 name,
1439 lf,
1440 sr,
1441 bootable,
1442 xattrs,
1443 expected_digest,
1444 more,
1445 ),
1446 Some(OpenRepo::Sha512(r)) => pull_stream::<Sha512HashValue>(
1447 r.clone(),
1448 image,
1449 name,
1450 lf,
1451 sr,
1452 bootable,
1453 xattrs,
1454 expected_digest,
1455 more,
1456 ),
1457 None => {
1458 use zlink::futures_util::stream;
1459 Box::pin(stream::once(async move {
1460 Err(OciError::InvalidHandle { handle })
1461 }))
1462 }
1463 }
1464 }
1465
1466 #[zlink(interface = "org.composefs.Oci", return_fds)]
1473 async fn oci_mount(
1474 &self,
1475 handle: u64,
1476 image: String,
1477 bootable: bool,
1478 options: MountParams,
1479 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1480 ) -> (
1481 std::result::Result<MountReply, OciError>,
1482 Vec<std::os::fd::OwnedFd>,
1483 ) {
1484 let result = match self.lookup_oci(handle) {
1485 Ok(OpenRepo::Sha256(ref r)) => {
1486 run_oci_mount::<Sha256HashValue>(r, &image, bootable, &options, fds)
1487 }
1488 Ok(OpenRepo::Sha512(ref r)) => {
1489 run_oci_mount::<Sha512HashValue>(r, &image, bootable, &options, fds)
1490 }
1491 Err(e) => Err(e),
1492 };
1493 match result {
1494 Ok((reply, fds)) => (Ok(reply), fds),
1495 Err(e) => (Err(e), vec![]),
1496 }
1497 }
1498
1499 #[zlink(interface = "org.composefs.Oci")]
1509 async fn get_info(&self) -> std::result::Result<GetInfoReply, OciError> {
1510 Ok(GetInfoReply {
1511 features: vec!["splitdirfdstream-v0".into()],
1512 })
1513 }
1514
1515 #[zlink(interface = "org.composefs.Oci")]
1520 async fn has_layer(
1521 &self,
1522 handle: u64,
1523 diff_id: String,
1524 ) -> std::result::Result<HasLayerReply, OciError> {
1525 let diff_id_parsed: composefs_oci::OciDigest =
1526 diff_id.parse().map_err(|e| OciError::InvalidDigest {
1527 message: format!("{e}"),
1528 })?;
1529 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1530
1531 fn check<ObjectID: FsVerityHashValue>(
1532 repo: &composefs::repository::Repository<ObjectID>,
1533 content_id: &str,
1534 ) -> std::result::Result<HasLayerReply, OciError> {
1535 match repo
1536 .has_stream(content_id)
1537 .map_err(|e| OciError::InternalError {
1538 message: format!("{e:#}"),
1539 })? {
1540 Some(verity) => Ok(HasLayerReply {
1541 present: true,
1542 layer_verity: Some(verity.to_hex()),
1543 }),
1544 None => Ok(HasLayerReply {
1545 present: false,
1546 layer_verity: None,
1547 }),
1548 }
1549 }
1550
1551 match self.lookup_oci(handle)? {
1552 OpenRepo::Sha256(ref r) => check::<Sha256HashValue>(r, &content_id),
1553 OpenRepo::Sha512(ref r) => check::<Sha512HashValue>(r, &content_id),
1554 }
1555 }
1556
1557 #[zlink(interface = "org.composefs.Oci", more, return_fds)]
1579 async fn get_layer(
1580 &self,
1581 more: bool,
1582 handle: u64,
1583 params: GetLayerParams,
1584 #[zlink(fds)] _fds: Vec<std::os::fd::OwnedFd>,
1585 ) -> impl zlink::futures_util::Stream<
1586 Item = (
1587 std::result::Result<zlink::Reply<GetLayerReply>, OciError>,
1588 Vec<std::os::fd::OwnedFd>,
1589 ),
1590 > + Unpin {
1591 use zlink::futures_util::stream::{self, StreamExt as _};
1592
1593 type StreamItem = (
1594 std::result::Result<zlink::Reply<GetLayerReply>, OciError>,
1595 Vec<std::os::fd::OwnedFd>,
1596 );
1597
1598 macro_rules! err_stream {
1599 ($e:expr) => {
1600 return stream::iter(std::iter::once::<StreamItem>((Err($e), vec![])))
1601 .left_stream()
1602 };
1603 }
1604
1605 let diff_id = match params.diff_id {
1607 Some(d) => d,
1608 None => err_stream!(OciError::InvalidRequest {
1609 message: "GetLayer: diff_id is required for the repo service".into(),
1610 }),
1611 };
1612
1613 let diff_id_parsed: composefs_oci::OciDigest = match diff_id.parse() {
1615 Ok(d) => d,
1616 Err(e) => err_stream!(OciError::InvalidDigest {
1617 message: format!("{e}"),
1618 }),
1619 };
1620 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1621
1622 fn do_serve_get_layer<ObjectID: FsVerityHashValue>(
1624 repo: &std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1625 content_id: &str,
1626 diff_id_str: &str,
1627 more: bool,
1628 ) -> std::result::Result<composefs_oci::layer_transport::GetLayerFrames, OciError>
1629 {
1630 let verity = repo
1631 .has_stream(content_id)
1632 .map_err(|e| OciError::InternalError {
1633 message: format!("{e:#}"),
1634 })?
1635 .ok_or_else(|| OciError::NoSuchLayer {
1636 diff_id: diff_id_str.to_string(),
1637 })?;
1638
1639 let seed = seed_from_id(content_id);
1640 let source = RepoLayerSource {
1641 repo: repo.clone(),
1642 layer_verity: verity,
1643 };
1644
1645 serve_get_layer(source, seed, more).map_err(|e| match e {
1646 composefs_oci::layer_transport::ServeGetLayerError::FdLimitExceeded(e) => {
1647 OciError::FdLimitExceeded {
1648 fd_count: e.fd_count as u64,
1649 max_per_frame: e.max_per_frame as u64,
1650 }
1651 }
1652 composefs_oci::layer_transport::ServeGetLayerError::Other(e) => {
1653 OciError::InternalError {
1654 message: format!("{e:#}"),
1655 }
1656 }
1657 })
1658 }
1659
1660 let frames = match self.lookup_oci(handle) {
1661 Ok(OpenRepo::Sha256(ref r)) => {
1662 do_serve_get_layer::<Sha256HashValue>(r, &content_id, &diff_id, more)
1663 }
1664 Ok(OpenRepo::Sha512(ref r)) => {
1665 do_serve_get_layer::<Sha512HashValue>(r, &content_id, &diff_id, more)
1666 }
1667 Err(e) => Err(e),
1668 };
1669
1670 let frames = match frames {
1671 Ok(f) => f,
1672 Err(e) => err_stream!(e),
1673 };
1674
1675 let dir_count = frames.dir_count;
1676 let batches = frames.batches;
1677 let n_frames = batches.len();
1678 let reply = GetLayerReply { dir_count };
1679
1680 stream::iter(batches.into_iter().enumerate().map(move |(i, batch)| {
1681 let is_last = i == n_frames - 1;
1682 (
1683 Ok(zlink::Reply::new(Some(reply.clone())).set_continues(Some(!is_last))),
1684 batch,
1685 )
1686 }))
1687 .right_stream()
1688 }
1689
1690 #[zlink(interface = "org.composefs.Oci")]
1707 async fn put_layer(
1708 &self,
1709 handle: u64,
1710 diff_id: String,
1711 zerocopy: bool,
1712 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1713 ) -> std::result::Result<PutLayerReply, OciError> {
1714 if fds.len() < 2 {
1716 return Err(OciError::InvalidRequest {
1717 message: format!(
1718 "expected at least 2 fds (1 pipe + >=1 dir fd), got {}",
1719 fds.len()
1720 ),
1721 });
1722 }
1723
1724 let diff_id_parsed: composefs_oci::OciDigest =
1725 diff_id.parse().map_err(|e| OciError::InvalidDigest {
1726 message: format!("{e}"),
1727 })?;
1728
1729 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1730
1731 let already_present = match self.lookup_oci(handle)? {
1735 OpenRepo::Sha256(ref r) => r
1736 .has_stream(&content_id)
1737 .map_err(|e| OciError::InternalError {
1738 message: format!("{e:#}"),
1739 })?
1740 .is_some(),
1741 OpenRepo::Sha512(ref r) => r
1742 .has_stream(&content_id)
1743 .map_err(|e| OciError::InternalError {
1744 message: format!("{e:#}"),
1745 })?
1746 .is_some(),
1747 };
1748
1749 let mut fds = fds;
1751 let pipe_read = fds.remove(0);
1752 let dir_fds = fds; async fn run_put_layer<ObjectID: FsVerityHashValue>(
1755 repo: std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1756 pipe_read: std::os::fd::OwnedFd,
1757 dir_fds: Vec<std::os::fd::OwnedFd>,
1758 diff_id: composefs_oci::OciDigest,
1759 zerocopy: bool,
1760 already_present: bool,
1761 ) -> std::result::Result<PutLayerReply, OciError> {
1762 tokio::task::spawn_blocking(move || {
1763 composefs_oci::layer_sync::drain_splitdirfdstream_verified(
1764 repo,
1765 pipe_read,
1766 dir_fds,
1767 &diff_id,
1768 zerocopy,
1769 composefs::repository::ImportContext::default(),
1770 )
1771 })
1772 .await
1773 .map_err(|e| OciError::InternalError {
1774 message: format!("spawn_blocking panic: {e}"),
1775 })?
1776 .map(|(verity, stats, _ctx)| PutLayerReply {
1777 layer_verity: verity.to_hex(),
1778 already_present,
1779 objects_reflinked: stats.objects_reflinked,
1780 objects_hardlinked: stats.objects_hardlinked,
1781 objects_copied: stats.objects_copied,
1782 objects_already_present: stats.objects_already_present,
1783 })
1784 .map_err(|e| match e {
1785 composefs_oci::layer_sync::VerifiedDrainError::DiffIdMismatch {
1786 expected,
1787 actual,
1788 } => OciError::DiffIdMismatch { expected, actual },
1789 composefs_oci::layer_sync::VerifiedDrainError::Other(err) => {
1790 OciError::InternalError {
1791 message: format!("{err:#}"),
1792 }
1793 }
1794 })
1795 }
1796
1797 match self.lookup_oci(handle)? {
1798 OpenRepo::Sha256(ref r) => {
1799 run_put_layer::<Sha256HashValue>(
1800 r.clone(),
1801 pipe_read,
1802 dir_fds,
1803 diff_id_parsed,
1804 zerocopy,
1805 already_present,
1806 )
1807 .await
1808 }
1809 OpenRepo::Sha512(ref r) => {
1810 run_put_layer::<Sha512HashValue>(
1811 r.clone(),
1812 pipe_read,
1813 dir_fds,
1814 diff_id_parsed,
1815 zerocopy,
1816 already_present,
1817 )
1818 .await
1819 }
1820 }
1821 }
1822
1823 #[zlink(interface = "org.composefs.Oci")]
1833 async fn finalize_image(
1834 &self,
1835 handle: u64,
1836 manifest_json: String,
1837 config_json: String,
1838 layers: Vec<LayerRef>,
1839 name: Option<String>,
1840 ) -> std::result::Result<FinalizeImageReply, OciError> {
1841 async fn run_finalize<ObjectID: FsVerityHashValue>(
1842 repo: std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1843 manifest_json: String,
1844 config_json: String,
1845 layers: Vec<LayerRef>,
1846 name: Option<String>,
1847 ) -> std::result::Result<FinalizeImageReply, OciError> {
1848 let mut layer_refs: Vec<(composefs_oci::OciDigest, ObjectID)> =
1850 Vec::with_capacity(layers.len());
1851 for lr in &layers {
1852 let diff_id: composefs_oci::OciDigest =
1853 lr.diff_id.parse().map_err(|e| OciError::InvalidDigest {
1854 message: format!("diff_id {:?}: {e}", lr.diff_id),
1855 })?;
1856 let verity = ObjectID::from_hex(&lr.layer_verity).map_err(|e| {
1857 OciError::InvalidDigest {
1858 message: format!("layer_verity {:?}: {e}", lr.layer_verity),
1859 }
1860 })?;
1861 layer_refs.push((diff_id, verity));
1862 }
1863
1864 tokio::task::spawn_blocking(move || {
1865 composefs_oci::layer_sync::finalize_oci_image(
1866 &repo,
1867 manifest_json.as_bytes(),
1868 config_json.as_bytes(),
1869 &layer_refs,
1870 name.as_deref(),
1871 )
1872 })
1873 .await
1874 .map_err(|e| OciError::InternalError {
1875 message: format!("spawn_blocking panic: {e}"),
1876 })?
1877 .map(
1878 |((manifest_digest, manifest_verity), (config_digest, config_verity))| {
1879 FinalizeImageReply {
1880 manifest_digest: manifest_digest.to_string(),
1881 manifest_verity: manifest_verity.to_hex(),
1882 config_digest: config_digest.to_string(),
1883 config_verity: config_verity.to_hex(),
1884 }
1885 },
1886 )
1887 .map_err(|e| OciError::InternalError {
1888 message: format!("{e:#}"),
1889 })
1890 }
1891
1892 match self.lookup_oci(handle)? {
1893 OpenRepo::Sha256(ref r) => {
1894 run_finalize::<Sha256HashValue>(
1895 r.clone(),
1896 manifest_json,
1897 config_json,
1898 layers,
1899 name,
1900 )
1901 .await
1902 }
1903 OpenRepo::Sha512(ref r) => {
1904 run_finalize::<Sha512HashValue>(
1905 r.clone(),
1906 manifest_json,
1907 config_json,
1908 layers,
1909 name,
1910 )
1911 .await
1912 }
1913 }
1914 }
1915 }
1916}
1917
1918#[derive(Debug)]
1925pub(crate) struct ActivatedListener {
1926 conn: Option<zlink::Connection<zlink::tokio::unix::Stream>>,
1928}
1929
1930impl zlink::Listener for ActivatedListener {
1931 type Socket = zlink::tokio::unix::Stream;
1932
1933 async fn accept(&mut self) -> zlink::Result<Option<zlink::Connection<Self::Socket>>> {
1934 match self.conn.take() {
1935 Some(conn) => Ok(Some(conn)),
1936 None => std::future::pending().await,
1937 }
1938 }
1939}
1940
1941pub(crate) enum ActivatedSocket {
1943 Connected(ActivatedListener),
1946 Listening(zlink::tokio::unix::Listener),
1949}
1950
1951#[allow(unsafe_code)]
1964pub(crate) fn try_activated_listener() -> Result<Option<ActivatedSocket>> {
1965 use std::os::fd::{FromRawFd as _, IntoRawFd as _, OwnedFd};
1966
1967 let fds = libsystemd::activation::receive_descriptors(true)
1968 .map_err(|e| anyhow::anyhow!("Failed to receive activation fds: {e}"))?;
1969
1970 let fd = match fds.into_iter().next() {
1971 Some(fd) => fd,
1972 None => return Ok(None),
1973 };
1974
1975 let owned: OwnedFd = unsafe { OwnedFd::from_raw_fd(fd.into_raw_fd()) };
1979
1980 let is_listening = rustix::net::sockopt::socket_acceptconn(&owned)
1983 .context("querying SO_ACCEPTCONN on activation fd")?;
1984
1985 if is_listening {
1986 let listener = zlink::tokio::unix::Listener::try_from(owned)
1989 .context("converting listening activation fd to zlink Listener")?;
1990 Ok(Some(ActivatedSocket::Listening(listener)))
1991 } else {
1992 let std_stream = std::os::unix::net::UnixStream::from(owned);
1995 std_stream
1996 .set_nonblocking(true)
1997 .context("setting systemd socket to non-blocking")?;
1998 let tokio_stream = tokio::net::UnixStream::from_std(std_stream)
1999 .context("converting systemd UnixStream to tokio")?;
2000 let zlink_stream =
2001 zlink::tokio::unix::Stream::try_from(tokio_stream).map_err(|e| anyhow::anyhow!(e))?;
2002 let conn = zlink::Connection::new(zlink_stream);
2003 Ok(Some(ActivatedSocket::Connected(ActivatedListener {
2004 conn: Some(conn),
2005 })))
2006 }
2007}
2008
2009pub(crate) async fn serve_activated<S>(service: S, listener: ActivatedListener) -> Result<()>
2019where
2020 S: zlink::Service<zlink::tokio::unix::Stream>,
2021{
2022 log::info!("Listening on systemd-activated socket");
2023 let server = zlink::Server::new(listener, service);
2024 tokio::task::LocalSet::new()
2025 .run_until(server.run())
2026 .await
2027 .context("running varlink server (activated)")
2028}
2029
2030pub(crate) async fn serve_on_listener<S>(
2036 service: S,
2037 listener: zlink::tokio::unix::Listener,
2038) -> Result<()>
2039where
2040 S: zlink::Service<zlink::tokio::unix::Stream>,
2041{
2042 let server = zlink::Server::new(listener, service);
2043 tokio::task::LocalSet::new()
2044 .run_until(server.run())
2045 .await
2046 .context("running varlink server")
2047}
2048
2049pub(crate) async fn serve<S>(service: S, address: Option<&Path>) -> Result<()>
2057where
2058 S: zlink::Service<zlink::tokio::unix::Stream>,
2059{
2060 match try_activated_listener()? {
2061 Some(ActivatedSocket::Connected(l)) => return serve_activated(service, l).await,
2062 Some(ActivatedSocket::Listening(listener)) => {
2063 log::info!("Listening on systemd-activated socket");
2064 return serve_on_listener(service, listener).await;
2065 }
2066 None => {}
2067 }
2068 let address = address.context("no --address given and not socket-activated")?;
2069 let listener = zlink::tokio::unix::bind(address)
2070 .with_context(|| format!("binding varlink socket at {}", address.display()))?;
2071 log::info!("Listening on {}", address.display());
2072 serve_on_listener(service, listener).await
2073}
2074
2075#[cfg(feature = "oci")]
2080pub mod oci {
2081 use super::*;
2082
2083 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2085 pub struct ImageEntry {
2086 pub name: String,
2088 pub manifest_digest: String,
2090 pub is_container: bool,
2092 pub architecture: String,
2094 pub os: String,
2096 pub created: Option<String>,
2098 pub layer_count: u64,
2100 pub referrer_count: u64,
2102 }
2103
2104 impl From<&composefs_oci::oci_image::ImageInfo> for ImageEntry {
2105 fn from(info: &composefs_oci::oci_image::ImageInfo) -> Self {
2106 Self {
2107 name: info.name.clone(),
2108 manifest_digest: info.manifest_digest.to_string(),
2109 is_container: info.is_container,
2110 architecture: info.architecture.clone(),
2111 os: info.os.clone(),
2112 created: info.created.clone(),
2113 layer_count: info.layer_count as u64,
2114 referrer_count: info.referrer_count as u64,
2115 }
2116 }
2117 }
2118
2119 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2121 pub struct ListImagesReply {
2122 pub images: Vec<ImageEntry>,
2124 }
2125
2126 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2131 pub struct OciFsckReply {
2132 pub ok: bool,
2134 pub images_checked: u64,
2136 pub images_corrupted: u64,
2138 pub errors: Vec<String>,
2140 pub repo: FsckReply,
2142 }
2143
2144 impl From<&composefs_oci::OciFsckResult> for OciFsckReply {
2145 fn from(result: &composefs_oci::OciFsckResult) -> Self {
2146 Self {
2147 ok: result.is_ok(),
2148 images_checked: result.images_checked(),
2149 images_corrupted: result.images_corrupted(),
2150 errors: result.errors().iter().map(|e| e.to_string()).collect(),
2151 repo: FsckReply::from(result.repo_result()),
2152 }
2153 }
2154 }
2155
2156 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2158 pub struct OciInspectReply {
2159 pub manifest: String,
2161 pub config: String,
2163 pub referrers: Vec<String>,
2165 pub composefs_erofs: Option<String>,
2167 pub composefs_boot_erofs: Option<String>,
2173 }
2174
2175 impl OciInspectReply {
2176 pub fn from_image<ObjectID: FsVerityHashValue>(
2179 repo: &Repository<ObjectID>,
2180 img: &composefs_oci::oci_image::OciImage<ObjectID>,
2181 ) -> anyhow::Result<Self> {
2182 let manifest = String::from_utf8(img.read_manifest_json(repo)?)
2183 .context("manifest is not valid UTF-8")?;
2184 let config = String::from_utf8(img.read_config_json(repo)?)
2185 .context("config is not valid UTF-8")?;
2186 let referrers = composefs_oci::oci_image::list_referrers(repo, img.manifest_digest())?
2187 .iter()
2188 .map(|(digest, _verity)| digest.to_string())
2189 .collect();
2190 Ok(Self {
2191 manifest,
2192 config,
2193 referrers,
2194 composefs_erofs: img.image_ref(repo.erofs_version()).map(|id| id.to_hex()),
2195 composefs_boot_erofs: img
2196 .boot_image_ref(repo.erofs_version())
2197 .map(|id| id.to_hex()),
2198 })
2199 }
2200 }
2201
2202 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2204 pub struct OciComputeIdReply {
2205 pub image_id: String,
2207 }
2208
2209 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2222 pub struct PullProgress {
2223 #[serde(skip_serializing_if = "Option::is_none", default)]
2225 pub started: Option<Started>,
2226 #[serde(skip_serializing_if = "Option::is_none", default)]
2228 pub progress: Option<Progress>,
2229 #[serde(skip_serializing_if = "Option::is_none", default)]
2231 pub skipped: Option<Skipped>,
2232 #[serde(skip_serializing_if = "Option::is_none", default)]
2234 pub done: Option<Done>,
2235 #[serde(skip_serializing_if = "Option::is_none", default)]
2237 pub message: Option<String>,
2238 #[serde(skip_serializing_if = "Option::is_none", default)]
2241 pub completed: Option<Completed>,
2242 }
2243
2244 #[derive(Debug, Clone, Copy, Serialize, Deserialize, zlink::introspect::Type)]
2246 pub enum ProgressUnit {
2247 Bytes,
2249 Items,
2251 }
2252
2253 impl From<composefs::progress::ProgressUnit> for ProgressUnit {
2254 fn from(unit: composefs::progress::ProgressUnit) -> Self {
2255 use composefs::progress::ProgressUnit as U;
2256 match unit {
2257 U::Bytes => ProgressUnit::Bytes,
2258 U::Items => ProgressUnit::Items,
2259 _ => ProgressUnit::Items,
2261 }
2262 }
2263 }
2264
2265 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2267 pub struct Started {
2268 pub id: String,
2270 pub total: Option<u64>,
2272 pub unit: ProgressUnit,
2274 }
2275
2276 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2278 pub struct Progress {
2279 pub id: String,
2281 pub fetched: u64,
2283 pub total: Option<u64>,
2285 }
2286
2287 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2289 pub struct Skipped {
2290 pub id: String,
2292 }
2293
2294 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2296 pub struct Done {
2297 pub id: String,
2299 pub transferred: u64,
2301 }
2302
2303 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2305 pub struct Completed {
2306 pub manifest_digest: String,
2308 pub config_digest: String,
2310 pub manifest_verity: String,
2312 pub config_verity: String,
2314 pub stats: String,
2316 pub boot_image: Option<String>,
2319 pub boot_image_mode: Option<String>,
2322 pub boot_image_format_version: Option<String>,
2325 }
2326
2327 impl PullProgress {
2328 fn empty() -> Self {
2331 PullProgress {
2332 started: None,
2333 progress: None,
2334 skipped: None,
2335 done: None,
2336 message: None,
2337 completed: None,
2338 }
2339 }
2340 }
2341
2342 impl From<composefs::progress::ProgressEvent> for PullProgress {
2343 fn from(event: composefs::progress::ProgressEvent) -> Self {
2347 use composefs::progress::ProgressEvent;
2348
2349 let mut p = PullProgress::empty();
2350 match event {
2351 ProgressEvent::Started { id, total, unit } => {
2352 p.started = Some(Started {
2353 id: id.into_inner(),
2354 total,
2355 unit: unit.into(),
2356 });
2357 }
2358 ProgressEvent::Progress { id, fetched, total } => {
2359 p.progress = Some(Progress {
2360 id: id.into_inner(),
2361 fetched,
2362 total,
2363 });
2364 }
2365 ProgressEvent::Skipped { id } => {
2366 p.skipped = Some(Skipped {
2367 id: id.into_inner(),
2368 });
2369 }
2370 ProgressEvent::Done { id, transferred } => {
2371 p.done = Some(Done {
2372 id: id.into_inner(),
2373 transferred,
2374 });
2375 }
2376 ProgressEvent::Message(s) => {
2377 p.message = Some(s);
2378 }
2379 other => {
2382 p.message = Some(format!("{other:?}"));
2383 }
2384 }
2385 p
2386 }
2387 }
2388
2389 struct ChannelReporter {
2392 tx: tokio::sync::mpsc::UnboundedSender<PullProgress>,
2393 }
2394
2395 impl std::fmt::Debug for ChannelReporter {
2396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2397 f.debug_struct("ChannelReporter").finish_non_exhaustive()
2398 }
2399 }
2400
2401 impl composefs::progress::ProgressReporter for ChannelReporter {
2402 fn report(&self, event: composefs::progress::ProgressEvent) {
2403 let _ = self.tx.send(PullProgress::from(event));
2406 }
2407 }
2408
2409 struct AbortOnDrop {
2415 handle: Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>>,
2416 }
2417
2418 impl AbortOnDrop {
2419 fn take(&mut self) -> Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>> {
2421 self.handle.take()
2422 }
2423 }
2424
2425 impl std::fmt::Debug for AbortOnDrop {
2426 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2427 f.debug_struct("AbortOnDrop").finish_non_exhaustive()
2428 }
2429 }
2430
2431 impl Drop for AbortOnDrop {
2432 fn drop(&mut self) {
2433 if let Some(handle) = &self.handle {
2434 handle.abort();
2435 }
2436 }
2437 }
2438
2439 pub(crate) fn parse_local_fetch(value: &str) -> composefs_oci::LocalFetchOpt {
2443 use composefs_oci::LocalFetchOpt;
2444 match value {
2445 "auto" | "if-possible" => LocalFetchOpt::IfPossible,
2446 "zerocopy" | "zero-copy" => LocalFetchOpt::ZeroCopy,
2447 _ => LocalFetchOpt::Disabled,
2448 }
2449 }
2450
2451 #[allow(clippy::too_many_arguments)]
2472 pub(crate) fn pull_stream<ObjectID: FsVerityHashValue>(
2473 repo: Arc<Repository<ObjectID>>,
2474 image: String,
2475 name: Option<String>,
2476 local_fetch: composefs_oci::LocalFetchOpt,
2477 storage_root: Option<PathBuf>,
2478 bootable: bool,
2479 xattrs: Option<composefs_oci::XattrFiltering>,
2480 expected_digest: Option<String>,
2481 more: bool,
2482 ) -> std::pin::Pin<
2483 Box<
2484 dyn zlink::futures_util::Stream<
2485 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
2486 >,
2487 >,
2488 > {
2489 use zlink::futures_util::stream;
2490
2491 if expected_digest.is_some() && !bootable {
2492 return Box::pin(stream::once(async move {
2493 Err(OciError::InvalidRequest {
2494 message: "expected_digest requires bootable".to_string(),
2495 })
2496 }));
2497 }
2498 if expected_digest.is_some() && xattrs.is_some() {
2499 return Box::pin(stream::once(async move {
2500 Err(OciError::InvalidRequest {
2501 message: "expected_digest and xattrs are mutually exclusive: \
2502 expected_digest searches every mode"
2503 .to_string(),
2504 })
2505 }));
2506 }
2507 let expected_digest = match expected_digest
2510 .map(|hex| ObjectID::from_hex(&hex).map(|id| (hex, id)))
2511 .transpose()
2512 {
2513 Ok(parsed) => parsed,
2514 Err(e) => {
2515 return Box::pin(stream::once(async move {
2516 Err(OciError::InvalidDigest {
2517 message: format!("invalid expected_digest: {e}"),
2518 })
2519 }));
2520 }
2521 };
2522
2523 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<PullProgress>();
2524 let reporter: Option<composefs::progress::SharedReporter> = if more {
2526 Some(std::sync::Arc::new(ChannelReporter { tx: tx.clone() }))
2527 } else {
2528 None
2529 };
2530
2531 let task_tx = tx.clone();
2536 let handle = tokio::task::spawn_local(async move {
2537 let opts = composefs_oci::PullOptions {
2538 local_fetch,
2539 storage_root: storage_root.as_deref(),
2540 progress: reporter,
2541 ..Default::default()
2542 };
2543 let result = composefs_oci::pull(&repo, &image, name.as_deref(), opts)
2544 .await
2545 .map_err(|e| OciError::InternalError {
2546 message: format!("{e:#}"),
2547 })?;
2548
2549 let (boot_image, boot_image_mode, boot_image_format_version) = if !bootable {
2550 (None, None, None)
2551 } else if let Some((expected_hex, expected)) = expected_digest {
2552 match composefs_oci::find_matching_boot_image(
2553 &repo,
2554 &result.manifest_digest,
2555 &expected,
2556 )
2557 .map_err(|e| OciError::InternalError {
2558 message: format!("{e:#}"),
2559 })? {
2560 composefs_oci::BootImageMatch::Found {
2561 mode,
2562 version,
2563 digest,
2564 } => (
2565 Some(digest.to_hex()),
2566 Some(mode.to_string()),
2567 Some(format!("{version:?}")),
2568 ),
2569 composefs_oci::BootImageMatch::NotFound(tried) => {
2570 return Err(OciError::BootImageMismatch {
2571 expected: expected_hex,
2572 tried: tried as u64,
2573 });
2574 }
2575 }
2576 } else {
2577 let mode = xattrs.unwrap_or_default();
2578 let transform_opts = composefs_oci::OciTransformOptions { xattrs: mode };
2579 let id = composefs_oci::generate_boot_image(
2580 &repo,
2581 &result.manifest_digest,
2582 &transform_opts,
2583 )
2584 .map_err(|e| OciError::InternalError {
2585 message: format!("{e:#}"),
2586 })?;
2587 (
2588 Some(id.to_hex()),
2589 Some(mode.to_string()),
2590 Some(format!("{:?}", repo.erofs_version())),
2591 )
2592 };
2593
2594 let completed = PullProgress {
2595 completed: Some(Completed {
2596 manifest_digest: result.manifest_digest.to_string(),
2597 config_digest: result.config_digest.to_string(),
2598 manifest_verity: result.manifest_verity.to_hex(),
2599 config_verity: result.config_verity.to_hex(),
2600 stats: result.stats.to_string(),
2601 boot_image,
2602 boot_image_mode,
2603 boot_image_format_version,
2604 }),
2605 ..PullProgress::empty()
2606 };
2607 let _ = task_tx.send(completed);
2609 Ok(())
2610 });
2611
2612 drop(tx);
2615
2616 struct State {
2617 rx: tokio::sync::mpsc::UnboundedReceiver<PullProgress>,
2618 handle: Option<AbortOnDrop>,
2619 done: bool,
2620 }
2621
2622 let state = State {
2623 rx,
2624 handle: Some(AbortOnDrop {
2625 handle: Some(handle),
2626 }),
2627 done: false,
2628 };
2629
2630 let stream = stream::unfold(state, |mut state| async move {
2631 if state.done {
2632 return None;
2633 }
2634 match state.rx.recv().await {
2635 Some(frame) => {
2636 let is_completed = frame.completed.is_some();
2637 if is_completed {
2638 state.done = true;
2639 if let Some(guard) = state.handle.as_mut() {
2642 let _ = guard.take();
2643 }
2644 }
2645 let reply = zlink::Reply::new(Some(frame)).set_continues(Some(!is_completed));
2646 Some((Ok(reply), state))
2647 }
2648 None => {
2649 state.done = true;
2652 let join = state.handle.as_mut().and_then(AbortOnDrop::take);
2655 let err = match join {
2656 Some(join) => match join.await {
2657 Ok(Ok(())) => OciError::InternalError {
2658 message: "pull completed without a result frame".to_string(),
2659 },
2660 Ok(Err(e)) => e,
2661 Err(_) => OciError::InternalError {
2662 message: "pull task panicked".to_string(),
2663 },
2664 },
2665 None => OciError::InternalError {
2666 message: "pull task panicked".to_string(),
2667 },
2668 };
2669 Some((Err(err), state))
2670 }
2671 }
2672 });
2673
2674 Box::pin(stream)
2675 }
2676
2677 #[derive(Debug, zlink::ReplyError, zlink::introspect::ReplyError)]
2679 #[zlink(interface = "org.composefs.Oci")]
2680 pub enum OciError {
2681 RepoNotFound {
2683 message: String,
2685 },
2686 InvalidHandle {
2688 handle: u64,
2690 },
2691 NoSuchImage {
2693 image: String,
2695 },
2696 InternalError {
2698 message: String,
2700 },
2701 NoSuchLayer {
2703 diff_id: String,
2705 },
2706 InvalidDigest {
2708 message: String,
2710 },
2711 DiffIdMismatch {
2715 expected: String,
2717 actual: String,
2719 },
2720 InvalidRequest {
2722 message: String,
2724 },
2725 FdLimitExceeded {
2729 fd_count: u64,
2731 max_per_frame: u64,
2733 },
2734 BootImageMismatch {
2739 expected: String,
2741 tried: u64,
2743 },
2744 }
2745}
2746
2747#[cfg(feature = "oci")]
2754pub mod layer_sync {
2755 use super::*;
2756
2757 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2759 pub struct GetInfoReply {
2760 pub features: Vec<String>,
2764 }
2765
2766 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2768 pub struct HasLayerReply {
2769 pub present: bool,
2771 pub layer_verity: Option<String>,
2773 }
2774
2775 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2806 pub struct GetLayerReply {
2807 pub dir_count: u32,
2810 }
2811
2812 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2819 pub struct PutLayerReply {
2820 pub layer_verity: String,
2822 pub already_present: bool,
2827
2828 #[serde(default)]
2831 pub objects_reflinked: u64,
2832 #[serde(default)]
2834 pub objects_hardlinked: u64,
2835 #[serde(default)]
2837 pub objects_copied: u64,
2838 #[serde(default)]
2840 pub objects_already_present: u64,
2841 }
2842
2843 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2849 pub struct LayerRef {
2850 pub diff_id: String,
2852 pub layer_verity: String,
2855 }
2856
2857 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2860 pub struct FinalizeImageReply {
2861 pub manifest_digest: String,
2863 pub manifest_verity: String,
2865 pub config_digest: String,
2867 pub config_verity: String,
2869 }
2870}
2871
2872pub mod proxy {
2878 #![allow(missing_docs)]
2879
2880 #[cfg(feature = "oci")]
2881 use super::layer_sync::{
2882 FinalizeImageReply, GetInfoReply, GetLayerReply, HasLayerReply, LayerRef, PutLayerReply,
2883 };
2884 #[cfg(feature = "oci")]
2885 use super::oci::{
2886 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
2887 };
2888 use super::{
2889 EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply,
2890 OpenRepositoryReply, RepositoryError,
2891 };
2892 #[cfg(feature = "oci")]
2893 pub use composefs_oci::varlink_types::GetLayerParams;
2894 #[cfg(feature = "oci")]
2895 use zlink::futures_util::Stream;
2896
2897 #[zlink::proxy(interface = "org.composefs.Repository")]
2899 pub trait RepositoryProxy {
2900 async fn init_repository(
2902 &mut self,
2903 path: &str,
2904 algorithm: Option<&str>,
2905 insecure: Option<bool>,
2906 ) -> zlink::Result<Result<InitRepositoryReply, RepositoryError>>;
2907
2908 async fn ensure_repository(
2910 &mut self,
2911 path: &str,
2912 algorithm: Option<&str>,
2913 insecure: Option<bool>,
2914 ) -> zlink::Result<Result<EnsureRepositoryReply, RepositoryError>>;
2915
2916 async fn open_repository(
2918 &mut self,
2919 path: Option<&str>,
2920 user: Option<bool>,
2921 system: Option<bool>,
2922 ) -> zlink::Result<Result<OpenRepositoryReply, RepositoryError>>;
2923
2924 async fn close_repository(
2926 &mut self,
2927 handle: u64,
2928 ) -> zlink::Result<Result<(), RepositoryError>>;
2929
2930 async fn fsck(
2932 &mut self,
2933 handle: u64,
2934 metadata_only: Option<bool>,
2935 ) -> zlink::Result<Result<FsckReply, RepositoryError>>;
2936
2937 async fn gc(
2939 &mut self,
2940 handle: u64,
2941 dry_run: bool,
2942 roots: Vec<String>,
2943 ) -> zlink::Result<Result<GcReply, RepositoryError>>;
2944
2945 async fn image_objects(
2947 &mut self,
2948 handle: u64,
2949 name: &str,
2950 ) -> zlink::Result<Result<ImageObjectsReply, RepositoryError>>;
2951 }
2952
2953 #[cfg(feature = "oci")]
2955 #[zlink::proxy(interface = "org.composefs.Oci")]
2956 #[allow(clippy::too_many_arguments)]
2957 pub trait OciProxy {
2958 async fn list_images(
2960 &mut self,
2961 handle: u64,
2962 filter: Option<&str>,
2963 ) -> zlink::Result<Result<ListImagesReply, OciError>>;
2964
2965 #[zlink(rename = "Check")]
2967 async fn oci_fsck(
2968 &mut self,
2969 handle: u64,
2970 image: Option<&str>,
2971 ) -> zlink::Result<Result<OciFsckReply, OciError>>;
2972
2973 async fn inspect(
2975 &mut self,
2976 handle: u64,
2977 image: &str,
2978 ) -> zlink::Result<Result<OciInspectReply, OciError>>;
2979
2980 async fn tag(
2982 &mut self,
2983 handle: u64,
2984 manifest_digest: &str,
2985 name: &str,
2986 ) -> zlink::Result<Result<(), OciError>>;
2987
2988 async fn untag(&mut self, handle: u64, name: &str) -> zlink::Result<Result<(), OciError>>;
2990
2991 async fn compute_id(
2993 &mut self,
2994 handle: u64,
2995 image: &str,
2996 verity: Option<&str>,
2997 bootable: bool,
2998 xattrs: Option<composefs_oci::XattrFiltering>,
2999 ) -> zlink::Result<Result<OciComputeIdReply, OciError>>;
3000
3001 #[zlink(more, rename = "Pull")]
3003 #[allow(clippy::too_many_arguments)]
3004 async fn pull(
3005 &mut self,
3006 handle: u64,
3007 image: &str,
3008 name: Option<&str>,
3009 local_fetch: &str,
3010 storage_root: Option<&str>,
3011 bootable: bool,
3012 xattrs: Option<composefs_oci::XattrFiltering>,
3013 expected_digest: Option<&str>,
3014 ) -> zlink::Result<impl Stream<Item = zlink::Result<Result<PullProgress, OciError>>>>;
3015
3016 async fn get_info(&mut self) -> zlink::Result<Result<GetInfoReply, OciError>>;
3018
3019 async fn has_layer(
3021 &mut self,
3022 handle: u64,
3023 diff_id: &str,
3024 ) -> zlink::Result<Result<HasLayerReply, OciError>>;
3025
3026 #[zlink(more, return_fds)]
3033 async fn get_layer(
3034 &mut self,
3035 handle: u64,
3036 params: GetLayerParams,
3037 ) -> zlink::Result<
3038 impl zlink::futures_util::Stream<
3039 Item = zlink::Result<(Result<GetLayerReply, OciError>, Vec<std::os::fd::OwnedFd>)>,
3040 >,
3041 >;
3042
3043 async fn put_layer(
3048 &mut self,
3049 handle: u64,
3050 diff_id: &str,
3051 zerocopy: bool,
3052 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
3053 ) -> zlink::Result<Result<PutLayerReply, OciError>>;
3054
3055 async fn finalize_image(
3061 &mut self,
3062 handle: u64,
3063 manifest_json: &str,
3064 config_json: &str,
3065 layers: Vec<LayerRef>,
3066 name: Option<&str>,
3067 ) -> zlink::Result<Result<FinalizeImageReply, OciError>>;
3068 }
3069}
3070
3071#[cfg(feature = "oci")]
3072pub(crate) use oci::*;
3073
3074#[cfg(feature = "oci")]
3085pub(crate) fn spawn_in_process(
3086 service: CfsctlService,
3087) -> std::io::Result<(zlink::tokio::unix::Connection, std::thread::JoinHandle<()>)> {
3088 let (client_std, server_std) = std::os::unix::net::UnixStream::pair()?;
3089 client_std.set_nonblocking(true)?;
3090 server_std.set_nonblocking(true)?;
3091
3092 let client_stream = tokio::net::UnixStream::from_std(client_std)?;
3093 let client_zlink =
3094 zlink::tokio::unix::Stream::try_from(client_stream).map_err(std::io::Error::other)?;
3095 let client_conn = zlink::Connection::new(client_zlink);
3096
3097 let handle = std::thread::Builder::new()
3098 .name("cfsctl-service-server".into())
3099 .spawn(move || {
3100 let rt = match tokio::runtime::Builder::new_current_thread()
3101 .enable_all()
3102 .build()
3103 {
3104 Ok(rt) => rt,
3105 Err(e) => {
3106 log::error!("CfsctlService server runtime build failed: {e:#?}");
3107 return;
3108 }
3109 };
3110 let local = tokio::task::LocalSet::new();
3111 local.block_on(&rt, async move {
3112 let server_stream = match tokio::net::UnixStream::from_std(server_std) {
3113 Ok(s) => s,
3114 Err(e) => {
3115 log::error!("CfsctlService server stream conversion failed: {e:#?}");
3116 return;
3117 }
3118 };
3119 let server_zlink = match zlink::tokio::unix::Stream::try_from(server_stream) {
3120 Ok(s) => s,
3121 Err(e) => {
3122 log::error!("CfsctlService server zlink stream conversion failed: {e:#?}");
3123 return;
3124 }
3125 };
3126 let listener = zlink::ReadyListener::new(server_zlink);
3127 let server = zlink::Server::new(listener, service);
3128 if let Err(e) = server.run().await {
3129 log::warn!("CfsctlService in-process server error: {e:#?}");
3130 }
3131 });
3132 })?;
3133
3134 Ok((client_conn, handle))
3135}
3136
3137#[cfg(all(test, feature = "oci"))]
3138mod layer_sync_tests {
3139 use std::io::Read as _;
3146 use std::os::fd::AsFd as _;
3147 use std::sync::Arc;
3148
3149 use composefs::fsverity::{FsVerityHashValue as _, Sha256HashValue};
3150 use composefs::repository::{Repository, RepositoryConfig};
3151 use composefs_splitdirfdstream::reconstruct;
3152
3153 use super::layer_sync::GetLayerReply;
3154 use super::oci::OciError;
3155 use super::proxy::{OciProxy, RepositoryProxy as _};
3156 use super::{CfsctlService, spawn_in_process};
3157 use composefs_oci::varlink_types::GetLayerParams;
3158
3159 async fn collect_get_layer<C>(
3167 client: &mut C,
3168 handle: u64,
3169 diff_id: &str,
3170 ) -> Result<(GetLayerReply, Vec<std::os::fd::OwnedFd>), OciError>
3171 where
3172 C: OciProxy,
3173 {
3174 use zlink::futures_util::StreamExt as _;
3175
3176 let params = GetLayerParams {
3177 diff_id: Some(diff_id.to_owned()),
3178 storage: None,
3179 ..Default::default()
3180 };
3181 let mut stream = std::pin::pin!(
3182 client
3183 .get_layer(handle, params)
3184 .await
3185 .expect("get_layer transport error")
3186 );
3187
3188 let mut all_fds: Vec<std::os::fd::OwnedFd> = Vec::new();
3189 let mut last_reply: Option<GetLayerReply> = None;
3190
3191 while let Some(item) = stream.next().await {
3192 let (result, fds) = item.expect("get_layer stream error");
3193 match result {
3194 Ok(reply) => {
3195 last_reply = Some(reply);
3196 }
3197 Err(e) => return Err(e),
3198 }
3199 all_fds.extend(fds);
3200 }
3201
3202 Ok((last_reply.expect("get_layer stream was empty"), all_fds))
3203 }
3204
3205 async fn collect_get_layer_split<C>(
3211 client: &mut C,
3212 handle: u64,
3213 diff_id: &str,
3214 ) -> (
3215 GetLayerReply,
3216 Vec<std::os::fd::OwnedFd>,
3217 Vec<std::os::fd::OwnedFd>,
3218 )
3219 where
3220 C: OciProxy,
3221 {
3222 let (reply, mut all_fds) = collect_get_layer(client, handle, diff_id)
3223 .await
3224 .expect("get_layer failed");
3225 let dir_count = reply.dir_count as usize;
3226 let pipe_and_dirfds_len = 1 + dir_count;
3228 assert!(
3229 all_fds.len() >= pipe_and_dirfds_len,
3230 "expected at least {pipe_and_dirfds_len} fds, got {}",
3231 all_fds.len()
3232 );
3233 let lifetime_fds = all_fds.split_off(pipe_and_dirfds_len);
3234 (reply, all_fds, lifetime_fds)
3235 }
3236
3237 fn build_tar_layer(file_size: usize) -> Vec<u8> {
3240 let content: Vec<u8> = (0..file_size).map(|i| (i % 251) as u8).collect();
3241 let mut builder = ::tar::Builder::new(vec![]);
3242 let mut header = ::tar::Header::new_ustar();
3243 header.set_uid(0);
3244 header.set_gid(0);
3245 header.set_mode(0o644);
3246 header.set_entry_type(::tar::EntryType::Regular);
3247 header.set_size(file_size as u64);
3248 builder
3249 .append_data(&mut header, format!("file_{file_size}"), &content[..])
3250 .unwrap();
3251 builder.into_inner().unwrap()
3252 }
3253
3254 fn create_test_repo() -> (Arc<Repository<Sha256HashValue>>, tempfile::TempDir) {
3256 let tempdir = tempfile::TempDir::new().unwrap();
3257 let (repo, _) = Repository::init_path(
3258 rustix::fs::CWD,
3259 tempdir.path().join("repo"),
3260 RepositoryConfig::default().set_insecure(),
3261 )
3262 .unwrap();
3263 (Arc::new(repo), tempdir)
3264 }
3265
3266 #[tokio::test(flavor = "multi_thread")]
3267 async fn test_layer_sync_in_process() {
3268 let (repo, _tempdir) = create_test_repo();
3270
3271 let tar_bytes = build_tar_layer(128 * 1024); let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3274 let (verity, _stats) =
3275 composefs_oci::import_layer(&repo, &diff_id, None, tar_bytes.as_slice())
3276 .await
3277 .expect("import_layer");
3278
3279 let mut expected = Vec::<u8>::new();
3281 {
3282 let mut reader = repo
3283 .open_stream("", Some(&verity), Some(composefs_oci::LAYER_CONTENT_TYPE))
3284 .expect("open_stream for cat");
3285 reader.cat(&repo, &mut expected).expect("cat");
3286 }
3287
3288 let repo_path = _tempdir.path().join("repo").to_str().unwrap().to_string();
3289
3290 let service = CfsctlService::insecure_for_test();
3292 let (mut client, _server_handle) = spawn_in_process(service).unwrap();
3293
3294 let open_reply = client
3296 .open_repository(Some(&repo_path), None, None)
3297 .await
3298 .unwrap()
3299 .expect("open_repository");
3300 let handle = open_reply.handle;
3301
3302 assert_eq!(
3304 open_reply.hash_algorithm.as_deref(),
3305 Some("sha256"),
3306 "hash_algorithm must be sha256 for a Sha256HashValue repo"
3307 );
3308 assert!(
3309 open_reply.objects_device_id.is_some(),
3310 "objects_device_id must be reported"
3311 );
3312
3313 let info = client.get_info().await.unwrap().expect("get_info");
3315 assert!(
3316 info.features.contains(&"splitdirfdstream-v0".to_string()),
3317 "expected splitdirfdstream-v0 in features"
3318 );
3319
3320 let has = client
3322 .has_layer(handle, diff_id.as_ref())
3323 .await
3324 .unwrap()
3325 .expect("has_layer");
3326 assert!(has.present, "layer must be present");
3327 assert_eq!(
3328 has.layer_verity.as_deref(),
3329 Some(verity.to_hex().as_str()),
3330 "verity mismatch"
3331 );
3332
3333 let fake_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
3335 let has_absent = client
3336 .has_layer(handle, fake_digest)
3337 .await
3338 .unwrap()
3339 .expect("has_layer absent");
3340 assert!(!has_absent.present, "absent layer must not be present");
3341 assert!(has_absent.layer_verity.is_none());
3342
3343 let (get_reply, pipe_and_dirfds, lifetime_fds) =
3347 collect_get_layer_split(&mut client, handle, diff_id.as_ref()).await;
3348 let dir_count = get_reply.dir_count as usize;
3349
3350 let _lifetime_fds = lifetime_fds;
3352
3353 let pipe_fd = pipe_and_dirfds[0].as_fd();
3355 let dir_fds: Vec<_> = pipe_and_dirfds[1..=dir_count]
3356 .iter()
3357 .map(|f| f.as_fd())
3358 .collect();
3359
3360 let pipe_owned = rustix::io::dup(pipe_fd).expect("dup pipe read");
3362 let mut pipe_file = std::fs::File::from(pipe_owned);
3363 let mut stream_bytes = Vec::new();
3364 pipe_file.read_to_end(&mut stream_bytes).unwrap();
3365 assert!(!stream_bytes.is_empty(), "stream must be non-empty");
3366
3367 let mut actual = Vec::new();
3369 reconstruct(stream_bytes.as_slice(), &dir_fds, &mut actual)
3370 .expect("reconstruct splitdirfdstream");
3371
3372 similar_asserts::assert_eq!(
3373 actual,
3374 expected,
3375 "reconstructed layer must equal cat() output"
3376 );
3377
3378 let err = collect_get_layer(&mut client, handle, fake_digest).await;
3380 match err {
3381 Err(super::oci::OciError::NoSuchLayer { .. }) => {}
3382 other => panic!("expected NoSuchLayer, got {other:?}"),
3383 }
3384 }
3385
3386 #[tokio::test(flavor = "multi_thread")]
3395 async fn test_put_layer_relay() {
3396 let (repo_a, _td_a) = create_test_repo();
3398 let tar_bytes = build_tar_layer(128 * 1024); let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3400 let (verity_a, _) =
3401 composefs_oci::import_layer(&repo_a, &diff_id, None, tar_bytes.as_slice())
3402 .await
3403 .expect("import_layer into repo_a");
3404
3405 let mut expected = Vec::<u8>::new();
3407 {
3408 let mut reader = repo_a
3409 .open_stream("", Some(&verity_a), Some(composefs_oci::LAYER_CONTENT_TYPE))
3410 .expect("open_stream for cat");
3411 reader.cat(&repo_a, &mut expected).expect("cat");
3412 }
3413 let repo_a_path = _td_a.path().join("repo").to_str().unwrap().to_string();
3414
3415 let (repo_b, _td_b) = create_test_repo();
3417 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3418
3419 let service_a = CfsctlService::insecure_for_test();
3421 let (mut client_a, _srv_a) = spawn_in_process(service_a).unwrap();
3422
3423 let service_b = CfsctlService::insecure_for_test();
3424 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3425
3426 let handle_a = client_a
3428 .open_repository(Some(&repo_a_path), None, None)
3429 .await
3430 .unwrap()
3431 .expect("open_repository A")
3432 .handle;
3433 let handle_b = client_b
3434 .open_repository(Some(&repo_b_path), None, None)
3435 .await
3436 .unwrap()
3437 .expect("open_repository B")
3438 .handle;
3439
3440 let (get_reply, pipe_and_dirfds, lifetime_fds) =
3443 collect_get_layer_split(&mut client_a, handle_a, diff_id.as_ref()).await;
3444 let dir_count = get_reply.dir_count as usize;
3445
3446 let put_fds = pipe_and_dirfds; let put_reply = client_b
3451 .put_layer(handle_b, diff_id.as_ref(), false, put_fds)
3452 .await
3453 .unwrap()
3454 .expect("put_layer");
3455 drop(lifetime_fds);
3457
3458 assert!(
3459 !put_reply.already_present,
3460 "first put_layer must report already_present = false"
3461 );
3462 assert!(
3463 dir_count > 0,
3464 "dir_count must be > 0 (dirfds region has at least one slot)"
3465 );
3466
3467 let total_stored =
3471 put_reply.objects_reflinked + put_reply.objects_hardlinked + put_reply.objects_copied;
3472 assert!(
3473 total_stored + put_reply.objects_already_present > 0,
3474 "put_layer must report at least one object stored, got {put_reply:?}"
3475 );
3476
3477 let content_id = composefs_oci::layer_content_id(&diff_id);
3479 assert!(
3480 repo_b
3481 .has_stream(&content_id)
3482 .expect("has_stream B")
3483 .is_some(),
3484 "repo B must have the layer after put_layer"
3485 );
3486
3487 let verity_b: Sha256HashValue =
3489 Sha256HashValue::from_hex(&put_reply.layer_verity).expect("parse layer_verity hex");
3490 let mut actual = Vec::<u8>::new();
3491 {
3492 let mut reader = repo_b
3493 .open_stream("", Some(&verity_b), Some(composefs_oci::LAYER_CONTENT_TYPE))
3494 .expect("open_stream B for cat");
3495 reader.cat(&repo_b, &mut actual).expect("cat B");
3496 }
3497 similar_asserts::assert_eq!(actual, expected, "repo B cat must equal repo A cat");
3498
3499 let (get_reply2, pipe_and_dirfds2, lifetime_fds2) =
3501 collect_get_layer_split(&mut client_a, handle_a, diff_id.as_ref()).await;
3502 let _ = get_reply2;
3503
3504 let put_reply2 = client_b
3505 .put_layer(handle_b, diff_id.as_ref(), false, pipe_and_dirfds2)
3506 .await
3507 .unwrap()
3508 .expect("put_layer 2nd");
3509 drop(lifetime_fds2);
3510
3511 assert!(
3512 put_reply2.already_present,
3513 "second put_layer must report already_present = true"
3514 );
3515 }
3516
3517 #[tokio::test(flavor = "multi_thread")]
3520 async fn test_put_layer_wrong_diff_id() {
3521 let (repo_a, _td_a) = create_test_repo();
3522 let tar_bytes = build_tar_layer(128 * 1024);
3523 let correct_diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3524 let (_verity_a, _) =
3525 composefs_oci::import_layer(&repo_a, &correct_diff_id, None, tar_bytes.as_slice())
3526 .await
3527 .expect("import_layer");
3528 let repo_a_path = _td_a.path().join("repo").to_str().unwrap().to_string();
3529
3530 let (_repo_b, _td_b) = create_test_repo();
3531 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3532
3533 let service_a = CfsctlService::insecure_for_test();
3534 let (mut client_a, _srv_a) = spawn_in_process(service_a).unwrap();
3535 let service_b = CfsctlService::insecure_for_test();
3536 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3537
3538 let handle_a = client_a
3539 .open_repository(Some(&repo_a_path), None, None)
3540 .await
3541 .unwrap()
3542 .expect("open_repository A")
3543 .handle;
3544 let handle_b = client_b
3545 .open_repository(Some(&repo_b_path), None, None)
3546 .await
3547 .unwrap()
3548 .expect("open_repository B")
3549 .handle;
3550
3551 let (_get_reply, pipe_and_dirfds, lifetime_fds) =
3553 collect_get_layer_split(&mut client_a, handle_a, correct_diff_id.as_ref()).await;
3554
3555 let wrong_diff_id =
3557 "sha256:0000000000000000000000000000000000000000000000000000000000000000";
3558 let put_err = client_b
3559 .put_layer(handle_b, wrong_diff_id, false, pipe_and_dirfds)
3560 .await
3561 .unwrap();
3562 drop(lifetime_fds);
3563
3564 match put_err {
3565 Err(super::oci::OciError::DiffIdMismatch { expected, actual }) => {
3566 assert_eq!(expected, wrong_diff_id);
3567 assert_eq!(actual, correct_diff_id.to_string());
3568 }
3569 other => panic!("expected DiffIdMismatch, got {other:?}"),
3570 }
3571
3572 let wrong_content_id = composefs_oci::layer_content_id(
3574 &wrong_diff_id.parse::<composefs_oci::OciDigest>().unwrap(),
3575 );
3576 assert!(
3577 _repo_b
3578 .has_stream(&wrong_content_id)
3579 .expect("has_stream B")
3580 .is_none(),
3581 "repo B must NOT have a stream for the wrong diff_id"
3582 );
3583 }
3584
3585 fn build_oci_tar_layer(payload_size: usize) -> Vec<u8> {
3594 let mut builder = ::tar::Builder::new(vec![]);
3595
3596 for (path, is_dir) in &[("./", true), ("./usr/", true), ("./usr/share/", true)] {
3597 let mut hdr = ::tar::Header::new_ustar();
3598 hdr.set_entry_type(::tar::EntryType::Directory);
3599 hdr.set_uid(0);
3600 hdr.set_gid(0);
3601 hdr.set_mode(0o755);
3602 hdr.set_size(0);
3603 let _ = is_dir; builder
3605 .append_data(&mut hdr, path, std::io::empty())
3606 .unwrap();
3607 }
3608
3609 let content: Vec<u8> = (0..payload_size).map(|i| (i % 251) as u8).collect();
3610 let mut file_hdr = ::tar::Header::new_ustar();
3611 file_hdr.set_entry_type(::tar::EntryType::Regular);
3612 file_hdr.set_uid(0);
3613 file_hdr.set_gid(0);
3614 file_hdr.set_mode(0o644);
3615 file_hdr.set_size(payload_size as u64);
3616 builder
3617 .append_data(
3618 &mut file_hdr,
3619 format!("./usr/share/data_{payload_size}"),
3620 content.as_slice(),
3621 )
3622 .unwrap();
3623
3624 builder.into_inner().unwrap()
3625 }
3626
3627 fn make_config_json(diff_ids: &[String]) -> String {
3632 let ids: Vec<String> = diff_ids.iter().map(|d| format!("\"{d}\"")).collect();
3633 format!(
3634 r#"{{"architecture":"amd64","os":"linux","rootfs":{{"type":"layers","diff_ids":[{}]}},"config":{{}}}}"#,
3635 ids.join(",")
3636 )
3637 }
3638
3639 fn make_manifest_json(
3641 config_json: &str,
3642 config_digest_str: &str,
3643 diff_ids: &[String],
3644 ) -> String {
3645 let layer_entries: Vec<String> = diff_ids
3646 .iter()
3647 .map(|d| {
3648 format!(
3649 r#"{{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"{d}","size":1}}"#
3650 )
3651 })
3652 .collect();
3653 format!(
3654 r#"{{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"{config_digest_str}","size":{}}},"layers":[{}]}}"#,
3655 config_json.len(),
3656 layer_entries.join(",")
3657 )
3658 }
3659
3660 #[tokio::test(flavor = "multi_thread")]
3668 async fn test_finalize_image_roundtrip() {
3669 use composefs_oci::OciDigest;
3670
3671 let (repo_b, _td_b) = create_test_repo();
3672 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3673
3674 let tar_bytes = build_oci_tar_layer(128 * 1024);
3676 let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3677 let (layer_verity, _) =
3678 composefs_oci::import_layer(&repo_b, &diff_id, None, tar_bytes.as_slice())
3679 .await
3680 .expect("import_layer into repo_b");
3681
3682 let diff_ids = vec![diff_id.to_string()];
3684 let config_json = make_config_json(&diff_ids);
3685 let config_digest = composefs_oci::sha256_content_digest(config_json.as_bytes());
3686 let manifest_json = make_manifest_json(&config_json, config_digest.as_ref(), &diff_ids);
3687
3688 let service_b = CfsctlService::insecure_for_test();
3690 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3691
3692 let handle_b = client_b
3693 .open_repository(Some(&repo_b_path), None, None)
3694 .await
3695 .unwrap()
3696 .expect("open_repository B")
3697 .handle;
3698
3699 let layers = vec![super::layer_sync::LayerRef {
3701 diff_id: diff_id.to_string(),
3702 layer_verity: layer_verity.to_hex(),
3703 }];
3704
3705 let reply = client_b
3707 .finalize_image(
3708 handle_b,
3709 &manifest_json,
3710 &config_json,
3711 layers,
3712 Some("finalize-test:v1"),
3713 )
3714 .await
3715 .unwrap()
3716 .expect("finalize_image");
3717
3718 assert!(
3720 !reply.manifest_digest.is_empty(),
3721 "manifest_digest must be non-empty"
3722 );
3723 assert!(
3724 !reply.manifest_verity.is_empty(),
3725 "manifest_verity must be non-empty"
3726 );
3727 assert!(
3728 !reply.config_digest.is_empty(),
3729 "config_digest must be non-empty"
3730 );
3731 assert!(
3732 !reply.config_verity.is_empty(),
3733 "config_verity must be non-empty"
3734 );
3735
3736 let manifest_digest: OciDigest = reply.manifest_digest.parse().unwrap();
3738 let config_digest2: OciDigest = reply.config_digest.parse().unwrap();
3739
3740 let manifest_id = composefs_oci::oci_image::manifest_identifier(&manifest_digest);
3741 assert!(
3742 repo_b
3743 .has_stream(&manifest_id)
3744 .expect("has_stream manifest")
3745 .is_some(),
3746 "manifest splitstream must exist in repo_b"
3747 );
3748
3749 let config_id2 = format!("oci-config-{config_digest2}");
3751 assert!(
3752 repo_b
3753 .has_stream(&config_id2)
3754 .expect("has_stream config")
3755 .is_some(),
3756 "config splitstream must exist in repo_b"
3757 );
3758
3759 let manifest_verity =
3761 Sha256HashValue::from_hex(&reply.manifest_verity).expect("parse manifest_verity");
3762 let erofs = composefs_oci::composefs_erofs_for_manifest(
3763 &repo_b,
3764 &manifest_digest,
3765 Some(&manifest_verity),
3766 repo_b.erofs_version(),
3767 )
3768 .expect("composefs_erofs_for_manifest");
3769 assert!(
3770 erofs.is_some(),
3771 "EROFS image must exist after finalize_image"
3772 );
3773 }
3774}