Skip to content

Commit f3799e5

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 628a4d8 commit f3799e5

5 files changed

Lines changed: 354 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ composefs-ioctls = { version = "0.3.0", path = "crates/composefs-ioctls", defaul
2222
composefs-oci = { version = "0.3.0", path = "crates/composefs-oci", default-features = false }
2323
composefs-boot = { version = "0.3.0", path = "crates/composefs-boot", default-features = false }
2424
composefs-http = { version = "0.3.0", path = "crates/composefs-http", default-features = false }
25+
splitfdstream = { version = "0.3.0", path = "crates/splitfdstream", default-features = false }
2526

2627
[profile.dev.package.sha2]
2728
# this is *really* slow otherwise

crates/composefs-oci/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ async-compression = { version = "0.4.0", default-features = false, features = ["
2121
bytes = { version = "1", default-features = false }
2222
composefs = { workspace = true }
2323
composefs-boot = { workspace = true, optional = true }
24+
splitfdstream = { workspace = true }
2425
containers-image-proxy = { version = "0.9.2", default-features = false }
2526
hex = { version = "0.4.0", default-features = false }
2627
indicatif = { version = "0.18.0", default-features = false, features = ["tokio"] }
28+
jsonrpc-fdpass = { git = "https://github.com/bootc-dev/jsonrpc-fdpass.git" }
2729
rustix = { version = "1.0.0", features = ["fs"] }
2830
serde = { version = "1.0", default-features = false, features = ["derive"] }
2931
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
@@ -14,6 +14,7 @@ pub mod boot;
1414
pub mod image;
1515
pub mod oci_image;
1616
pub mod skopeo;
17+
pub mod splitfdstream;
1718
pub mod tar;
1819

1920
/// Test utilities for building OCI images from dumpfile strings.
@@ -25,6 +26,10 @@ pub mod test_util;
2526
// Re-export the composefs crate for consumers who only need composefs-oci
2627
pub use composefs;
2728

29+
pub use splitfdstream::{
30+
CompleteImageImportResult, import_complete_image_from_splitfdstream, import_from_splitfdstream,
31+
};
32+
2833
use std::{collections::HashMap, sync::Arc};
2934

3035
use anyhow::{Result, ensure};

crates/composefs-oci/src/oci_image.rs

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

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

53-
use crate::ContentAndVerity;
5453
use crate::skopeo::{OCI_BLOB_CONTENT_TYPE, OCI_CONFIG_CONTENT_TYPE, OCI_MANIFEST_CONTENT_TYPE};
54+
use crate::{ContentAndVerity, sha256_content_digest};
5555

5656
/// Data and named refs from a splitstream with external object storage.
5757
type ExternalData<ObjectID> = (Vec<u8>, HashMap<Box<str>, ObjectID>);
@@ -684,6 +684,54 @@ pub(crate) fn rewrite_manifest<ObjectID: FsVerityHashValue>(
684684
Ok((manifest_digest.clone(), id))
685685
}
686686

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

0 commit comments

Comments
 (0)