use a3s_box_core::error::{BoxError, Result};
use std::path::PathBuf;
use std::path::{Component, Path};
use super::image::OciImage;
use super::layers::{extract_layer_with_metadata, finalize_rootfs_metadata};
pub struct OciRootfsBuilder {
rootfs_path: PathBuf,
image_path: PathBuf,
guest_init_path: Option<PathBuf>,
resolv_conf: Option<String>,
}
impl OciRootfsBuilder {
pub fn new(rootfs_path: impl Into<PathBuf>) -> Self {
Self {
rootfs_path: rootfs_path.into(),
image_path: PathBuf::new(),
guest_init_path: None,
resolv_conf: None,
}
}
pub fn with_resolv_conf(mut self, content: impl Into<String>) -> Self {
let content = content.into();
if !content.is_empty() {
self.resolv_conf = Some(content);
}
self
}
pub fn with_image(mut self, path: impl Into<PathBuf>) -> Self {
self.image_path = path.into();
self
}
pub fn with_guest_init(mut self, path: impl Into<PathBuf>) -> Self {
self.guest_init_path = Some(path.into());
self
}
pub fn build(&self) -> Result<()> {
tracing::info!(
rootfs = %self.rootfs_path.display(),
"Building OCI rootfs"
);
if self.image_path.as_os_str().is_empty() {
return Err(BoxError::OciImageError(
"OCI image path not set".to_string(),
));
}
self.create_base_structure()?;
self.extract_image()?;
if self.guest_init_path.is_some() {
self.install_guest_init()?;
}
self.create_essential_files()?;
finalize_rootfs_metadata(&self.rootfs_path)?;
tracing::info!("OCI rootfs built successfully");
Ok(())
}
pub fn install_guest_init_only(&self) -> Result<()> {
if self.guest_init_path.is_some() {
self.install_guest_init()?;
}
Ok(())
}
fn create_base_structure(&self) -> Result<()> {
let dirs = [
"dev",
"proc",
"sys",
"tmp",
"run",
"etc",
"var",
"var/tmp",
"var/log",
"workspace",
];
for dir in dirs {
let full_path = self.rootfs_path.join(dir);
std::fs::create_dir_all(&full_path).map_err(|e| {
BoxError::BuildError(format!(
"Failed to create directory {}: {}",
full_path.display(),
e
))
})?;
tracing::debug!(dir = %full_path.display(), "Created directory");
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&self.rootfs_path, std::fs::Permissions::from_mode(0o755))
.map_err(|error| {
BoxError::BuildError(format!(
"Failed to set rootfs permissions on {}: {error}",
self.rootfs_path.display()
))
})?;
}
Ok(())
}
fn extract_image(&self) -> Result<()> {
let image = OciImage::from_path(&self.image_path)?;
tracing::info!(
image = %self.image_path.display(),
rootfs = %self.rootfs_path.display(),
layers = image.layer_paths().len(),
"Extracting OCI image"
);
for layer_path in image.layer_paths() {
extract_layer_with_metadata(layer_path, &self.rootfs_path)?;
}
Ok(())
}
fn install_guest_init(&self) -> Result<()> {
let src = self
.guest_init_path
.as_ref()
.ok_or_else(|| BoxError::BuildError("Guest init path not set".to_string()))?;
if !src.exists() {
return Err(BoxError::BuildError(format!(
"Guest init binary not found: {}",
src.display()
)));
}
#[cfg(target_os = "windows")]
let install_dir = {
let sbin_link = self.rootfs_path.join("sbin");
match std::fs::symlink_metadata(&sbin_link) {
Ok(meta) if meta.is_dir() => sbin_link.clone(),
Ok(meta) if meta.file_type().is_symlink() => {
let target = std::fs::read_link(&sbin_link).map_err(|err| {
BoxError::BuildError(format!(
"Failed to resolve /sbin symlink {}: {}",
sbin_link.display(),
err
))
})?;
if target.is_absolute() {
target
} else {
self.rootfs_path.join(target)
}
}
Ok(_) => {
return Err(BoxError::BuildError(format!(
"Cannot install guest init because {} exists and is not a directory or symlink",
sbin_link.display()
)));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
let usr_sbin = self.rootfs_path.join("usr").join("sbin");
if usr_sbin.is_dir() {
usr_sbin
} else {
std::fs::create_dir_all(&sbin_link).map_err(|e| {
BoxError::BuildError(format!("Failed to create /sbin directory: {}", e))
})?;
sbin_link.clone()
}
}
Err(err) => {
return Err(BoxError::BuildError(format!(
"Failed to inspect /sbin path {}: {}",
sbin_link.display(),
err
)));
}
}
};
#[cfg(not(target_os = "windows"))]
let install_dir = {
let sbin_dir = self.rootfs_path.join("sbin");
std::fs::create_dir_all(&sbin_dir).map_err(|e| {
BoxError::BuildError(format!("Failed to create /sbin directory: {}", e))
})?;
sbin_dir
};
std::fs::create_dir_all(&install_dir).map_err(|e| {
BoxError::BuildError(format!(
"Failed to create guest init install directory {}: {}",
install_dir.display(),
e
))
})?;
let dest = install_dir.join("init");
if dest.exists() || dest.symlink_metadata().is_ok() {
std::fs::remove_file(&dest).map_err(|e| {
BoxError::BuildError(format!(
"Failed to remove existing {}: {}",
dest.display(),
e
))
})?;
}
std::fs::copy(src, &dest).map_err(|e| {
BoxError::BuildError(format!(
"Failed to copy guest init to {}: {}",
dest.display(),
e
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&dest)
.map_err(|e| BoxError::BuildError(format!("Failed to get permissions: {}", e)))?
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&dest, perms)
.map_err(|e| BoxError::BuildError(format!("Failed to set permissions: {}", e)))?;
}
tracing::info!(
src = %src.display(),
dst = %dest.display(),
"Installed guest init"
);
Ok(())
}
fn create_essential_files(&self) -> Result<()> {
self.ensure_passwd_entries(&[
("root", "root:x:0:0:root:/root:/bin/sh"),
("nobody", "nobody:x:65534:65534:nobody:/:/bin/false"),
])?;
self.ensure_group_entries(&[("root", "root:x:0:"), ("nogroup", "nogroup:x:65534:")])?;
self.write_file("etc/hosts", "127.0.0.1\tlocalhost\n::1\t\tlocalhost\n")?;
let resolv_conf = self
.resolv_conf
.as_deref()
.unwrap_or("nameserver 8.8.8.8\nnameserver 8.8.4.4\n");
self.write_file("etc/resolv.conf", resolv_conf)?;
self.write_file(
"etc/nsswitch.conf",
"passwd: files\ngroup: files\nhosts: files dns\n",
)?;
Ok(())
}
fn ensure_passwd_entries(&self, required: &[(&str, &str)]) -> Result<()> {
let passwd_path = self.rootfs_file_path("etc/passwd")?;
let existing = std::fs::read_to_string(&passwd_path).unwrap_or_default();
let mut content = existing.clone();
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
for (username, entry) in required {
let has_user = existing
.lines()
.any(|line| line.split(':').next() == Some(username));
if !has_user {
content.push_str(entry);
content.push('\n');
}
}
self.write_file("etc/passwd", &content)
}
fn ensure_group_entries(&self, required: &[(&str, &str)]) -> Result<()> {
let group_path = self.rootfs_file_path("etc/group")?;
let existing = std::fs::read_to_string(&group_path).unwrap_or_default();
let mut content = existing.clone();
if !content.is_empty() && !content.ends_with('\n') {
content.push('\n');
}
for (groupname, entry) in required {
let has_group = existing
.lines()
.any(|line| line.split(':').next() == Some(groupname));
if !has_group {
content.push_str(entry);
content.push('\n');
}
}
self.write_file("etc/group", &content)
}
fn write_file(&self, relative_path: &str, content: &str) -> Result<()> {
let full_path = self.rootfs_file_path(relative_path)?;
if let Some(parent) = full_path.parent() {
if parent.exists() && !parent.is_dir() {
return Err(BoxError::BuildError(format!(
"Cannot write {} because parent {} exists and is not a directory",
full_path.display(),
parent.display()
)));
}
std::fs::create_dir_all(parent).map_err(|e| {
BoxError::BuildError(format!("Failed to create parent directory: {}", e))
})?;
}
std::fs::write(&full_path, content).map_err(|e| {
BoxError::BuildError(format!("Failed to write {}: {}", full_path.display(), e))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&full_path, std::fs::Permissions::from_mode(0o644)).map_err(
|e| {
BoxError::BuildError(format!(
"Failed to set permissions on {}: {}",
full_path.display(),
e
))
},
)?;
}
tracing::debug!(path = %full_path.display(), "Created file");
Ok(())
}
fn rootfs_file_path(&self, relative_path: &str) -> Result<PathBuf> {
let relative = Path::new(relative_path);
if relative.is_absolute()
|| relative
.components()
.any(|component| matches!(component, Component::ParentDir | Component::Prefix(_)))
{
return Err(BoxError::BuildError(format!(
"Invalid rootfs relative path: {relative_path}"
)));
}
let file_name = relative.file_name().ok_or_else(|| {
BoxError::BuildError(format!("Invalid rootfs file path: {relative_path}"))
})?;
let parent = relative.parent().unwrap_or_else(|| Path::new(""));
Ok(self.resolve_rootfs_dir(parent)?.join(file_name))
}
fn resolve_rootfs_dir(&self, relative_dir: &Path) -> Result<PathBuf> {
let mut current = self.rootfs_path.clone();
for component in relative_dir.components() {
let Component::Normal(name) = component else {
if matches!(component, Component::CurDir) {
continue;
}
return Err(BoxError::BuildError(format!(
"Invalid rootfs directory path: {}",
relative_dir.display()
)));
};
let candidate = current.join(name);
match std::fs::symlink_metadata(&candidate) {
Ok(metadata) if metadata.file_type().is_symlink() => {
let target = std::fs::read_link(&candidate).map_err(|e| {
BoxError::BuildError(format!(
"Failed to resolve rootfs symlink {}: {}",
candidate.display(),
e
))
})?;
current = if target.is_absolute() {
let stripped = target.strip_prefix("/").map_err(|_| {
BoxError::BuildError(format!(
"Invalid absolute rootfs symlink target {}",
target.display()
))
})?;
self.rootfs_path.join(stripped)
} else {
current.join(target)
};
}
Ok(metadata) if !metadata.is_dir() => {
return Err(BoxError::BuildError(format!(
"Cannot use {} as a rootfs directory because it is not a directory",
candidate.display()
)));
}
Ok(_) | Err(_) => {
current = candidate;
}
}
}
Ok(current)
}
pub fn image_config(&self) -> Result<super::image::OciImageConfig> {
let image = OciImage::from_path(&self.image_path)?;
Ok(image.config().clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;
#[test]
fn test_oci_rootfs_builder_creates_base_structure() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image(&image);
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
assert!(rootfs_path.join("dev").exists());
assert!(rootfs_path.join("proc").exists());
assert!(rootfs_path.join("sys").exists());
assert!(rootfs_path.join("tmp").exists());
assert!(rootfs_path.join("etc").exists());
assert!(rootfs_path.join("workspace").exists());
}
#[cfg(unix)]
#[test]
fn test_oci_rootfs_builder_makes_root_searchable_by_image_users() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
std::fs::create_dir_all(&rootfs_path).unwrap();
std::fs::set_permissions(&rootfs_path, std::fs::Permissions::from_mode(0o700)).unwrap();
create_test_oci_image(&image);
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
let mode = std::fs::metadata(&rootfs_path)
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o755);
}
#[test]
fn test_oci_rootfs_builder_creates_essential_files() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image(&image);
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
assert!(rootfs_path.join("etc/passwd").exists());
assert!(rootfs_path.join("etc/group").exists());
assert!(rootfs_path.join("etc/hosts").exists());
assert!(rootfs_path.join("etc/resolv.conf").exists());
let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
assert!(passwd.contains("root:x:0:0"));
}
#[cfg(unix)]
#[test]
fn test_oci_rootfs_builder_makes_essential_files_world_readable() {
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let builder = OciRootfsBuilder::new(&rootfs_path);
let essential_files = ["passwd", "group", "hosts", "resolv.conf", "nsswitch.conf"];
fs::create_dir_all(rootfs_path.join("etc")).unwrap();
for name in essential_files {
let path = rootfs_path.join("etc").join(name);
fs::write(&path, "image content\n").unwrap();
fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap();
}
builder.create_essential_files().unwrap();
for name in essential_files {
let mode = fs::metadata(rootfs_path.join("etc").join(name))
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o644, "unexpected mode for /etc/{name}");
}
}
#[test]
fn test_oci_rootfs_builder_extracts_image_at_root() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image_with_file(&image, "app/main.py", b"print('hello')");
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
let extracted = rootfs_path.join("app/main.py");
assert!(extracted.exists());
let content = fs::read_to_string(extracted).unwrap();
assert_eq!(content, "print('hello')");
}
#[test]
fn test_oci_rootfs_builder_no_image_set() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let result = OciRootfsBuilder::new(&rootfs_path).build();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("image path not set"));
}
#[test]
fn test_oci_rootfs_builder_uses_custom_resolv_conf_and_default_when_empty() {
let temp_dir = TempDir::new().unwrap();
let custom_rootfs = temp_dir.path().join("custom-rootfs");
let default_rootfs = temp_dir.path().join("default-rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image(&image);
OciRootfsBuilder::new(&custom_rootfs)
.with_image(&image)
.with_resolv_conf("nameserver 10.0.0.10\nsearch svc.cluster.local\n")
.build()
.unwrap();
assert_eq!(
fs::read_to_string(custom_rootfs.join("etc/resolv.conf")).unwrap(),
"nameserver 10.0.0.10\nsearch svc.cluster.local\n"
);
OciRootfsBuilder::new(&default_rootfs)
.with_image(&image)
.with_resolv_conf("")
.build()
.unwrap();
let resolv_conf = fs::read_to_string(default_rootfs.join("etc/resolv.conf")).unwrap();
assert!(resolv_conf.contains("nameserver 8.8.8.8"));
assert!(resolv_conf.contains("nameserver 8.8.4.4"));
}
#[test]
fn test_oci_rootfs_builder_writes_essential_files_inside_absolute_etc_symlink() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image_with_etc_symlink(&image);
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
assert!(rootfs_path
.join("etc")
.symlink_metadata()
.unwrap()
.file_type()
.is_symlink());
assert!(rootfs_path.join("usr/etc/passwd").exists());
assert!(rootfs_path.join("usr/etc/group").exists());
assert!(rootfs_path.join("usr/etc/hosts").exists());
assert!(rootfs_path.join("usr/etc/resolv.conf").exists());
assert!(rootfs_path.join("usr/etc/nsswitch.conf").exists());
}
#[test]
fn test_oci_rootfs_builder_preserves_existing_passwd_and_group_entries() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let image = temp_dir.path().join("image");
create_test_oci_image_with_files(
&image,
&[
(
"etc/passwd",
b"root:x:0:0:Root User:/root:/bin/bash\napp:x:1000:1000:App:/app:/bin/sh"
.as_slice(),
),
("etc/group", b"root:x:0:\napp:x:1000:".as_slice()),
],
);
OciRootfsBuilder::new(&rootfs_path)
.with_image(&image)
.build()
.unwrap();
let passwd = fs::read_to_string(rootfs_path.join("etc/passwd")).unwrap();
assert_eq!(entry_count(&passwd, "root"), 1);
assert!(passwd.contains("root:x:0:0:Root User:/root:/bin/bash\n"));
assert!(passwd.contains("app:x:1000:1000:App:/app:/bin/sh\n"));
assert!(passwd.contains("nobody:x:65534:65534:nobody:/:/bin/false\n"));
let group = fs::read_to_string(rootfs_path.join("etc/group")).unwrap();
assert_eq!(entry_count(&group, "root"), 1);
assert!(group.contains("root:x:0:\n"));
assert!(group.contains("app:x:1000:\n"));
assert!(group.contains("nogroup:x:65534:\n"));
}
#[test]
fn test_install_guest_init_only_noop_without_guest_init() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
OciRootfsBuilder::new(&rootfs_path)
.install_guest_init_only()
.unwrap();
assert!(!rootfs_path.exists());
}
#[test]
fn test_install_guest_init_only_replaces_existing_init_and_sets_permissions() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let guest_init = temp_dir.path().join("guest-init");
fs::create_dir_all(rootfs_path.join("sbin")).unwrap();
fs::write(rootfs_path.join("sbin/init"), b"old init").unwrap();
fs::write(&guest_init, b"new guest init").unwrap();
OciRootfsBuilder::new(&rootfs_path)
.with_guest_init(&guest_init)
.install_guest_init_only()
.unwrap();
let installed = rootfs_path.join("sbin/init");
assert_eq!(fs::read(&installed).unwrap(), b"new guest init");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
fs::metadata(&installed).unwrap().permissions().mode() & 0o777,
0o755
);
}
}
#[test]
fn test_install_guest_init_only_errors_when_source_missing() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let missing_guest_init = temp_dir.path().join("missing-guest-init");
let err = OciRootfsBuilder::new(&rootfs_path)
.with_guest_init(&missing_guest_init)
.install_guest_init_only()
.unwrap_err();
assert!(err.to_string().contains("Guest init binary not found"));
}
#[test]
fn test_oci_rootfs_builder_image_config_reads_oci_config() {
let temp_dir = TempDir::new().unwrap();
let image = temp_dir.path().join("image");
create_test_oci_image(&image);
let config = OciRootfsBuilder::new(temp_dir.path().join("rootfs"))
.with_image(&image)
.image_config()
.unwrap();
assert_eq!(
config.entrypoint,
Some(vec!["/usr/local/bin/app".to_string()])
);
assert_eq!(config.cmd, None);
assert_eq!(
config.env,
vec![(
"PATH".to_string(),
"/usr/local/bin:/usr/bin:/bin".to_string()
)]
);
assert_eq!(config.working_dir, Some("/app".to_string()));
}
#[cfg(target_os = "windows")]
#[test]
fn test_install_guest_init_prefers_usr_sbin_when_sbin_missing() {
let temp_dir = TempDir::new().unwrap();
let rootfs_path = temp_dir.path().join("rootfs");
let guest_init = temp_dir.path().join("guest-init");
fs::create_dir_all(rootfs_path.join("usr").join("sbin")).unwrap();
fs::write(&guest_init, b"guest-init").unwrap();
let builder = OciRootfsBuilder {
rootfs_path: rootfs_path.clone(),
image_path: PathBuf::new(),
guest_init_path: Some(guest_init),
};
builder.install_guest_init().unwrap();
assert!(rootfs_path.join("usr").join("sbin").join("init").exists());
assert!(!rootfs_path.join("sbin").exists());
}
fn create_test_oci_image(path: &Path) {
create_test_oci_image_with_file(path, "test.txt", b"test content");
}
fn create_test_oci_image_with_file(path: &Path, filename: &str, content: &[u8]) {
create_test_oci_image_with_files(path, &[(filename, content)]);
}
fn create_test_oci_image_with_etc_symlink(path: &Path) {
create_test_oci_image_with_entries(path, &[], Some(("/usr/etc", "etc")));
}
fn entry_count(content: &str, name: &str) -> usize {
content
.lines()
.filter(|line| line.split(':').next() == Some(name))
.count()
}
fn create_test_oci_image_with_files(path: &Path, files: &[(&str, &[u8])]) {
create_test_oci_image_with_entries(path, files, None);
}
fn create_test_oci_image_with_entries(
path: &Path,
files: &[(&str, &[u8])],
symlink: Option<(&str, &str)>,
) {
use flate2::write::GzEncoder;
use flate2::Compression;
use tar::Builder;
fs::create_dir_all(path.join("blobs/sha256")).unwrap();
fs::write(path.join("oci-layout"), r#"{"imageLayoutVersion":"1.0.0"}"#).unwrap();
let layer_hash = "layer123";
let layer_path = path.join("blobs/sha256").join(layer_hash);
{
let file = fs::File::create(&layer_path).unwrap();
let encoder = GzEncoder::new(file, Compression::default());
let mut builder = Builder::new(encoder);
for (filename, content) in files {
let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_uid(0);
header.set_gid(0);
header.set_cksum();
builder
.append_data(&mut header, *filename, *content)
.unwrap();
}
if let Some((target, link_name)) = symlink {
let mut header = tar::Header::new_gnu();
header.set_entry_type(tar::EntryType::Symlink);
header.set_size(0);
header.set_mode(0o777);
header.set_uid(0);
header.set_gid(0);
header.set_link_name(target).unwrap();
header.set_cksum();
builder
.append_data(&mut header, link_name, std::io::empty())
.unwrap();
}
builder.finish().unwrap();
}
let config_content = r#"{
"architecture": "amd64",
"os": "linux",
"config": {
"Entrypoint": ["/usr/local/bin/app"],
"Cmd": null,
"Env": ["PATH=/usr/local/bin:/usr/bin:/bin"],
"WorkingDir": "/app"
},
"rootfs": {
"type": "layers",
"diff_ids": ["sha256:layer123"]
},
"history": []
}"#;
let config_hash = "config456";
fs::write(path.join("blobs/sha256").join(config_hash), config_content).unwrap();
let manifest_content = format!(
r#"{{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {{
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:{}",
"size": {}
}},
"layers": [
{{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:{}",
"size": 100
}}
]
}}"#,
config_hash,
config_content.len(),
layer_hash
);
let manifest_hash = "manifest789";
fs::write(
path.join("blobs/sha256").join(manifest_hash),
&manifest_content,
)
.unwrap();
let index_content = format!(
r#"{{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:{}",
"size": {}
}}
]
}}"#,
manifest_hash,
manifest_content.len()
);
fs::write(path.join("index.json"), index_content).unwrap();
}
}