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 options = params
630 .to_mount_options(fds)
631 .map_err(|e| oci::OciError::InternalError {
632 message: format!("{e:?}"),
633 })?;
634
635 let mount_fd = img.mount(repo, bootable, &options).map_err(|e| {
636 match e.downcast_ref::<composefs_oci::oci_image::OciErofsNotFound>() {
637 Some(_) => oci::OciError::InternalError {
640 message: if bootable {
641 "No boot EROFS image linked".into()
642 } else {
643 "No composefs EROFS image linked".into()
644 },
645 },
646 None => oci::OciError::InternalError {
647 message: format!("{e:#}"),
648 },
649 }
650 })?;
651
652 Ok((MountReply { fd_index: 0 }, vec![mount_fd]))
653}
654
655fn parse_algorithm(algorithm: Option<&str>) -> std::result::Result<Algorithm, RepositoryError> {
658 match algorithm {
659 Some(s) => s.parse().map_err(|e| RepositoryError::InvalidSpec {
660 message: format!("invalid algorithm: {e}"),
661 }),
662 None => Ok(Algorithm::SHA512),
663 }
664}
665
666fn run_init_repository(
673 path: &Path,
674 algorithm: Algorithm,
675 insecure: bool,
676) -> std::result::Result<InitRepositoryReply, RepositoryError> {
677 if let Some(parent) = path.parent() {
679 std::fs::create_dir_all(parent).map_err(|e| RepositoryError::InternalError {
680 message: format!("creating parent directories for {}: {e:#}", path.display()),
681 })?;
682 }
683
684 let created = match algorithm {
685 Algorithm::Sha256 { .. } => {
686 let config = if insecure {
687 RepositoryConfig::new(algorithm).set_insecure()
688 } else {
689 RepositoryConfig::new(algorithm)
690 };
691 Repository::<Sha256HashValue>::init_path(CWD, path, config)
692 .map_err(|e| RepositoryError::InternalError {
693 message: format!("{e:#}"),
694 })?
695 .1
696 }
697 Algorithm::Sha512 { .. } => {
698 let config = if insecure {
699 RepositoryConfig::new(algorithm).set_insecure()
700 } else {
701 RepositoryConfig::new(algorithm)
702 };
703 Repository::<Sha512HashValue>::init_path(CWD, path, config)
704 .map_err(|e| RepositoryError::InternalError {
705 message: format!("{e:#}"),
706 })?
707 .1
708 }
709 };
710 Ok(InitRepositoryReply { created })
711}
712
713pub(crate) fn run_ensure_repository(
721 path: &Path,
722 algorithm: Algorithm,
723 insecure: bool,
724 erofs_formats: Option<composefs::erofs::format::FormatConfig>,
725) -> anyhow::Result<composefs::repository::EnsureStatus> {
726 if let Some(parent) = path.parent() {
727 std::fs::create_dir_all(parent)
728 .with_context(|| format!("creating parent directories for {}", path.display()))?;
729 }
730
731 let mut config = RepositoryConfig::new(algorithm);
732 if insecure {
733 config = config.set_insecure();
734 }
735 if let Some(formats) = erofs_formats {
736 config.erofs_formats = formats;
737 }
738
739 let status = match algorithm {
740 Algorithm::Sha256 { .. } => {
741 Repository::<Sha256HashValue>::ensure_path(CWD, path, config)?.1
742 }
743 Algorithm::Sha512 { .. } => {
744 Repository::<Sha512HashValue>::ensure_path(CWD, path, config)?.1
745 }
746 };
747 Ok(status)
748}
749
750#[cfg(feature = "oci")]
753async fn run_list_images<ObjectID: FsVerityHashValue>(
754 repo: &Repository<ObjectID>,
755 filter: Option<String>,
756) -> std::result::Result<Vec<oci::ImageEntry>, oci::OciError> {
757 composefs_oci::oci_image::list_images(repo)
758 .map(|imgs| {
759 imgs.iter()
760 .filter(|img| match &filter {
761 Some(needle) => img.name.contains(needle.as_str()),
762 None => true,
763 })
764 .map(oci::ImageEntry::from)
765 .collect()
766 })
767 .map_err(|e| oci::OciError::InternalError {
768 message: format!("{e:#}"),
769 })
770}
771
772#[cfg(feature = "oci")]
777async fn run_oci_fsck<ObjectID: FsVerityHashValue>(
778 repo: &Repository<ObjectID>,
779 image: Option<String>,
780) -> std::result::Result<oci::OciFsckReply, oci::OciError> {
781 let result = match image {
782 Some(name) => composefs_oci::oci_fsck_image(repo, &name).await,
783 None => composefs_oci::oci_fsck(repo).await,
784 }
785 .map_err(|e| oci::OciError::InternalError {
786 message: format!("{e:#}"),
787 })?;
788 Ok(oci::OciFsckReply::from(&result))
789}
790
791#[cfg(feature = "oci")]
793async fn run_inspect<ObjectID: FsVerityHashValue>(
794 repo: &Repository<ObjectID>,
795 image: String,
796) -> std::result::Result<oci::OciInspectReply, oci::OciError> {
797 let reference: crate::OciReference =
798 image.parse().map_err(|e| oci::OciError::InternalError {
799 message: format!("invalid image reference: {e:#}"),
800 })?;
801 let img = crate::resolve_oci_image(repo, &reference).map_err(|e| {
802 if let Some(nf) = e.downcast_ref::<composefs_oci::OciRefNotFound>() {
803 oci::OciError::NoSuchImage {
804 image: nf.name.clone(),
805 }
806 } else if let Some(nf) = e.downcast_ref::<composefs_oci::OciImageNotFound>() {
807 oci::OciError::NoSuchImage {
808 image: nf.digest.clone(),
809 }
810 } else {
811 oci::OciError::InternalError {
812 message: format!("{e:#}"),
813 }
814 }
815 })?;
816
817 oci::OciInspectReply::from_image(repo, &img).map_err(|e| oci::OciError::InternalError {
818 message: format!("{e:#}"),
819 })
820}
821
822#[cfg(feature = "oci")]
824async fn run_tag<ObjectID: FsVerityHashValue>(
825 repo: &Repository<ObjectID>,
826 manifest_digest: String,
827 name: String,
828) -> std::result::Result<(), oci::OciError> {
829 let digest: composefs_oci::OciDigest =
830 manifest_digest
831 .parse()
832 .map_err(|e| oci::OciError::InternalError {
833 message: format!("invalid digest: {e}"),
834 })?;
835 composefs_oci::oci_image::tag_image(repo, &digest, &name).map_err(|e| {
836 oci::OciError::InternalError {
837 message: format!("{e:#}"),
838 }
839 })
840}
841
842#[cfg(feature = "oci")]
844async fn run_untag<ObjectID: FsVerityHashValue>(
845 repo: &Repository<ObjectID>,
846 name: String,
847) -> std::result::Result<(), oci::OciError> {
848 composefs_oci::oci_image::untag_image(repo, &name).map_err(|e| oci::OciError::InternalError {
849 message: format!("{e:#}"),
850 })
851}
852
853#[cfg(feature = "oci")]
859async fn run_compute_id<ObjectID: FsVerityHashValue>(
860 repo: &Repository<ObjectID>,
861 image: String,
862 verity: Option<String>,
863 bootable: bool,
864 xattrs: Option<composefs_oci::XattrFiltering>,
865) -> std::result::Result<oci::OciComputeIdReply, oci::OciError> {
866 let reference: crate::OciReference =
867 image.parse().map_err(|e| oci::OciError::InternalError {
868 message: format!("invalid image reference: {e:#}"),
869 })?;
870 let verity_override =
871 crate::verity_opt::<ObjectID>(&verity).map_err(|e| oci::OciError::InternalError {
872 message: format!("invalid verity: {e:#}"),
873 })?;
874 let (config_digest, config_verity) =
875 crate::resolve_oci_config(repo, &reference, verity_override).map_err(|e| {
876 oci::OciError::InternalError {
877 message: format!("{e:#}"),
878 }
879 })?;
880
881 let transform_opts = composefs_oci::OciTransformOptions {
882 xattrs: xattrs.unwrap_or_default(),
883 };
884 let mut fs = composefs_oci::image::create_filesystem(
885 repo,
886 &config_digest,
887 config_verity.as_ref(),
888 &transform_opts,
889 )
890 .map_err(|e| oci::OciError::InternalError {
891 message: format!("{e:#}"),
892 })?;
893 if bootable {
894 use composefs_boot::BootOps as _;
895 fs.transform_for_boot(repo)
896 .map_err(|e| oci::OciError::InternalError {
897 message: format!("{e:#}"),
898 })?;
899 }
900 let id = fs.compute_image_id(repo.erofs_version());
901 Ok(oci::OciComputeIdReply {
902 image_id: id.to_hex(),
903 })
904}
905
906#[cfg(not(feature = "oci"))]
924mod service_impl {
925 #![allow(missing_docs)]
926
927 use super::{
928 CfsctlService, EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply,
929 InitRepositoryReply, ListImageRefsReply, MountParams, MountReply, OpenRepo,
930 OpenRepositoryReply, RepositoryError, parse_algorithm, run_ensure_repository, run_fsck,
931 run_gc, run_image_objects, run_init_repository, run_list_image_refs, run_mount,
932 };
933 use composefs::fsverity::{Sha256HashValue, Sha512HashValue};
934
935 #[zlink::service(
936 interface = "org.composefs.Repository",
937 vendor = "org.composefs",
938 product = "cfsctl",
939 version = env!("CARGO_PKG_VERSION"),
940 url = "https://github.com/composefs/composefs-rs"
941 )]
942 impl<Sock> CfsctlService {
943 async fn init_repository(
953 &mut self,
954 path: String,
955 algorithm: Option<String>,
956 insecure: Option<bool>,
957 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
958 let algorithm = parse_algorithm(algorithm.as_deref())?;
959 let insecure = insecure.unwrap_or(self.open_opts.insecure);
960 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
961 }
962
963 async fn ensure_repository(
975 &mut self,
976 path: String,
977 algorithm: Option<String>,
978 insecure: Option<bool>,
979 ) -> std::result::Result<EnsureRepositoryReply, RepositoryError> {
980 let algorithm = parse_algorithm(algorithm.as_deref())?;
981 let insecure = insecure.unwrap_or(self.open_opts.insecure);
982 let status =
983 run_ensure_repository(std::path::Path::new(&path), algorithm, insecure, None)
984 .map_err(|e| RepositoryError::InternalError {
985 message: format!("{e:#}"),
986 })?;
987 Ok(EnsureRepositoryReply { status })
988 }
989
990 async fn open_repository(
994 &mut self,
995 path: Option<String>,
996 user: Option<bool>,
997 system: Option<bool>,
998 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
999 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
1000 let selected = Self::resolve_selector(path, user, system)?;
1001 self.do_open(&selected, Some(conn.id()))
1002 }
1003
1004 async fn close_repository(
1006 &mut self,
1007 handle: u64,
1008 ) -> std::result::Result<(), RepositoryError> {
1009 self.repos
1010 .remove(&handle)
1011 .map(|_| ())
1012 .ok_or(RepositoryError::InvalidHandle { handle })
1013 }
1014
1015 async fn fsck(
1021 &self,
1022 handle: u64,
1023 metadata_only: Option<bool>,
1024 ) -> std::result::Result<FsckReply, RepositoryError> {
1025 let metadata_only = metadata_only.unwrap_or(false);
1026 let result = match self.lookup_repo(handle)? {
1027 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
1028 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
1029 }?;
1030 Ok(FsckReply::from(&result))
1031 }
1032
1033 async fn gc(
1035 &self,
1036 handle: u64,
1037 dry_run: bool,
1038 roots: Vec<String>,
1039 ) -> std::result::Result<GcReply, RepositoryError> {
1040 match self.lookup_repo(handle)? {
1041 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
1042 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
1043 }
1044 }
1045
1046 async fn image_objects(
1048 &self,
1049 handle: u64,
1050 name: String,
1051 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
1052 match self.lookup_repo(handle)? {
1053 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
1054 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
1055 }
1056 }
1057
1058 async fn list_image_refs(
1060 &self,
1061 handle: u64,
1062 ) -> std::result::Result<ListImageRefsReply, RepositoryError> {
1063 match self.lookup_repo(handle)? {
1064 OpenRepo::Sha256(ref r) => run_list_image_refs::<Sha256HashValue>(r),
1065 OpenRepo::Sha512(ref r) => run_list_image_refs::<Sha512HashValue>(r),
1066 }
1067 }
1068
1069 #[zlink(return_fds)]
1075 async fn mount(
1076 &self,
1077 handle: u64,
1078 name: String,
1079 options: MountParams,
1080 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1081 ) -> (
1082 std::result::Result<MountReply, RepositoryError>,
1083 Vec<std::os::fd::OwnedFd>,
1084 ) {
1085 let result = match self.lookup_repo(handle) {
1086 Ok(OpenRepo::Sha256(ref r)) => {
1087 run_mount::<Sha256HashValue>(r, &name, &options, fds)
1088 }
1089 Ok(OpenRepo::Sha512(ref r)) => {
1090 run_mount::<Sha512HashValue>(r, &name, &options, fds)
1091 }
1092 Err(e) => Err(e),
1093 };
1094 match result {
1095 Ok((reply, fds)) => (Ok(reply), fds),
1096 Err(e) => (Err(e), vec![]),
1097 }
1098 }
1099 }
1100}
1101
1102#[cfg(feature = "oci")]
1107mod service_impl {
1108 #![allow(missing_docs)]
1109
1110 use super::layer_sync::{
1111 FinalizeImageReply, GetInfoReply, GetLayerReply, HasLayerReply, LayerRef, PutLayerReply,
1112 };
1113 use super::oci::{
1114 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
1115 parse_local_fetch, pull_stream,
1116 };
1117 use super::{
1118 CfsctlService, EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply,
1119 InitRepositoryReply, ListImageRefsReply, MountParams, MountReply, OpenRepo,
1120 OpenRepositoryReply, RepositoryError, parse_algorithm, run_compute_id,
1121 run_ensure_repository, run_fsck, run_gc, run_image_objects, run_init_repository,
1122 run_inspect, run_list_image_refs, run_list_images, run_mount, run_oci_fsck, run_oci_mount,
1123 run_tag, run_untag,
1124 };
1125 use composefs::fsverity::{FsVerityHashValue, Sha256HashValue, Sha512HashValue};
1126 use composefs_oci::layer_transport::{RepoLayerSource, serve_get_layer};
1127 use composefs_oci::varlink_types::GetLayerParams;
1128 use composefs_splitdirfdstream::seed_from_id;
1129
1130 #[zlink::service(
1131 interface = "org.composefs.Repository",
1132 vendor = "org.composefs",
1133 product = "cfsctl",
1134 version = env!("CARGO_PKG_VERSION"),
1135 url = "https://github.com/composefs/composefs-rs"
1136 )]
1137 impl<Sock> CfsctlService {
1138 async fn init_repository(
1150 &mut self,
1151 path: String,
1152 algorithm: Option<String>,
1153 insecure: Option<bool>,
1154 ) -> std::result::Result<InitRepositoryReply, RepositoryError> {
1155 let algorithm = parse_algorithm(algorithm.as_deref())?;
1156 let insecure = insecure.unwrap_or(self.open_opts.insecure);
1157 run_init_repository(std::path::Path::new(&path), algorithm, insecure)
1158 }
1159
1160 async fn ensure_repository(
1172 &mut self,
1173 path: String,
1174 algorithm: Option<String>,
1175 insecure: Option<bool>,
1176 ) -> std::result::Result<EnsureRepositoryReply, RepositoryError> {
1177 let algorithm = parse_algorithm(algorithm.as_deref())?;
1178 let insecure = insecure.unwrap_or(self.open_opts.insecure);
1179 let status =
1180 run_ensure_repository(std::path::Path::new(&path), algorithm, insecure, None)
1181 .map_err(|e| RepositoryError::InternalError {
1182 message: format!("{e:#}"),
1183 })?;
1184 Ok(EnsureRepositoryReply { status })
1185 }
1186
1187 async fn open_repository(
1191 &mut self,
1192 path: Option<String>,
1193 user: Option<bool>,
1194 system: Option<bool>,
1195 #[zlink(connection)] conn: &mut zlink::Connection<Sock>,
1196 ) -> std::result::Result<OpenRepositoryReply, RepositoryError> {
1197 let selected = Self::resolve_selector(path, user, system)?;
1198 self.do_open(&selected, Some(conn.id()))
1199 }
1200
1201 async fn close_repository(
1203 &mut self,
1204 handle: u64,
1205 ) -> std::result::Result<(), RepositoryError> {
1206 self.repos
1207 .remove(&handle)
1208 .map(|_| ())
1209 .ok_or(RepositoryError::InvalidHandle { handle })
1210 }
1211
1212 async fn fsck(
1218 &self,
1219 handle: u64,
1220 metadata_only: Option<bool>,
1221 ) -> std::result::Result<FsckReply, RepositoryError> {
1222 let metadata_only = metadata_only.unwrap_or(false);
1223 let result = match self.lookup_repo(handle)? {
1224 OpenRepo::Sha256(ref r) => run_fsck::<Sha256HashValue>(r, metadata_only).await,
1225 OpenRepo::Sha512(ref r) => run_fsck::<Sha512HashValue>(r, metadata_only).await,
1226 }?;
1227 Ok(FsckReply::from(&result))
1228 }
1229
1230 async fn gc(
1232 &self,
1233 handle: u64,
1234 dry_run: bool,
1235 roots: Vec<String>,
1236 ) -> std::result::Result<GcReply, RepositoryError> {
1237 match self.lookup_repo(handle)? {
1238 OpenRepo::Sha256(ref r) => run_gc::<Sha256HashValue>(r, dry_run, roots).await,
1239 OpenRepo::Sha512(ref r) => run_gc::<Sha512HashValue>(r, dry_run, roots).await,
1240 }
1241 }
1242
1243 async fn image_objects(
1245 &self,
1246 handle: u64,
1247 name: String,
1248 ) -> std::result::Result<ImageObjectsReply, RepositoryError> {
1249 match self.lookup_repo(handle)? {
1250 OpenRepo::Sha256(ref r) => run_image_objects::<Sha256HashValue>(r, name).await,
1251 OpenRepo::Sha512(ref r) => run_image_objects::<Sha512HashValue>(r, name).await,
1252 }
1253 }
1254
1255 async fn list_image_refs(
1257 &self,
1258 handle: u64,
1259 ) -> std::result::Result<ListImageRefsReply, RepositoryError> {
1260 match self.lookup_repo(handle)? {
1261 OpenRepo::Sha256(ref r) => run_list_image_refs::<Sha256HashValue>(r),
1262 OpenRepo::Sha512(ref r) => run_list_image_refs::<Sha512HashValue>(r),
1263 }
1264 }
1265
1266 #[zlink(return_fds)]
1272 async fn mount(
1273 &self,
1274 handle: u64,
1275 name: String,
1276 options: MountParams,
1277 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1278 ) -> (
1279 std::result::Result<MountReply, RepositoryError>,
1280 Vec<std::os::fd::OwnedFd>,
1281 ) {
1282 let result = match self.lookup_repo(handle) {
1283 Ok(OpenRepo::Sha256(ref r)) => {
1284 run_mount::<Sha256HashValue>(r, &name, &options, fds)
1285 }
1286 Ok(OpenRepo::Sha512(ref r)) => {
1287 run_mount::<Sha512HashValue>(r, &name, &options, fds)
1288 }
1289 Err(e) => Err(e),
1290 };
1291 match result {
1292 Ok((reply, fds)) => (Ok(reply), fds),
1293 Err(e) => (Err(e), vec![]),
1294 }
1295 }
1296
1297 #[zlink(interface = "org.composefs.Oci")]
1308 async fn list_images(
1309 &self,
1310 handle: u64,
1311 filter: Option<String>,
1312 ) -> std::result::Result<ListImagesReply, OciError> {
1313 let images = match self.lookup_oci(handle)? {
1314 OpenRepo::Sha256(ref r) => run_list_images::<Sha256HashValue>(r, filter).await,
1315 OpenRepo::Sha512(ref r) => run_list_images::<Sha512HashValue>(r, filter).await,
1316 }?;
1317 Ok(ListImagesReply { images })
1318 }
1319
1320 #[zlink(interface = "org.composefs.Oci", rename = "Check")]
1326 async fn oci_fsck(
1327 &self,
1328 handle: u64,
1329 image: Option<String>,
1330 ) -> std::result::Result<OciFsckReply, OciError> {
1331 match self.lookup_oci(handle)? {
1332 OpenRepo::Sha256(ref r) => run_oci_fsck::<Sha256HashValue>(r, image).await,
1333 OpenRepo::Sha512(ref r) => run_oci_fsck::<Sha512HashValue>(r, image).await,
1334 }
1335 }
1336
1337 #[zlink(interface = "org.composefs.Oci")]
1339 async fn inspect(
1340 &self,
1341 handle: u64,
1342 image: String,
1343 ) -> std::result::Result<OciInspectReply, OciError> {
1344 match self.lookup_oci(handle)? {
1345 OpenRepo::Sha256(ref r) => run_inspect::<Sha256HashValue>(r, image).await,
1346 OpenRepo::Sha512(ref r) => run_inspect::<Sha512HashValue>(r, image).await,
1347 }
1348 }
1349
1350 #[zlink(interface = "org.composefs.Oci")]
1352 async fn tag(
1353 &self,
1354 handle: u64,
1355 manifest_digest: String,
1356 name: String,
1357 ) -> std::result::Result<(), OciError> {
1358 match self.lookup_oci(handle)? {
1359 OpenRepo::Sha256(ref r) => {
1360 run_tag::<Sha256HashValue>(r, manifest_digest, name).await
1361 }
1362 OpenRepo::Sha512(ref r) => {
1363 run_tag::<Sha512HashValue>(r, manifest_digest, name).await
1364 }
1365 }
1366 }
1367
1368 #[zlink(interface = "org.composefs.Oci")]
1370 async fn untag(&self, handle: u64, name: String) -> std::result::Result<(), OciError> {
1371 match self.lookup_oci(handle)? {
1372 OpenRepo::Sha256(ref r) => run_untag::<Sha256HashValue>(r, name).await,
1373 OpenRepo::Sha512(ref r) => run_untag::<Sha512HashValue>(r, name).await,
1374 }
1375 }
1376
1377 #[zlink(interface = "org.composefs.Oci")]
1379 async fn compute_id(
1380 &self,
1381 handle: u64,
1382 image: String,
1383 verity: Option<String>,
1384 bootable: bool,
1385 xattrs: Option<composefs_oci::XattrFiltering>,
1386 ) -> std::result::Result<OciComputeIdReply, OciError> {
1387 match self.lookup_oci(handle)? {
1388 OpenRepo::Sha256(ref r) => {
1389 run_compute_id::<Sha256HashValue>(r, image, verity, bootable, xattrs).await
1390 }
1391 OpenRepo::Sha512(ref r) => {
1392 run_compute_id::<Sha512HashValue>(r, image, verity, bootable, xattrs).await
1393 }
1394 }
1395 }
1396
1397 #[zlink(interface = "org.composefs.Oci", more)]
1412 #[allow(clippy::too_many_arguments)]
1413 async fn pull(
1414 &self,
1415 more: bool,
1416 handle: u64,
1417 image: String,
1418 name: Option<String>,
1419 local_fetch: String,
1420 storage_root: Option<String>,
1421 bootable: bool,
1422 xattrs: Option<composefs_oci::XattrFiltering>,
1423 expected_digest: Option<String>,
1424 ) -> impl zlink::futures_util::Stream<
1425 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
1426 > {
1427 let lf = parse_local_fetch(&local_fetch);
1428 let sr = storage_root.map(std::path::PathBuf::from);
1429 match self.repos.get(&handle).map(|entry| &entry.repo) {
1434 Some(OpenRepo::Sha256(r)) => pull_stream::<Sha256HashValue>(
1435 r.clone(),
1436 image,
1437 name,
1438 lf,
1439 sr,
1440 bootable,
1441 xattrs,
1442 expected_digest,
1443 more,
1444 ),
1445 Some(OpenRepo::Sha512(r)) => pull_stream::<Sha512HashValue>(
1446 r.clone(),
1447 image,
1448 name,
1449 lf,
1450 sr,
1451 bootable,
1452 xattrs,
1453 expected_digest,
1454 more,
1455 ),
1456 None => {
1457 use zlink::futures_util::stream;
1458 Box::pin(stream::once(async move {
1459 Err(OciError::InvalidHandle { handle })
1460 }))
1461 }
1462 }
1463 }
1464
1465 #[zlink(interface = "org.composefs.Oci", return_fds)]
1472 async fn oci_mount(
1473 &self,
1474 handle: u64,
1475 image: String,
1476 bootable: bool,
1477 options: MountParams,
1478 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1479 ) -> (
1480 std::result::Result<MountReply, OciError>,
1481 Vec<std::os::fd::OwnedFd>,
1482 ) {
1483 let result = match self.lookup_oci(handle) {
1484 Ok(OpenRepo::Sha256(ref r)) => {
1485 run_oci_mount::<Sha256HashValue>(r, &image, bootable, &options, fds)
1486 }
1487 Ok(OpenRepo::Sha512(ref r)) => {
1488 run_oci_mount::<Sha512HashValue>(r, &image, bootable, &options, fds)
1489 }
1490 Err(e) => Err(e),
1491 };
1492 match result {
1493 Ok((reply, fds)) => (Ok(reply), fds),
1494 Err(e) => (Err(e), vec![]),
1495 }
1496 }
1497
1498 #[zlink(interface = "org.composefs.Oci")]
1508 async fn get_info(&self) -> std::result::Result<GetInfoReply, OciError> {
1509 Ok(GetInfoReply {
1510 features: vec!["splitdirfdstream-v0".into()],
1511 })
1512 }
1513
1514 #[zlink(interface = "org.composefs.Oci")]
1519 async fn has_layer(
1520 &self,
1521 handle: u64,
1522 diff_id: String,
1523 ) -> std::result::Result<HasLayerReply, OciError> {
1524 let diff_id_parsed: composefs_oci::OciDigest =
1525 diff_id.parse().map_err(|e| OciError::InvalidDigest {
1526 message: format!("{e}"),
1527 })?;
1528 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1529
1530 fn check<ObjectID: FsVerityHashValue>(
1531 repo: &composefs::repository::Repository<ObjectID>,
1532 content_id: &str,
1533 ) -> std::result::Result<HasLayerReply, OciError> {
1534 match repo
1535 .has_stream(content_id)
1536 .map_err(|e| OciError::InternalError {
1537 message: format!("{e:#}"),
1538 })? {
1539 Some(verity) => Ok(HasLayerReply {
1540 present: true,
1541 layer_verity: Some(verity.to_hex()),
1542 }),
1543 None => Ok(HasLayerReply {
1544 present: false,
1545 layer_verity: None,
1546 }),
1547 }
1548 }
1549
1550 match self.lookup_oci(handle)? {
1551 OpenRepo::Sha256(ref r) => check::<Sha256HashValue>(r, &content_id),
1552 OpenRepo::Sha512(ref r) => check::<Sha512HashValue>(r, &content_id),
1553 }
1554 }
1555
1556 #[zlink(interface = "org.composefs.Oci", more, return_fds)]
1578 async fn get_layer(
1579 &self,
1580 more: bool,
1581 handle: u64,
1582 params: GetLayerParams,
1583 #[zlink(fds)] _fds: Vec<std::os::fd::OwnedFd>,
1584 ) -> impl zlink::futures_util::Stream<
1585 Item = (
1586 std::result::Result<zlink::Reply<GetLayerReply>, OciError>,
1587 Vec<std::os::fd::OwnedFd>,
1588 ),
1589 > + Unpin {
1590 use zlink::futures_util::stream::{self, StreamExt as _};
1591
1592 type StreamItem = (
1593 std::result::Result<zlink::Reply<GetLayerReply>, OciError>,
1594 Vec<std::os::fd::OwnedFd>,
1595 );
1596
1597 macro_rules! err_stream {
1598 ($e:expr) => {
1599 return stream::iter(std::iter::once::<StreamItem>((Err($e), vec![])))
1600 .left_stream()
1601 };
1602 }
1603
1604 let diff_id = match params.diff_id {
1606 Some(d) => d,
1607 None => err_stream!(OciError::InvalidRequest {
1608 message: "GetLayer: diff_id is required for the repo service".into(),
1609 }),
1610 };
1611
1612 let diff_id_parsed: composefs_oci::OciDigest = match diff_id.parse() {
1614 Ok(d) => d,
1615 Err(e) => err_stream!(OciError::InvalidDigest {
1616 message: format!("{e}"),
1617 }),
1618 };
1619 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1620
1621 fn do_serve_get_layer<ObjectID: FsVerityHashValue>(
1623 repo: &std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1624 content_id: &str,
1625 diff_id_str: &str,
1626 more: bool,
1627 ) -> std::result::Result<composefs_oci::layer_transport::GetLayerFrames, OciError>
1628 {
1629 let verity = repo
1630 .has_stream(content_id)
1631 .map_err(|e| OciError::InternalError {
1632 message: format!("{e:#}"),
1633 })?
1634 .ok_or_else(|| OciError::NoSuchLayer {
1635 diff_id: diff_id_str.to_string(),
1636 })?;
1637
1638 let seed = seed_from_id(content_id);
1639 let source = RepoLayerSource {
1640 repo: repo.clone(),
1641 layer_verity: verity,
1642 };
1643
1644 serve_get_layer(source, seed, more).map_err(|e| match e {
1645 composefs_oci::layer_transport::ServeGetLayerError::FdLimitExceeded(e) => {
1646 OciError::FdLimitExceeded {
1647 fd_count: e.fd_count as u64,
1648 max_per_frame: e.max_per_frame as u64,
1649 }
1650 }
1651 composefs_oci::layer_transport::ServeGetLayerError::Other(e) => {
1652 OciError::InternalError {
1653 message: format!("{e:#}"),
1654 }
1655 }
1656 })
1657 }
1658
1659 let frames = match self.lookup_oci(handle) {
1660 Ok(OpenRepo::Sha256(ref r)) => {
1661 do_serve_get_layer::<Sha256HashValue>(r, &content_id, &diff_id, more)
1662 }
1663 Ok(OpenRepo::Sha512(ref r)) => {
1664 do_serve_get_layer::<Sha512HashValue>(r, &content_id, &diff_id, more)
1665 }
1666 Err(e) => Err(e),
1667 };
1668
1669 let frames = match frames {
1670 Ok(f) => f,
1671 Err(e) => err_stream!(e),
1672 };
1673
1674 let dir_count = frames.dir_count;
1675 let batches = frames.batches;
1676 let n_frames = batches.len();
1677 let reply = GetLayerReply { dir_count };
1678
1679 stream::iter(batches.into_iter().enumerate().map(move |(i, batch)| {
1680 let is_last = i == n_frames - 1;
1681 (
1682 Ok(zlink::Reply::new(Some(reply.clone())).set_continues(Some(!is_last))),
1683 batch,
1684 )
1685 }))
1686 .right_stream()
1687 }
1688
1689 #[zlink(interface = "org.composefs.Oci")]
1706 async fn put_layer(
1707 &self,
1708 handle: u64,
1709 diff_id: String,
1710 zerocopy: bool,
1711 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
1712 ) -> std::result::Result<PutLayerReply, OciError> {
1713 if fds.len() < 2 {
1715 return Err(OciError::InvalidRequest {
1716 message: format!(
1717 "expected at least 2 fds (1 pipe + >=1 dir fd), got {}",
1718 fds.len()
1719 ),
1720 });
1721 }
1722
1723 let diff_id_parsed: composefs_oci::OciDigest =
1724 diff_id.parse().map_err(|e| OciError::InvalidDigest {
1725 message: format!("{e}"),
1726 })?;
1727
1728 let content_id = composefs_oci::layer_content_id(&diff_id_parsed);
1729
1730 let already_present = match self.lookup_oci(handle)? {
1734 OpenRepo::Sha256(ref r) => r
1735 .has_stream(&content_id)
1736 .map_err(|e| OciError::InternalError {
1737 message: format!("{e:#}"),
1738 })?
1739 .is_some(),
1740 OpenRepo::Sha512(ref r) => r
1741 .has_stream(&content_id)
1742 .map_err(|e| OciError::InternalError {
1743 message: format!("{e:#}"),
1744 })?
1745 .is_some(),
1746 };
1747
1748 let mut fds = fds;
1750 let pipe_read = fds.remove(0);
1751 let dir_fds = fds; async fn run_put_layer<ObjectID: FsVerityHashValue>(
1754 repo: std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1755 pipe_read: std::os::fd::OwnedFd,
1756 dir_fds: Vec<std::os::fd::OwnedFd>,
1757 diff_id: composefs_oci::OciDigest,
1758 zerocopy: bool,
1759 already_present: bool,
1760 ) -> std::result::Result<PutLayerReply, OciError> {
1761 tokio::task::spawn_blocking(move || {
1762 composefs_oci::layer_sync::drain_splitdirfdstream_verified(
1763 repo,
1764 pipe_read,
1765 dir_fds,
1766 &diff_id,
1767 zerocopy,
1768 composefs::repository::ImportContext::default(),
1769 )
1770 })
1771 .await
1772 .map_err(|e| OciError::InternalError {
1773 message: format!("spawn_blocking panic: {e}"),
1774 })?
1775 .map(|(verity, stats, _ctx)| PutLayerReply {
1776 layer_verity: verity.to_hex(),
1777 already_present,
1778 objects_reflinked: stats.objects_reflinked,
1779 objects_hardlinked: stats.objects_hardlinked,
1780 objects_copied: stats.objects_copied,
1781 objects_already_present: stats.objects_already_present,
1782 })
1783 .map_err(|e| match e {
1784 composefs_oci::layer_sync::VerifiedDrainError::DiffIdMismatch {
1785 expected,
1786 actual,
1787 } => OciError::DiffIdMismatch { expected, actual },
1788 composefs_oci::layer_sync::VerifiedDrainError::Other(err) => {
1789 OciError::InternalError {
1790 message: format!("{err:#}"),
1791 }
1792 }
1793 })
1794 }
1795
1796 match self.lookup_oci(handle)? {
1797 OpenRepo::Sha256(ref r) => {
1798 run_put_layer::<Sha256HashValue>(
1799 r.clone(),
1800 pipe_read,
1801 dir_fds,
1802 diff_id_parsed,
1803 zerocopy,
1804 already_present,
1805 )
1806 .await
1807 }
1808 OpenRepo::Sha512(ref r) => {
1809 run_put_layer::<Sha512HashValue>(
1810 r.clone(),
1811 pipe_read,
1812 dir_fds,
1813 diff_id_parsed,
1814 zerocopy,
1815 already_present,
1816 )
1817 .await
1818 }
1819 }
1820 }
1821
1822 #[zlink(interface = "org.composefs.Oci")]
1832 async fn finalize_image(
1833 &self,
1834 handle: u64,
1835 manifest_json: String,
1836 config_json: String,
1837 layers: Vec<LayerRef>,
1838 name: Option<String>,
1839 ) -> std::result::Result<FinalizeImageReply, OciError> {
1840 async fn run_finalize<ObjectID: FsVerityHashValue>(
1841 repo: std::sync::Arc<composefs::repository::Repository<ObjectID>>,
1842 manifest_json: String,
1843 config_json: String,
1844 layers: Vec<LayerRef>,
1845 name: Option<String>,
1846 ) -> std::result::Result<FinalizeImageReply, OciError> {
1847 let mut layer_refs: Vec<(composefs_oci::OciDigest, ObjectID)> =
1849 Vec::with_capacity(layers.len());
1850 for lr in &layers {
1851 let diff_id: composefs_oci::OciDigest =
1852 lr.diff_id.parse().map_err(|e| OciError::InvalidDigest {
1853 message: format!("diff_id {:?}: {e}", lr.diff_id),
1854 })?;
1855 let verity = ObjectID::from_hex(&lr.layer_verity).map_err(|e| {
1856 OciError::InvalidDigest {
1857 message: format!("layer_verity {:?}: {e}", lr.layer_verity),
1858 }
1859 })?;
1860 layer_refs.push((diff_id, verity));
1861 }
1862
1863 tokio::task::spawn_blocking(move || {
1864 composefs_oci::layer_sync::finalize_oci_image(
1865 &repo,
1866 manifest_json.as_bytes(),
1867 config_json.as_bytes(),
1868 &layer_refs,
1869 name.as_deref(),
1870 None,
1874 )
1875 })
1876 .await
1877 .map_err(|e| OciError::InternalError {
1878 message: format!("spawn_blocking panic: {e}"),
1879 })?
1880 .map(
1881 |((manifest_digest, manifest_verity), (config_digest, config_verity))| {
1882 FinalizeImageReply {
1883 manifest_digest: manifest_digest.to_string(),
1884 manifest_verity: manifest_verity.to_hex(),
1885 config_digest: config_digest.to_string(),
1886 config_verity: config_verity.to_hex(),
1887 }
1888 },
1889 )
1890 .map_err(|e| OciError::InternalError {
1891 message: format!("{e:#}"),
1892 })
1893 }
1894
1895 match self.lookup_oci(handle)? {
1896 OpenRepo::Sha256(ref r) => {
1897 run_finalize::<Sha256HashValue>(
1898 r.clone(),
1899 manifest_json,
1900 config_json,
1901 layers,
1902 name,
1903 )
1904 .await
1905 }
1906 OpenRepo::Sha512(ref r) => {
1907 run_finalize::<Sha512HashValue>(
1908 r.clone(),
1909 manifest_json,
1910 config_json,
1911 layers,
1912 name,
1913 )
1914 .await
1915 }
1916 }
1917 }
1918 }
1919}
1920
1921#[derive(Debug)]
1928pub(crate) struct ActivatedListener {
1929 conn: Option<zlink::Connection<zlink::tokio::unix::Stream>>,
1931}
1932
1933impl zlink::Listener for ActivatedListener {
1934 type Socket = zlink::tokio::unix::Stream;
1935
1936 async fn accept(&mut self) -> zlink::Result<Option<zlink::Connection<Self::Socket>>> {
1937 match self.conn.take() {
1938 Some(conn) => Ok(Some(conn)),
1939 None => std::future::pending().await,
1940 }
1941 }
1942}
1943
1944pub(crate) enum ActivatedSocket {
1946 Connected(ActivatedListener),
1949 Listening(zlink::tokio::unix::Listener),
1952}
1953
1954#[allow(unsafe_code)]
1967pub(crate) fn try_activated_listener() -> Result<Option<ActivatedSocket>> {
1968 use std::os::fd::{FromRawFd as _, IntoRawFd as _, OwnedFd};
1969
1970 let fds = libsystemd::activation::receive_descriptors(true)
1971 .map_err(|e| anyhow::anyhow!("Failed to receive activation fds: {e}"))?;
1972
1973 let fd = match fds.into_iter().next() {
1974 Some(fd) => fd,
1975 None => return Ok(None),
1976 };
1977
1978 let owned: OwnedFd = unsafe { OwnedFd::from_raw_fd(fd.into_raw_fd()) };
1982
1983 let is_listening = rustix::net::sockopt::socket_acceptconn(&owned)
1986 .context("querying SO_ACCEPTCONN on activation fd")?;
1987
1988 if is_listening {
1989 let listener = zlink::tokio::unix::Listener::try_from(owned)
1992 .context("converting listening activation fd to zlink Listener")?;
1993 Ok(Some(ActivatedSocket::Listening(listener)))
1994 } else {
1995 let std_stream = std::os::unix::net::UnixStream::from(owned);
1998 std_stream
1999 .set_nonblocking(true)
2000 .context("setting systemd socket to non-blocking")?;
2001 let tokio_stream = tokio::net::UnixStream::from_std(std_stream)
2002 .context("converting systemd UnixStream to tokio")?;
2003 let zlink_stream =
2004 zlink::tokio::unix::Stream::try_from(tokio_stream).map_err(|e| anyhow::anyhow!(e))?;
2005 let conn = zlink::Connection::new(zlink_stream);
2006 Ok(Some(ActivatedSocket::Connected(ActivatedListener {
2007 conn: Some(conn),
2008 })))
2009 }
2010}
2011
2012pub(crate) async fn serve_activated<S>(service: S, listener: ActivatedListener) -> Result<()>
2022where
2023 S: zlink::Service<zlink::tokio::unix::Stream>,
2024{
2025 log::info!("Listening on systemd-activated socket");
2026 let server = zlink::Server::new(listener, service);
2027 tokio::task::LocalSet::new()
2028 .run_until(server.run())
2029 .await
2030 .context("running varlink server (activated)")
2031}
2032
2033pub(crate) async fn serve_on_listener<S>(
2039 service: S,
2040 listener: zlink::tokio::unix::Listener,
2041) -> Result<()>
2042where
2043 S: zlink::Service<zlink::tokio::unix::Stream>,
2044{
2045 let server = zlink::Server::new(listener, service);
2046 tokio::task::LocalSet::new()
2047 .run_until(server.run())
2048 .await
2049 .context("running varlink server")
2050}
2051
2052pub(crate) async fn serve<S>(service: S, address: Option<&Path>) -> Result<()>
2060where
2061 S: zlink::Service<zlink::tokio::unix::Stream>,
2062{
2063 match try_activated_listener()? {
2064 Some(ActivatedSocket::Connected(l)) => return serve_activated(service, l).await,
2065 Some(ActivatedSocket::Listening(listener)) => {
2066 log::info!("Listening on systemd-activated socket");
2067 return serve_on_listener(service, listener).await;
2068 }
2069 None => {}
2070 }
2071 let address = address.context("no --address given and not socket-activated")?;
2072 let listener = zlink::tokio::unix::bind(address)
2073 .with_context(|| format!("binding varlink socket at {}", address.display()))?;
2074 log::info!("Listening on {}", address.display());
2075 serve_on_listener(service, listener).await
2076}
2077
2078#[cfg(feature = "oci")]
2083pub mod oci {
2084 use super::*;
2085
2086 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2088 pub struct ImageEntry {
2089 pub name: String,
2091 pub manifest_digest: String,
2093 pub is_container: bool,
2095 pub architecture: String,
2097 pub os: String,
2099 pub created: Option<String>,
2101 pub layer_count: u64,
2103 pub referrer_count: u64,
2105 }
2106
2107 impl From<&composefs_oci::oci_image::ImageInfo> for ImageEntry {
2108 fn from(info: &composefs_oci::oci_image::ImageInfo) -> Self {
2109 Self {
2110 name: info.name.clone(),
2111 manifest_digest: info.manifest_digest.to_string(),
2112 is_container: info.is_container,
2113 architecture: info.architecture.clone(),
2114 os: info.os.clone(),
2115 created: info.created.clone(),
2116 layer_count: info.layer_count as u64,
2117 referrer_count: info.referrer_count as u64,
2118 }
2119 }
2120 }
2121
2122 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2124 pub struct ListImagesReply {
2125 pub images: Vec<ImageEntry>,
2127 }
2128
2129 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2134 pub struct OciFsckReply {
2135 pub ok: bool,
2137 pub images_checked: u64,
2139 pub images_corrupted: u64,
2141 pub errors: Vec<String>,
2143 pub repo: FsckReply,
2145 }
2146
2147 impl From<&composefs_oci::OciFsckResult> for OciFsckReply {
2148 fn from(result: &composefs_oci::OciFsckResult) -> Self {
2149 Self {
2150 ok: result.is_ok(),
2151 images_checked: result.images_checked(),
2152 images_corrupted: result.images_corrupted(),
2153 errors: result.errors().iter().map(|e| e.to_string()).collect(),
2154 repo: FsckReply::from(result.repo_result()),
2155 }
2156 }
2157 }
2158
2159 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2161 pub struct OciInspectReply {
2162 pub manifest: String,
2164 pub config: String,
2166 pub referrers: Vec<String>,
2168 pub composefs_erofs: Option<String>,
2170 pub composefs_boot_erofs: Option<String>,
2176 }
2177
2178 impl OciInspectReply {
2179 pub fn from_image<ObjectID: FsVerityHashValue>(
2182 repo: &Repository<ObjectID>,
2183 img: &composefs_oci::oci_image::OciImage<ObjectID>,
2184 ) -> anyhow::Result<Self> {
2185 let manifest = String::from_utf8(img.read_manifest_json(repo)?)
2186 .context("manifest is not valid UTF-8")?;
2187 let config = String::from_utf8(img.read_config_json(repo)?)
2188 .context("config is not valid UTF-8")?;
2189 let referrers = composefs_oci::oci_image::list_referrers(repo, img.manifest_digest())?
2190 .iter()
2191 .map(|(digest, _verity)| digest.to_string())
2192 .collect();
2193 Ok(Self {
2194 manifest,
2195 config,
2196 referrers,
2197 composefs_erofs: img.image_ref(repo.erofs_version()).map(|id| id.to_hex()),
2198 composefs_boot_erofs: img
2199 .boot_image_ref(repo.erofs_version())
2200 .map(|id| id.to_hex()),
2201 })
2202 }
2203 }
2204
2205 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2207 pub struct OciComputeIdReply {
2208 pub image_id: String,
2210 }
2211
2212 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2225 pub struct PullProgress {
2226 #[serde(skip_serializing_if = "Option::is_none", default)]
2228 pub started: Option<Started>,
2229 #[serde(skip_serializing_if = "Option::is_none", default)]
2231 pub progress: Option<Progress>,
2232 #[serde(skip_serializing_if = "Option::is_none", default)]
2234 pub skipped: Option<Skipped>,
2235 #[serde(skip_serializing_if = "Option::is_none", default)]
2237 pub done: Option<Done>,
2238 #[serde(skip_serializing_if = "Option::is_none", default)]
2240 pub message: Option<String>,
2241 #[serde(skip_serializing_if = "Option::is_none", default)]
2244 pub completed: Option<Completed>,
2245 }
2246
2247 #[derive(Debug, Clone, Copy, Serialize, Deserialize, zlink::introspect::Type)]
2249 pub enum ProgressUnit {
2250 Bytes,
2252 Items,
2254 }
2255
2256 impl From<composefs::progress::ProgressUnit> for ProgressUnit {
2257 fn from(unit: composefs::progress::ProgressUnit) -> Self {
2258 use composefs::progress::ProgressUnit as U;
2259 match unit {
2260 U::Bytes => ProgressUnit::Bytes,
2261 U::Items => ProgressUnit::Items,
2262 _ => ProgressUnit::Items,
2264 }
2265 }
2266 }
2267
2268 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2270 pub struct Started {
2271 pub id: String,
2273 pub total: Option<u64>,
2275 pub unit: ProgressUnit,
2277 }
2278
2279 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2281 pub struct Progress {
2282 pub id: String,
2284 pub fetched: u64,
2286 pub total: Option<u64>,
2288 }
2289
2290 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2292 pub struct Skipped {
2293 pub id: String,
2295 }
2296
2297 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2299 pub struct Done {
2300 pub id: String,
2302 pub transferred: u64,
2304 }
2305
2306 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2308 pub struct Completed {
2309 pub manifest_digest: String,
2311 pub config_digest: String,
2313 pub manifest_verity: String,
2315 pub config_verity: String,
2317 pub stats: String,
2319 pub boot_image: Option<String>,
2322 pub boot_image_mode: Option<String>,
2325 pub boot_image_format_version: Option<String>,
2328 }
2329
2330 impl PullProgress {
2331 fn empty() -> Self {
2334 PullProgress {
2335 started: None,
2336 progress: None,
2337 skipped: None,
2338 done: None,
2339 message: None,
2340 completed: None,
2341 }
2342 }
2343 }
2344
2345 impl From<composefs::progress::ProgressEvent> for PullProgress {
2346 fn from(event: composefs::progress::ProgressEvent) -> Self {
2350 use composefs::progress::ProgressEvent;
2351
2352 let mut p = PullProgress::empty();
2353 match event {
2354 ProgressEvent::Started { id, total, unit } => {
2355 p.started = Some(Started {
2356 id: id.into_inner(),
2357 total,
2358 unit: unit.into(),
2359 });
2360 }
2361 ProgressEvent::Progress { id, fetched, total } => {
2362 p.progress = Some(Progress {
2363 id: id.into_inner(),
2364 fetched,
2365 total,
2366 });
2367 }
2368 ProgressEvent::Skipped { id } => {
2369 p.skipped = Some(Skipped {
2370 id: id.into_inner(),
2371 });
2372 }
2373 ProgressEvent::Done { id, transferred } => {
2374 p.done = Some(Done {
2375 id: id.into_inner(),
2376 transferred,
2377 });
2378 }
2379 ProgressEvent::Message(s) => {
2380 p.message = Some(s);
2381 }
2382 other => {
2385 p.message = Some(format!("{other:?}"));
2386 }
2387 }
2388 p
2389 }
2390 }
2391
2392 struct ChannelReporter {
2395 tx: tokio::sync::mpsc::UnboundedSender<PullProgress>,
2396 }
2397
2398 impl std::fmt::Debug for ChannelReporter {
2399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2400 f.debug_struct("ChannelReporter").finish_non_exhaustive()
2401 }
2402 }
2403
2404 impl composefs::progress::ProgressReporter for ChannelReporter {
2405 fn report(&self, event: composefs::progress::ProgressEvent) {
2406 let _ = self.tx.send(PullProgress::from(event));
2409 }
2410 }
2411
2412 struct AbortOnDrop {
2418 handle: Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>>,
2419 }
2420
2421 impl AbortOnDrop {
2422 fn take(&mut self) -> Option<tokio::task::JoinHandle<std::result::Result<(), OciError>>> {
2424 self.handle.take()
2425 }
2426 }
2427
2428 impl std::fmt::Debug for AbortOnDrop {
2429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2430 f.debug_struct("AbortOnDrop").finish_non_exhaustive()
2431 }
2432 }
2433
2434 impl Drop for AbortOnDrop {
2435 fn drop(&mut self) {
2436 if let Some(handle) = &self.handle {
2437 handle.abort();
2438 }
2439 }
2440 }
2441
2442 pub(crate) fn parse_local_fetch(value: &str) -> composefs_oci::LocalFetchOpt {
2446 use composefs_oci::LocalFetchOpt;
2447 match value {
2448 "auto" | "if-possible" => LocalFetchOpt::IfPossible,
2449 "zerocopy" | "zero-copy" => LocalFetchOpt::ZeroCopy,
2450 _ => LocalFetchOpt::Disabled,
2451 }
2452 }
2453
2454 pub(crate) fn want_bootable_pull(
2471 bootable: bool,
2472 has_expected_digest: bool,
2473 xattrs: Option<composefs_oci::XattrFiltering>,
2474 ) -> bool {
2475 bootable
2476 && !has_expected_digest
2477 && xattrs.unwrap_or_default() == composefs_oci::XattrFiltering::default()
2478 }
2479
2480 #[cfg(test)]
2481 mod want_bootable_pull_tests {
2482 use super::want_bootable_pull;
2483 use composefs_oci::XattrFiltering;
2484
2485 #[test]
2486 fn matches_expected_decision_table() {
2487 let default_mode = XattrFiltering::default();
2488 let non_default_mode = XattrFiltering::KeepUserXattrs;
2489 assert_ne!(default_mode, non_default_mode, "test fixture sanity check");
2490
2491 let cases = [
2493 (false, false, None, false),
2494 (false, false, Some(default_mode), false),
2495 (true, false, None, true),
2496 (true, false, Some(default_mode), true),
2497 (true, false, Some(non_default_mode), false),
2498 (true, true, None, false),
2499 (true, true, Some(default_mode), false),
2500 (false, true, None, false),
2501 ];
2502 for (bootable, has_expected_digest, xattrs, expected) in cases {
2503 assert_eq!(
2504 want_bootable_pull(bootable, has_expected_digest, xattrs),
2505 expected,
2506 "bootable={bootable} has_expected_digest={has_expected_digest} \
2507 xattrs={xattrs:?}"
2508 );
2509 }
2510 }
2511 }
2512
2513 #[allow(clippy::too_many_arguments)]
2535 pub(crate) fn pull_stream<ObjectID: FsVerityHashValue>(
2536 repo: Arc<Repository<ObjectID>>,
2537 image: String,
2538 name: Option<String>,
2539 local_fetch: composefs_oci::LocalFetchOpt,
2540 storage_root: Option<PathBuf>,
2541 bootable: bool,
2542 xattrs: Option<composefs_oci::XattrFiltering>,
2543 expected_digest: Option<String>,
2544 more: bool,
2545 ) -> std::pin::Pin<
2546 Box<
2547 dyn zlink::futures_util::Stream<
2548 Item = std::result::Result<zlink::Reply<PullProgress>, OciError>,
2549 >,
2550 >,
2551 > {
2552 use zlink::futures_util::stream;
2553
2554 if expected_digest.is_some() && !bootable {
2555 return Box::pin(stream::once(async move {
2556 Err(OciError::InvalidRequest {
2557 message: "expected_digest requires bootable".to_string(),
2558 })
2559 }));
2560 }
2561 if expected_digest.is_some() && xattrs.is_some() {
2562 return Box::pin(stream::once(async move {
2563 Err(OciError::InvalidRequest {
2564 message: "expected_digest and xattrs are mutually exclusive: \
2565 expected_digest searches every mode"
2566 .to_string(),
2567 })
2568 }));
2569 }
2570 let expected_digest = match expected_digest
2573 .map(|hex| ObjectID::from_hex(&hex).map(|id| (hex, id)))
2574 .transpose()
2575 {
2576 Ok(parsed) => parsed,
2577 Err(e) => {
2578 return Box::pin(stream::once(async move {
2579 Err(OciError::InvalidDigest {
2580 message: format!("invalid expected_digest: {e}"),
2581 })
2582 }));
2583 }
2584 };
2585
2586 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<PullProgress>();
2587 let reporter: Option<composefs::progress::SharedReporter> = if more {
2589 Some(std::sync::Arc::new(ChannelReporter { tx: tx.clone() }))
2590 } else {
2591 None
2592 };
2593
2594 let task_tx = tx.clone();
2599 let use_bootable_opt = want_bootable_pull(bootable, expected_digest.is_some(), xattrs);
2600 let handle = tokio::task::spawn_local(async move {
2601 let opts = composefs_oci::PullOptions {
2602 local_fetch,
2603 storage_root: storage_root.as_deref(),
2604 progress: reporter,
2605 bootable: use_bootable_opt,
2606 ..Default::default()
2607 };
2608 let result = composefs_oci::pull(&repo, &image, name.as_deref(), opts)
2609 .await
2610 .map_err(|e| OciError::InternalError {
2611 message: format!("{e:#}"),
2612 })?;
2613
2614 let (boot_image, boot_image_mode, boot_image_format_version) = if !bootable {
2615 (None, None, None)
2616 } else if let Some((expected_hex, expected)) = expected_digest {
2617 match composefs_oci::find_matching_boot_image(
2618 &repo,
2619 &result.manifest_digest,
2620 &expected,
2621 )
2622 .map_err(|e| OciError::InternalError {
2623 message: format!("{e:#}"),
2624 })? {
2625 composefs_oci::BootImageMatch::Found {
2626 mode,
2627 version,
2628 digest,
2629 } => (
2630 Some(digest.to_hex()),
2631 Some(mode.to_string()),
2632 Some(format!("{version:?}")),
2633 ),
2634 composefs_oci::BootImageMatch::NotFound(tried) => {
2635 return Err(OciError::BootImageMismatch {
2636 expected: expected_hex,
2637 tried: tried as u64,
2638 });
2639 }
2640 }
2641 } else {
2642 let mode = xattrs.unwrap_or_default();
2643 let transform_opts = composefs_oci::OciTransformOptions { xattrs: mode };
2644 let id = composefs_oci::generate_boot_image(
2645 &repo,
2646 &result.manifest_digest,
2647 &transform_opts,
2648 )
2649 .map_err(|e| OciError::InternalError {
2650 message: format!("{e:#}"),
2651 })?;
2652 (
2653 Some(id.to_hex()),
2654 Some(mode.to_string()),
2655 Some(format!("{:?}", repo.erofs_version())),
2656 )
2657 };
2658
2659 let completed = PullProgress {
2660 completed: Some(Completed {
2661 manifest_digest: result.manifest_digest.to_string(),
2662 config_digest: result.config_digest.to_string(),
2663 manifest_verity: result.manifest_verity.to_hex(),
2664 config_verity: result.config_verity.to_hex(),
2665 stats: result.stats.to_string(),
2666 boot_image,
2667 boot_image_mode,
2668 boot_image_format_version,
2669 }),
2670 ..PullProgress::empty()
2671 };
2672 let _ = task_tx.send(completed);
2674 Ok(())
2675 });
2676
2677 drop(tx);
2680
2681 struct State {
2682 rx: tokio::sync::mpsc::UnboundedReceiver<PullProgress>,
2683 handle: Option<AbortOnDrop>,
2684 done: bool,
2685 }
2686
2687 let state = State {
2688 rx,
2689 handle: Some(AbortOnDrop {
2690 handle: Some(handle),
2691 }),
2692 done: false,
2693 };
2694
2695 let stream = stream::unfold(state, |mut state| async move {
2696 if state.done {
2697 return None;
2698 }
2699 match state.rx.recv().await {
2700 Some(frame) => {
2701 let is_completed = frame.completed.is_some();
2702 if is_completed {
2703 state.done = true;
2704 if let Some(guard) = state.handle.as_mut() {
2707 let _ = guard.take();
2708 }
2709 }
2710 let reply = zlink::Reply::new(Some(frame)).set_continues(Some(!is_completed));
2711 Some((Ok(reply), state))
2712 }
2713 None => {
2714 state.done = true;
2717 let join = state.handle.as_mut().and_then(AbortOnDrop::take);
2720 let err = match join {
2721 Some(join) => match join.await {
2722 Ok(Ok(())) => OciError::InternalError {
2723 message: "pull completed without a result frame".to_string(),
2724 },
2725 Ok(Err(e)) => e,
2726 Err(_) => OciError::InternalError {
2727 message: "pull task panicked".to_string(),
2728 },
2729 },
2730 None => OciError::InternalError {
2731 message: "pull task panicked".to_string(),
2732 },
2733 };
2734 Some((Err(err), state))
2735 }
2736 }
2737 });
2738
2739 Box::pin(stream)
2740 }
2741
2742 #[derive(Debug, zlink::ReplyError, zlink::introspect::ReplyError)]
2744 #[zlink(interface = "org.composefs.Oci")]
2745 pub enum OciError {
2746 RepoNotFound {
2748 message: String,
2750 },
2751 InvalidHandle {
2753 handle: u64,
2755 },
2756 NoSuchImage {
2758 image: String,
2760 },
2761 InternalError {
2763 message: String,
2765 },
2766 NoSuchLayer {
2768 diff_id: String,
2770 },
2771 InvalidDigest {
2773 message: String,
2775 },
2776 DiffIdMismatch {
2780 expected: String,
2782 actual: String,
2784 },
2785 InvalidRequest {
2787 message: String,
2789 },
2790 FdLimitExceeded {
2794 fd_count: u64,
2796 max_per_frame: u64,
2798 },
2799 BootImageMismatch {
2804 expected: String,
2806 tried: u64,
2808 },
2809 }
2810}
2811
2812#[cfg(feature = "oci")]
2819pub mod layer_sync {
2820 use super::*;
2821
2822 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2824 pub struct GetInfoReply {
2825 pub features: Vec<String>,
2829 }
2830
2831 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2833 pub struct HasLayerReply {
2834 pub present: bool,
2836 pub layer_verity: Option<String>,
2838 }
2839
2840 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2871 pub struct GetLayerReply {
2872 pub dir_count: u32,
2875 }
2876
2877 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2884 pub struct PutLayerReply {
2885 pub layer_verity: String,
2887 pub already_present: bool,
2892
2893 #[serde(default)]
2896 pub objects_reflinked: u64,
2897 #[serde(default)]
2899 pub objects_hardlinked: u64,
2900 #[serde(default)]
2902 pub objects_copied: u64,
2903 #[serde(default)]
2905 pub objects_already_present: u64,
2906 }
2907
2908 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2914 pub struct LayerRef {
2915 pub diff_id: String,
2917 pub layer_verity: String,
2920 }
2921
2922 #[derive(Debug, Clone, Serialize, Deserialize, zlink::introspect::Type)]
2925 pub struct FinalizeImageReply {
2926 pub manifest_digest: String,
2928 pub manifest_verity: String,
2930 pub config_digest: String,
2932 pub config_verity: String,
2934 }
2935}
2936
2937pub mod proxy {
2943 #![allow(missing_docs)]
2944
2945 #[cfg(feature = "oci")]
2946 use super::layer_sync::{
2947 FinalizeImageReply, GetInfoReply, GetLayerReply, HasLayerReply, LayerRef, PutLayerReply,
2948 };
2949 #[cfg(feature = "oci")]
2950 use super::oci::{
2951 ListImagesReply, OciComputeIdReply, OciError, OciFsckReply, OciInspectReply, PullProgress,
2952 };
2953 use super::{
2954 EnsureRepositoryReply, FsckReply, GcReply, ImageObjectsReply, InitRepositoryReply,
2955 OpenRepositoryReply, RepositoryError,
2956 };
2957 #[cfg(feature = "oci")]
2958 pub use composefs_oci::varlink_types::GetLayerParams;
2959 #[cfg(feature = "oci")]
2960 use zlink::futures_util::Stream;
2961
2962 #[zlink::proxy(interface = "org.composefs.Repository")]
2964 pub trait RepositoryProxy {
2965 async fn init_repository(
2967 &mut self,
2968 path: &str,
2969 algorithm: Option<&str>,
2970 insecure: Option<bool>,
2971 ) -> zlink::Result<Result<InitRepositoryReply, RepositoryError>>;
2972
2973 async fn ensure_repository(
2975 &mut self,
2976 path: &str,
2977 algorithm: Option<&str>,
2978 insecure: Option<bool>,
2979 ) -> zlink::Result<Result<EnsureRepositoryReply, RepositoryError>>;
2980
2981 async fn open_repository(
2983 &mut self,
2984 path: Option<&str>,
2985 user: Option<bool>,
2986 system: Option<bool>,
2987 ) -> zlink::Result<Result<OpenRepositoryReply, RepositoryError>>;
2988
2989 async fn close_repository(
2991 &mut self,
2992 handle: u64,
2993 ) -> zlink::Result<Result<(), RepositoryError>>;
2994
2995 async fn fsck(
2997 &mut self,
2998 handle: u64,
2999 metadata_only: Option<bool>,
3000 ) -> zlink::Result<Result<FsckReply, RepositoryError>>;
3001
3002 async fn gc(
3004 &mut self,
3005 handle: u64,
3006 dry_run: bool,
3007 roots: Vec<String>,
3008 ) -> zlink::Result<Result<GcReply, RepositoryError>>;
3009
3010 async fn image_objects(
3012 &mut self,
3013 handle: u64,
3014 name: &str,
3015 ) -> zlink::Result<Result<ImageObjectsReply, RepositoryError>>;
3016 }
3017
3018 #[cfg(feature = "oci")]
3020 #[zlink::proxy(interface = "org.composefs.Oci")]
3021 #[allow(clippy::too_many_arguments)]
3022 pub trait OciProxy {
3023 async fn list_images(
3025 &mut self,
3026 handle: u64,
3027 filter: Option<&str>,
3028 ) -> zlink::Result<Result<ListImagesReply, OciError>>;
3029
3030 #[zlink(rename = "Check")]
3032 async fn oci_fsck(
3033 &mut self,
3034 handle: u64,
3035 image: Option<&str>,
3036 ) -> zlink::Result<Result<OciFsckReply, OciError>>;
3037
3038 async fn inspect(
3040 &mut self,
3041 handle: u64,
3042 image: &str,
3043 ) -> zlink::Result<Result<OciInspectReply, OciError>>;
3044
3045 async fn tag(
3047 &mut self,
3048 handle: u64,
3049 manifest_digest: &str,
3050 name: &str,
3051 ) -> zlink::Result<Result<(), OciError>>;
3052
3053 async fn untag(&mut self, handle: u64, name: &str) -> zlink::Result<Result<(), OciError>>;
3055
3056 async fn compute_id(
3058 &mut self,
3059 handle: u64,
3060 image: &str,
3061 verity: Option<&str>,
3062 bootable: bool,
3063 xattrs: Option<composefs_oci::XattrFiltering>,
3064 ) -> zlink::Result<Result<OciComputeIdReply, OciError>>;
3065
3066 #[zlink(more, rename = "Pull")]
3068 #[allow(clippy::too_many_arguments)]
3069 async fn pull(
3070 &mut self,
3071 handle: u64,
3072 image: &str,
3073 name: Option<&str>,
3074 local_fetch: &str,
3075 storage_root: Option<&str>,
3076 bootable: bool,
3077 xattrs: Option<composefs_oci::XattrFiltering>,
3078 expected_digest: Option<&str>,
3079 ) -> zlink::Result<impl Stream<Item = zlink::Result<Result<PullProgress, OciError>>>>;
3080
3081 async fn get_info(&mut self) -> zlink::Result<Result<GetInfoReply, OciError>>;
3083
3084 async fn has_layer(
3086 &mut self,
3087 handle: u64,
3088 diff_id: &str,
3089 ) -> zlink::Result<Result<HasLayerReply, OciError>>;
3090
3091 #[zlink(more, return_fds)]
3098 async fn get_layer(
3099 &mut self,
3100 handle: u64,
3101 params: GetLayerParams,
3102 ) -> zlink::Result<
3103 impl zlink::futures_util::Stream<
3104 Item = zlink::Result<(Result<GetLayerReply, OciError>, Vec<std::os::fd::OwnedFd>)>,
3105 >,
3106 >;
3107
3108 async fn put_layer(
3113 &mut self,
3114 handle: u64,
3115 diff_id: &str,
3116 zerocopy: bool,
3117 #[zlink(fds)] fds: Vec<std::os::fd::OwnedFd>,
3118 ) -> zlink::Result<Result<PutLayerReply, OciError>>;
3119
3120 async fn finalize_image(
3126 &mut self,
3127 handle: u64,
3128 manifest_json: &str,
3129 config_json: &str,
3130 layers: Vec<LayerRef>,
3131 name: Option<&str>,
3132 ) -> zlink::Result<Result<FinalizeImageReply, OciError>>;
3133 }
3134}
3135
3136#[cfg(feature = "oci")]
3137pub(crate) use oci::*;
3138
3139#[cfg(feature = "oci")]
3150pub(crate) fn spawn_in_process(
3151 service: CfsctlService,
3152) -> std::io::Result<(zlink::tokio::unix::Connection, std::thread::JoinHandle<()>)> {
3153 let (client_std, server_std) = std::os::unix::net::UnixStream::pair()?;
3154 client_std.set_nonblocking(true)?;
3155 server_std.set_nonblocking(true)?;
3156
3157 let client_stream = tokio::net::UnixStream::from_std(client_std)?;
3158 let client_zlink =
3159 zlink::tokio::unix::Stream::try_from(client_stream).map_err(std::io::Error::other)?;
3160 let client_conn = zlink::Connection::new(client_zlink);
3161
3162 let handle = std::thread::Builder::new()
3163 .name("cfsctl-service-server".into())
3164 .spawn(move || {
3165 let rt = match tokio::runtime::Builder::new_current_thread()
3166 .enable_all()
3167 .build()
3168 {
3169 Ok(rt) => rt,
3170 Err(e) => {
3171 log::error!("CfsctlService server runtime build failed: {e:#?}");
3172 return;
3173 }
3174 };
3175 let local = tokio::task::LocalSet::new();
3176 local.block_on(&rt, async move {
3177 let server_stream = match tokio::net::UnixStream::from_std(server_std) {
3178 Ok(s) => s,
3179 Err(e) => {
3180 log::error!("CfsctlService server stream conversion failed: {e:#?}");
3181 return;
3182 }
3183 };
3184 let server_zlink = match zlink::tokio::unix::Stream::try_from(server_stream) {
3185 Ok(s) => s,
3186 Err(e) => {
3187 log::error!("CfsctlService server zlink stream conversion failed: {e:#?}");
3188 return;
3189 }
3190 };
3191 let listener = zlink::ReadyListener::new(server_zlink);
3192 let server = zlink::Server::new(listener, service);
3193 if let Err(e) = server.run().await {
3194 log::warn!("CfsctlService in-process server error: {e:#?}");
3195 }
3196 });
3197 })?;
3198
3199 Ok((client_conn, handle))
3200}
3201
3202#[cfg(all(test, feature = "oci"))]
3203mod layer_sync_tests {
3204 use std::io::Read as _;
3211 use std::os::fd::AsFd as _;
3212 use std::sync::Arc;
3213
3214 use composefs::fsverity::{FsVerityHashValue as _, Sha256HashValue};
3215 use composefs::repository::{Repository, RepositoryConfig};
3216 use composefs_splitdirfdstream::reconstruct;
3217
3218 use super::layer_sync::GetLayerReply;
3219 use super::oci::OciError;
3220 use super::proxy::{OciProxy, RepositoryProxy as _};
3221 use super::{CfsctlService, spawn_in_process};
3222 use composefs_oci::varlink_types::GetLayerParams;
3223
3224 async fn collect_get_layer<C>(
3232 client: &mut C,
3233 handle: u64,
3234 diff_id: &str,
3235 ) -> Result<(GetLayerReply, Vec<std::os::fd::OwnedFd>), OciError>
3236 where
3237 C: OciProxy,
3238 {
3239 use zlink::futures_util::StreamExt as _;
3240
3241 let params = GetLayerParams {
3242 diff_id: Some(diff_id.to_owned()),
3243 storage: None,
3244 ..Default::default()
3245 };
3246 let mut stream = std::pin::pin!(
3247 client
3248 .get_layer(handle, params)
3249 .await
3250 .expect("get_layer transport error")
3251 );
3252
3253 let mut all_fds: Vec<std::os::fd::OwnedFd> = Vec::new();
3254 let mut last_reply: Option<GetLayerReply> = None;
3255
3256 while let Some(item) = stream.next().await {
3257 let (result, fds) = item.expect("get_layer stream error");
3258 match result {
3259 Ok(reply) => {
3260 last_reply = Some(reply);
3261 }
3262 Err(e) => return Err(e),
3263 }
3264 all_fds.extend(fds);
3265 }
3266
3267 Ok((last_reply.expect("get_layer stream was empty"), all_fds))
3268 }
3269
3270 async fn collect_get_layer_split<C>(
3276 client: &mut C,
3277 handle: u64,
3278 diff_id: &str,
3279 ) -> (
3280 GetLayerReply,
3281 Vec<std::os::fd::OwnedFd>,
3282 Vec<std::os::fd::OwnedFd>,
3283 )
3284 where
3285 C: OciProxy,
3286 {
3287 let (reply, mut all_fds) = collect_get_layer(client, handle, diff_id)
3288 .await
3289 .expect("get_layer failed");
3290 let dir_count = reply.dir_count as usize;
3291 let pipe_and_dirfds_len = 1 + dir_count;
3293 assert!(
3294 all_fds.len() >= pipe_and_dirfds_len,
3295 "expected at least {pipe_and_dirfds_len} fds, got {}",
3296 all_fds.len()
3297 );
3298 let lifetime_fds = all_fds.split_off(pipe_and_dirfds_len);
3299 (reply, all_fds, lifetime_fds)
3300 }
3301
3302 fn build_tar_layer(file_size: usize) -> Vec<u8> {
3305 let content: Vec<u8> = (0..file_size).map(|i| (i % 251) as u8).collect();
3306 let mut builder = ::tar::Builder::new(vec![]);
3307 let mut header = ::tar::Header::new_ustar();
3308 header.set_uid(0);
3309 header.set_gid(0);
3310 header.set_mode(0o644);
3311 header.set_entry_type(::tar::EntryType::Regular);
3312 header.set_size(file_size as u64);
3313 builder
3314 .append_data(&mut header, format!("file_{file_size}"), &content[..])
3315 .unwrap();
3316 builder.into_inner().unwrap()
3317 }
3318
3319 fn create_test_repo() -> (Arc<Repository<Sha256HashValue>>, tempfile::TempDir) {
3321 let tempdir = tempfile::TempDir::new().unwrap();
3322 let (repo, _) = Repository::init_path(
3323 rustix::fs::CWD,
3324 tempdir.path().join("repo"),
3325 RepositoryConfig::default().set_insecure(),
3326 )
3327 .unwrap();
3328 (Arc::new(repo), tempdir)
3329 }
3330
3331 #[tokio::test(flavor = "multi_thread")]
3332 async fn test_layer_sync_in_process() {
3333 let (repo, _tempdir) = create_test_repo();
3335
3336 let tar_bytes = build_tar_layer(128 * 1024); let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3339 let (verity, _stats) =
3340 composefs_oci::import_layer(&repo, &diff_id, None, tar_bytes.as_slice())
3341 .await
3342 .expect("import_layer");
3343
3344 let mut expected = Vec::<u8>::new();
3346 {
3347 let mut reader = repo
3348 .open_stream("", Some(&verity), Some(composefs_oci::LAYER_CONTENT_TYPE))
3349 .expect("open_stream for cat");
3350 reader.cat(&repo, &mut expected).expect("cat");
3351 }
3352
3353 let repo_path = _tempdir.path().join("repo").to_str().unwrap().to_string();
3354
3355 let service = CfsctlService::insecure_for_test();
3357 let (mut client, _server_handle) = spawn_in_process(service).unwrap();
3358
3359 let open_reply = client
3361 .open_repository(Some(&repo_path), None, None)
3362 .await
3363 .unwrap()
3364 .expect("open_repository");
3365 let handle = open_reply.handle;
3366
3367 assert_eq!(
3369 open_reply.hash_algorithm.as_deref(),
3370 Some("sha256"),
3371 "hash_algorithm must be sha256 for a Sha256HashValue repo"
3372 );
3373 assert!(
3374 open_reply.objects_device_id.is_some(),
3375 "objects_device_id must be reported"
3376 );
3377
3378 let info = client.get_info().await.unwrap().expect("get_info");
3380 assert!(
3381 info.features.contains(&"splitdirfdstream-v0".to_string()),
3382 "expected splitdirfdstream-v0 in features"
3383 );
3384
3385 let has = client
3387 .has_layer(handle, diff_id.as_ref())
3388 .await
3389 .unwrap()
3390 .expect("has_layer");
3391 assert!(has.present, "layer must be present");
3392 assert_eq!(
3393 has.layer_verity.as_deref(),
3394 Some(verity.to_hex().as_str()),
3395 "verity mismatch"
3396 );
3397
3398 let fake_digest = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
3400 let has_absent = client
3401 .has_layer(handle, fake_digest)
3402 .await
3403 .unwrap()
3404 .expect("has_layer absent");
3405 assert!(!has_absent.present, "absent layer must not be present");
3406 assert!(has_absent.layer_verity.is_none());
3407
3408 let (get_reply, pipe_and_dirfds, lifetime_fds) =
3412 collect_get_layer_split(&mut client, handle, diff_id.as_ref()).await;
3413 let dir_count = get_reply.dir_count as usize;
3414
3415 let _lifetime_fds = lifetime_fds;
3417
3418 let pipe_fd = pipe_and_dirfds[0].as_fd();
3420 let dir_fds: Vec<_> = pipe_and_dirfds[1..=dir_count]
3421 .iter()
3422 .map(|f| f.as_fd())
3423 .collect();
3424
3425 let pipe_owned = rustix::io::dup(pipe_fd).expect("dup pipe read");
3427 let mut pipe_file = std::fs::File::from(pipe_owned);
3428 let mut stream_bytes = Vec::new();
3429 pipe_file.read_to_end(&mut stream_bytes).unwrap();
3430 assert!(!stream_bytes.is_empty(), "stream must be non-empty");
3431
3432 let mut actual = Vec::new();
3434 reconstruct(stream_bytes.as_slice(), &dir_fds, &mut actual)
3435 .expect("reconstruct splitdirfdstream");
3436
3437 similar_asserts::assert_eq!(
3438 actual,
3439 expected,
3440 "reconstructed layer must equal cat() output"
3441 );
3442
3443 let err = collect_get_layer(&mut client, handle, fake_digest).await;
3445 match err {
3446 Err(super::oci::OciError::NoSuchLayer { .. }) => {}
3447 other => panic!("expected NoSuchLayer, got {other:?}"),
3448 }
3449 }
3450
3451 #[tokio::test(flavor = "multi_thread")]
3460 async fn test_put_layer_relay() {
3461 let (repo_a, _td_a) = create_test_repo();
3463 let tar_bytes = build_tar_layer(128 * 1024); let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3465 let (verity_a, _) =
3466 composefs_oci::import_layer(&repo_a, &diff_id, None, tar_bytes.as_slice())
3467 .await
3468 .expect("import_layer into repo_a");
3469
3470 let mut expected = Vec::<u8>::new();
3472 {
3473 let mut reader = repo_a
3474 .open_stream("", Some(&verity_a), Some(composefs_oci::LAYER_CONTENT_TYPE))
3475 .expect("open_stream for cat");
3476 reader.cat(&repo_a, &mut expected).expect("cat");
3477 }
3478 let repo_a_path = _td_a.path().join("repo").to_str().unwrap().to_string();
3479
3480 let (repo_b, _td_b) = create_test_repo();
3482 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3483
3484 let service_a = CfsctlService::insecure_for_test();
3486 let (mut client_a, _srv_a) = spawn_in_process(service_a).unwrap();
3487
3488 let service_b = CfsctlService::insecure_for_test();
3489 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3490
3491 let handle_a = client_a
3493 .open_repository(Some(&repo_a_path), None, None)
3494 .await
3495 .unwrap()
3496 .expect("open_repository A")
3497 .handle;
3498 let handle_b = client_b
3499 .open_repository(Some(&repo_b_path), None, None)
3500 .await
3501 .unwrap()
3502 .expect("open_repository B")
3503 .handle;
3504
3505 let (get_reply, pipe_and_dirfds, lifetime_fds) =
3508 collect_get_layer_split(&mut client_a, handle_a, diff_id.as_ref()).await;
3509 let dir_count = get_reply.dir_count as usize;
3510
3511 let put_fds = pipe_and_dirfds; let put_reply = client_b
3516 .put_layer(handle_b, diff_id.as_ref(), false, put_fds)
3517 .await
3518 .unwrap()
3519 .expect("put_layer");
3520 drop(lifetime_fds);
3522
3523 assert!(
3524 !put_reply.already_present,
3525 "first put_layer must report already_present = false"
3526 );
3527 assert!(
3528 dir_count > 0,
3529 "dir_count must be > 0 (dirfds region has at least one slot)"
3530 );
3531
3532 let total_stored =
3536 put_reply.objects_reflinked + put_reply.objects_hardlinked + put_reply.objects_copied;
3537 assert!(
3538 total_stored + put_reply.objects_already_present > 0,
3539 "put_layer must report at least one object stored, got {put_reply:?}"
3540 );
3541
3542 let content_id = composefs_oci::layer_content_id(&diff_id);
3544 assert!(
3545 repo_b
3546 .has_stream(&content_id)
3547 .expect("has_stream B")
3548 .is_some(),
3549 "repo B must have the layer after put_layer"
3550 );
3551
3552 let verity_b: Sha256HashValue =
3554 Sha256HashValue::from_hex(&put_reply.layer_verity).expect("parse layer_verity hex");
3555 let mut actual = Vec::<u8>::new();
3556 {
3557 let mut reader = repo_b
3558 .open_stream("", Some(&verity_b), Some(composefs_oci::LAYER_CONTENT_TYPE))
3559 .expect("open_stream B for cat");
3560 reader.cat(&repo_b, &mut actual).expect("cat B");
3561 }
3562 similar_asserts::assert_eq!(actual, expected, "repo B cat must equal repo A cat");
3563
3564 let (get_reply2, pipe_and_dirfds2, lifetime_fds2) =
3566 collect_get_layer_split(&mut client_a, handle_a, diff_id.as_ref()).await;
3567 let _ = get_reply2;
3568
3569 let put_reply2 = client_b
3570 .put_layer(handle_b, diff_id.as_ref(), false, pipe_and_dirfds2)
3571 .await
3572 .unwrap()
3573 .expect("put_layer 2nd");
3574 drop(lifetime_fds2);
3575
3576 assert!(
3577 put_reply2.already_present,
3578 "second put_layer must report already_present = true"
3579 );
3580 }
3581
3582 #[tokio::test(flavor = "multi_thread")]
3585 async fn test_put_layer_wrong_diff_id() {
3586 let (repo_a, _td_a) = create_test_repo();
3587 let tar_bytes = build_tar_layer(128 * 1024);
3588 let correct_diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3589 let (_verity_a, _) =
3590 composefs_oci::import_layer(&repo_a, &correct_diff_id, None, tar_bytes.as_slice())
3591 .await
3592 .expect("import_layer");
3593 let repo_a_path = _td_a.path().join("repo").to_str().unwrap().to_string();
3594
3595 let (_repo_b, _td_b) = create_test_repo();
3596 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3597
3598 let service_a = CfsctlService::insecure_for_test();
3599 let (mut client_a, _srv_a) = spawn_in_process(service_a).unwrap();
3600 let service_b = CfsctlService::insecure_for_test();
3601 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3602
3603 let handle_a = client_a
3604 .open_repository(Some(&repo_a_path), None, None)
3605 .await
3606 .unwrap()
3607 .expect("open_repository A")
3608 .handle;
3609 let handle_b = client_b
3610 .open_repository(Some(&repo_b_path), None, None)
3611 .await
3612 .unwrap()
3613 .expect("open_repository B")
3614 .handle;
3615
3616 let (_get_reply, pipe_and_dirfds, lifetime_fds) =
3618 collect_get_layer_split(&mut client_a, handle_a, correct_diff_id.as_ref()).await;
3619
3620 let wrong_diff_id =
3622 "sha256:0000000000000000000000000000000000000000000000000000000000000000";
3623 let put_err = client_b
3624 .put_layer(handle_b, wrong_diff_id, false, pipe_and_dirfds)
3625 .await
3626 .unwrap();
3627 drop(lifetime_fds);
3628
3629 match put_err {
3630 Err(super::oci::OciError::DiffIdMismatch { expected, actual }) => {
3631 assert_eq!(expected, wrong_diff_id);
3632 assert_eq!(actual, correct_diff_id.to_string());
3633 }
3634 other => panic!("expected DiffIdMismatch, got {other:?}"),
3635 }
3636
3637 let wrong_content_id = composefs_oci::layer_content_id(
3639 &wrong_diff_id.parse::<composefs_oci::OciDigest>().unwrap(),
3640 );
3641 assert!(
3642 _repo_b
3643 .has_stream(&wrong_content_id)
3644 .expect("has_stream B")
3645 .is_none(),
3646 "repo B must NOT have a stream for the wrong diff_id"
3647 );
3648 }
3649
3650 fn build_oci_tar_layer(payload_size: usize) -> Vec<u8> {
3659 let mut builder = ::tar::Builder::new(vec![]);
3660
3661 for (path, is_dir) in &[("./", true), ("./usr/", true), ("./usr/share/", true)] {
3662 let mut hdr = ::tar::Header::new_ustar();
3663 hdr.set_entry_type(::tar::EntryType::Directory);
3664 hdr.set_uid(0);
3665 hdr.set_gid(0);
3666 hdr.set_mode(0o755);
3667 hdr.set_size(0);
3668 let _ = is_dir; builder
3670 .append_data(&mut hdr, path, std::io::empty())
3671 .unwrap();
3672 }
3673
3674 let content: Vec<u8> = (0..payload_size).map(|i| (i % 251) as u8).collect();
3675 let mut file_hdr = ::tar::Header::new_ustar();
3676 file_hdr.set_entry_type(::tar::EntryType::Regular);
3677 file_hdr.set_uid(0);
3678 file_hdr.set_gid(0);
3679 file_hdr.set_mode(0o644);
3680 file_hdr.set_size(payload_size as u64);
3681 builder
3682 .append_data(
3683 &mut file_hdr,
3684 format!("./usr/share/data_{payload_size}"),
3685 content.as_slice(),
3686 )
3687 .unwrap();
3688
3689 builder.into_inner().unwrap()
3690 }
3691
3692 fn make_config_json(diff_ids: &[String]) -> String {
3697 let ids: Vec<String> = diff_ids.iter().map(|d| format!("\"{d}\"")).collect();
3698 format!(
3699 r#"{{"architecture":"amd64","os":"linux","rootfs":{{"type":"layers","diff_ids":[{}]}},"config":{{}}}}"#,
3700 ids.join(",")
3701 )
3702 }
3703
3704 fn make_manifest_json(
3706 config_json: &str,
3707 config_digest_str: &str,
3708 diff_ids: &[String],
3709 ) -> String {
3710 let layer_entries: Vec<String> = diff_ids
3711 .iter()
3712 .map(|d| {
3713 format!(
3714 r#"{{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"{d}","size":1}}"#
3715 )
3716 })
3717 .collect();
3718 format!(
3719 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":[{}]}}"#,
3720 config_json.len(),
3721 layer_entries.join(",")
3722 )
3723 }
3724
3725 #[tokio::test(flavor = "multi_thread")]
3733 async fn test_finalize_image_roundtrip() {
3734 use composefs_oci::OciDigest;
3735
3736 let (repo_b, _td_b) = create_test_repo();
3737 let repo_b_path = _td_b.path().join("repo").to_str().unwrap().to_string();
3738
3739 let tar_bytes = build_oci_tar_layer(128 * 1024);
3741 let diff_id = composefs_oci::sha256_content_digest(&tar_bytes);
3742 let (layer_verity, _) =
3743 composefs_oci::import_layer(&repo_b, &diff_id, None, tar_bytes.as_slice())
3744 .await
3745 .expect("import_layer into repo_b");
3746
3747 let diff_ids = vec![diff_id.to_string()];
3749 let config_json = make_config_json(&diff_ids);
3750 let config_digest = composefs_oci::sha256_content_digest(config_json.as_bytes());
3751 let manifest_json = make_manifest_json(&config_json, config_digest.as_ref(), &diff_ids);
3752
3753 let service_b = CfsctlService::insecure_for_test();
3755 let (mut client_b, _srv_b) = spawn_in_process(service_b).unwrap();
3756
3757 let handle_b = client_b
3758 .open_repository(Some(&repo_b_path), None, None)
3759 .await
3760 .unwrap()
3761 .expect("open_repository B")
3762 .handle;
3763
3764 let layers = vec![super::layer_sync::LayerRef {
3766 diff_id: diff_id.to_string(),
3767 layer_verity: layer_verity.to_hex(),
3768 }];
3769
3770 let reply = client_b
3772 .finalize_image(
3773 handle_b,
3774 &manifest_json,
3775 &config_json,
3776 layers,
3777 Some("finalize-test:v1"),
3778 )
3779 .await
3780 .unwrap()
3781 .expect("finalize_image");
3782
3783 assert!(
3785 !reply.manifest_digest.is_empty(),
3786 "manifest_digest must be non-empty"
3787 );
3788 assert!(
3789 !reply.manifest_verity.is_empty(),
3790 "manifest_verity must be non-empty"
3791 );
3792 assert!(
3793 !reply.config_digest.is_empty(),
3794 "config_digest must be non-empty"
3795 );
3796 assert!(
3797 !reply.config_verity.is_empty(),
3798 "config_verity must be non-empty"
3799 );
3800
3801 let manifest_digest: OciDigest = reply.manifest_digest.parse().unwrap();
3803 let config_digest2: OciDigest = reply.config_digest.parse().unwrap();
3804
3805 let manifest_id = composefs_oci::oci_image::manifest_identifier(&manifest_digest);
3806 assert!(
3807 repo_b
3808 .has_stream(&manifest_id)
3809 .expect("has_stream manifest")
3810 .is_some(),
3811 "manifest splitstream must exist in repo_b"
3812 );
3813
3814 let config_id2 = format!("oci-config-{config_digest2}");
3816 assert!(
3817 repo_b
3818 .has_stream(&config_id2)
3819 .expect("has_stream config")
3820 .is_some(),
3821 "config splitstream must exist in repo_b"
3822 );
3823
3824 let manifest_verity =
3826 Sha256HashValue::from_hex(&reply.manifest_verity).expect("parse manifest_verity");
3827 let erofs = composefs_oci::composefs_erofs_for_manifest(
3828 &repo_b,
3829 &manifest_digest,
3830 Some(&manifest_verity),
3831 repo_b.erofs_version(),
3832 )
3833 .expect("composefs_erofs_for_manifest");
3834 assert!(
3835 erofs.is_some(),
3836 "EROFS image must exist after finalize_image"
3837 );
3838 }
3839}