pub mod builder;
pub mod layer;
pub mod layout;
pub mod manifest;
pub mod packages;
pub mod registry;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::str::FromStr;
pub use builder::{BuildOptions, BuildOutput, Builder};
pub use layer::LayerOwner;
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OciCopy {
pub host: PathBuf,
pub image: String,
}
impl OciCopy {
pub fn validate(&self) -> Result<(), String> {
if self.host.as_os_str().is_empty() {
return Err("copy host path must not be empty".to_string());
}
validate_image_path(&self.image)
}
}
impl FromStr for OciCopy {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let (host, image) = value
.rsplit_once(':')
.ok_or_else(|| "copy must be HOST_PATH:IMAGE_PATH".to_string())?;
if host.is_empty() {
return Err("copy host path must not be empty".to_string());
}
let copy = Self {
host: PathBuf::from(host),
image: image.to_string(),
};
copy.validate()?;
Ok(copy)
}
}
fn validate_image_path(path: &str) -> Result<(), String> {
if !path.starts_with('/') {
return Err(format!("copy image path must be absolute (got {path:?})"));
}
if path.trim_matches('/').is_empty() {
return Err(format!(
"copy image path must not be the root `/` (got {path:?})"
));
}
if path.split('/').any(|part| part == "." || part == "..") {
return Err(format!(
"copy image path must not contain `.` or `..` components (got {path:?})"
));
}
Ok(())
}
pub fn normalize_arch(a: &str) -> &str {
match a {
"x86_64" => "amd64",
"aarch64" => "arm64",
other => other,
}
}
pub fn normalize_os(o: &str) -> &str {
match o {
"macos" | "windows" => "linux",
other => other,
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct OciConfig {
#[serde(default)]
pub from: Option<String>,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub workdir: Option<String>,
#[serde(default)]
pub entrypoint: Option<Vec<String>>,
#[serde(default)]
pub cmd: Option<Vec<String>>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub user_id: Option<u32>,
#[serde(default)]
pub group_id: Option<u32>,
#[serde(default)]
pub mount_point: Option<String>,
#[serde(default)]
pub copy: Vec<OciCopy>,
#[serde(default)]
pub env: IndexMap<String, String>,
#[serde(default)]
pub labels: IndexMap<String, String>,
}
impl OciConfig {
pub fn fill_defaults_from(&mut self, other: Self) {
if self.from.is_none() {
self.from = other.from;
}
if self.tag.is_none() {
self.tag = other.tag;
}
if self.workdir.is_none() {
self.workdir = other.workdir;
}
if self.entrypoint.is_none() {
self.entrypoint = other.entrypoint;
}
if self.cmd.is_none() {
self.cmd = other.cmd;
}
if self.user.is_none() {
self.user = other.user;
}
let had_user_id = self.user_id.is_some();
if self.user_id.is_none() {
self.user_id = other.user_id;
}
if self.group_id.is_none() && !had_user_id {
self.group_id = other.group_id;
}
if self.mount_point.is_none() {
self.mount_point = other.mount_point;
}
let mut copy = other.copy;
copy.append(&mut self.copy);
self.copy = copy;
for (k, v) in other.env {
self.env.entry(k).or_insert(v);
}
for (k, v) in other.labels {
self.labels.entry(k).or_insert(v);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn copy(host: &str, image: &str) -> OciCopy {
OciCopy {
host: PathBuf::from(host),
image: image.to_string(),
}
}
#[test]
fn copy_rejects_root_and_relative_image_paths() {
assert!("file:/".parse::<OciCopy>().is_err());
assert!("file:relative".parse::<OciCopy>().is_err());
}
#[test]
fn layered_copies_put_more_specific_entries_last() {
let mut merged = OciConfig {
copy: vec![copy("project", "/same")],
..Default::default()
};
merged.fill_defaults_from(OciConfig {
copy: vec![copy("parent", "/same")],
..Default::default()
});
assert_eq!(
merged.copy,
vec![copy("parent", "/same"), copy("project", "/same")]
);
}
}