Skip to content

Commit f679965

Browse files
committed
composefs-oci: add support for splitfdstream
Assisted-by: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
1 parent 915a7e8 commit f679965

5 files changed

Lines changed: 355 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ composefs-ioctls = { version = "0.4.0", path = "crates/composefs-ioctls", defaul
3838
composefs-oci = { version = "0.4.0", path = "crates/composefs-oci", default-features = false }
3939
composefs-boot = { version = "0.4.0", path = "crates/composefs-boot", default-features = false }
4040
composefs-http = { version = "0.4.0", path = "crates/composefs-http", default-features = false }
41+
composefs-splitfdstream = { version = "0.4.0", path = "crates/composefs-splitfdstream", default-features = false }
42+
splitfdstream = { package = "composefs-splitfdstream", version = "0.4.0", path = "crates/composefs-splitfdstream", default-features = false }
4143
cap-std-ext = "5.1.2"
4244
ocidir = "0.7.2"
4345

crates/composefs-oci/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ bytes = { version = "1", default-features = false }
2626
composefs = { workspace = true }
2727
zlink-core = { workspace = true, optional = true }
2828
composefs-boot = { workspace = true, optional = true }
29+
splitfdstream = { workspace = true }
2930
containers-image-proxy = { version = "0.10", default-features = false }
3031
cstorage = { package = "composefs-storage", path = "../composefs-storage", version = "0.4.0", optional = true }
3132
hex = { version = "0.4.0", default-features = false }
3233
indicatif = { version = "0.17.0", default-features = false }
34+
jsonrpc-fdpass = { workspace = true }
3335
rustix = { version = "1.0.0", features = ["fs"] }
3436
serde = { version = "1.0", default-features = false, features = ["derive"] }
3537
thiserror = { version = "2.0.0", default-features = false }

crates/composefs-oci/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod oci_layout;
2626
/// Re-exported from [`composefs::progress`]; use that path directly in new code.
2727
pub mod progress;
2828
pub mod skopeo;
29+
pub mod splitfdstream;
2930
pub mod tar;
3031

3132
/// Test utilities for building OCI images from dumpfile strings.
@@ -37,6 +38,10 @@ pub mod test_util;
3738
// Re-export the composefs crate for consumers who only need composefs-oci
3839
pub use composefs;
3940

41+
pub use splitfdstream::{
42+
CompleteImageImportResult, import_complete_image_from_splitfdstream, import_from_splitfdstream,
43+
};
44+
4045
use std::io::Read;
4146
use std::{collections::HashMap, sync::Arc};
4247

crates/composefs-oci/src/oci_image.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ use serde::Serialize;
5050

5151
use composefs::{fsverity::FsVerityHashValue, repository::Repository};
5252

53-
use crate::ContentAndVerity;
5453
use crate::layer::is_tar_media_type;
5554
use crate::skopeo::{OCI_BLOB_CONTENT_TYPE, OCI_CONFIG_CONTENT_TYPE, OCI_MANIFEST_CONTENT_TYPE};
55+
use crate::{ContentAndVerity, sha256_content_digest};
5656

5757
/// Error marker: an OCI reference (tag) does not exist.
5858
#[derive(Debug, thiserror::Error)]
@@ -682,6 +682,54 @@ pub(crate) fn rewrite_manifest<ObjectID: FsVerityHashValue, S: AsRef<str>>(
682682
Ok((manifest_digest.clone(), id))
683683
}
684684

685+
/// Writes a manifest to the repository from raw JSON bytes.
686+
///
687+
/// Unlike [`write_manifest`], this preserves the exact JSON bytes from the
688+
/// original source, avoiding digest mismatches from re-serialization.
689+
pub fn write_manifest_raw<ObjectID: FsVerityHashValue>(
690+
repo: &Arc<Repository<ObjectID>>,
691+
manifest_json: &[u8],
692+
manifest_digest: &str,
693+
config_verity: &ObjectID,
694+
layer_verities: &HashMap<Box<str>, ObjectID>,
695+
reference: Option<&str>,
696+
) -> Result<(String, ObjectID)> {
697+
let digest: OciDigest = manifest_digest.parse().context("parsing manifest digest")?;
698+
let content_id = manifest_identifier(&digest);
699+
700+
if let Some(verity) = repo.has_stream(&content_id)? {
701+
if let Some(name) = reference {
702+
tag_image(repo, &digest, name)?;
703+
}
704+
return Ok((manifest_digest.to_string(), verity));
705+
}
706+
707+
let computed = sha256_content_digest(manifest_json);
708+
ensure!(
709+
digest == computed,
710+
"Manifest digest mismatch: expected {digest}, got {computed}"
711+
);
712+
713+
let manifest: ImageManifest =
714+
serde_json::from_slice(manifest_json).context("parsing manifest JSON")?;
715+
716+
let mut stream = repo.create_stream(OCI_MANIFEST_CONTENT_TYPE)?;
717+
718+
let config_key = format!("config:{}", manifest.config().digest());
719+
stream.add_named_stream_ref(&config_key, config_verity);
720+
721+
for (diff_id, verity) in layer_verities {
722+
stream.add_named_stream_ref(diff_id, verity);
723+
}
724+
725+
stream.write_external(manifest_json)?;
726+
727+
let oci_ref = reference.map(oci_ref_path);
728+
let id = repo.write_stream(stream, &content_id, oci_ref.as_deref())?;
729+
730+
Ok((manifest_digest.to_string(), id))
731+
}
732+
685733
/// Checks if a manifest exists.
686734
pub fn has_manifest<ObjectID: FsVerityHashValue>(
687735
repo: &Repository<ObjectID>,

0 commit comments

Comments
 (0)