From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id C13571FF0AA for ; Tue, 03 Nov 2026 16:13:29 +0100 (CET) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 418B0218D3; Tue, 03 Nov 2026 16:13:26 +0100 (CET) From: Filip Schauer To: pve-devel@lists.proxmox.com Subject: [PATCH proxmox 3/3] oci: factor out parsing and extraction into modules Date: Tue, 3 Nov 2026 16:11:46 +0100 Message-ID: <20261103151153.290754-4-f.schauer@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20261103151153.290754-1-f.schauer@proxmox.com> References: <20261103151153.290754-1-f.schauer@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1793718796589 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.311 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: HGWAEO5QGBZ2AFGLI3HGARLQVQZZ5FZO X-Message-ID-Hash: HGWAEO5QGBZ2AFGLI3HGARLQVQZZ5FZO X-MailFrom: f.schauer@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Signed-off-by: Filip Schauer --- proxmox-oci/src/extract.rs | 270 +++++++++++++++++++++++++++++++ proxmox-oci/src/lib.rs | 318 +------------------------------------ proxmox-oci/src/parse.rs | 51 ++++++ 3 files changed, 328 insertions(+), 311 deletions(-) create mode 100644 proxmox-oci/src/extract.rs create mode 100644 proxmox-oci/src/parse.rs diff --git a/proxmox-oci/src/extract.rs b/proxmox-oci/src/extract.rs new file mode 100644 index 00000000..d4aa9e4b --- /dev/null +++ b/proxmox-oci/src/extract.rs @@ -0,0 +1,270 @@ +use std::ffi::OsStr; +use std::fs::{read_dir, remove_dir_all, remove_file}; +use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; +use std::os::unix::ffi::OsStrExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use flate2::read::GzDecoder; +use oci_spec::image::{ImageConfiguration, ImageManifest, MediaType}; +use tar::{Archive, EntryType}; +use thiserror::Error; + +use crate::digest::{DigestError, VerifyingReader}; +use crate::oci_tar_image::{OciTarImage, OciTarImageBlob}; + +const WHITEOUT_PREFIX: &[u8] = b".wh."; +const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq"; + +#[derive(Debug, Error)] +pub enum ExtractError { + #[error("Incorrectly formatted digest: \"{0}\"")] + InvalidDigest(String), + #[error(transparent)] + DigestError(#[from] DigestError), + #[error("Layer file {0} mentioned in image manifest is missing")] + MissingLayerFile(oci_spec::image::Digest), + #[error(transparent)] + Io(#[from] io::Error), + #[error("Layer has wrong media type: {0}")] + WrongMediaType(String), +} + +pub(crate) fn extract_image_rootfs>( + mut oci_tar_image: OciTarImage, + image_manifest: &ImageManifest, + image_config: &ImageConfiguration, + target_path: P, +) -> Result<(), ExtractError> { + let target_path = target_path.as_ref().canonicalize()?; + + for (layer_descriptor, diffid_str) in image_manifest + .layers() + .iter() + .zip(image_config.rootfs().diff_ids()) + { + let layer_diffid = oci_spec::image::Digest::from_str(diffid_str) + .map_err(|_| ExtractError::InvalidDigest(diffid_str.to_string()))?; + let mut layer_file = oci_tar_image.open_blob(layer_descriptor.digest()).ok_or( + ExtractError::MissingLayerFile(layer_descriptor.digest().clone()), + )?; + + type DecodeFn = Box Fn(&'a mut T) -> io::Result>>; + let decode_fn: DecodeFn> = match layer_descriptor.media_type() { + MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => { + Box::new(|file| Ok(Box::new(file))) + } + MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => { + Box::new(|file| Ok(Box::new(GzDecoder::new(file)))) + } + MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => { + Box::new(|file| Ok(Box::new(zstd::Decoder::new(file)?))) + } + // Error on any other non-ImageLayer related media types. + // Match explicitly to avoid missing new image layer types when oci-spec updates. + media_type @ (MediaType::Descriptor + | MediaType::LayoutHeader + | MediaType::ImageManifest + | MediaType::ImageIndex + | MediaType::ImageConfig + | MediaType::ArtifactManifest + | MediaType::EmptyJSON + | MediaType::Other(_)) => { + return Err(ExtractError::WrongMediaType(media_type.to_string())); + } + }; + + let mut verified = VerifyingReader::new(decode_fn(&mut layer_file)?, layer_diffid)?; + remove_replaced_paths(&mut verified, &target_path)?; + + // Force an EOF so VerifyingReader actually verifies. + io::copy(&mut verified, &mut io::sink())?; + drop(verified); + + layer_file.seek(SeekFrom::Start(0))?; + extract_archive(&mut decode_fn(&mut layer_file)?, &target_path)?; + + oci_tar_image = layer_file.into_oci_tar_image(); + } + + Ok(()) +} + +/// Validate a path previously received from a `tar::Entry`. +/// +/// Tar files may contain arbitrary paths, so reject absolute paths and paths containing parent +/// directory references, as joining them onto the target directory would lead outside of it. +pub(crate) fn validate_path(path: &Path) -> io::Result<()> { + use std::path::Component; + + if !path + .components() + .all(|c| matches!(c, Component::Normal(_) | Component::CurDir)) + { + return Err(io::Error::other(format!( + "illegal path {path:?} in layer - absolute paths and '..' components are not allowed" + ))); + } + + Ok(()) +} + +/// Join the `path` of a layer entry onto the resolved `target_path`, resolving its parent only. +/// +/// Fails if the parent leads outside of the target through a symlink, like the tar crate checks +/// before unpacking, assuming that nothing else modifies the target meanwhile. Returns `None` if +/// the parent does not exist or `path` has no last component. +fn resolve_in_target(target_path: &Path, path: &Path) -> io::Result> { + let (Some(parent), Some(name)) = (path.parent(), path.file_name()) else { + return Ok(None); + }; + Ok(resolve_dir_in_target(target_path, parent)?.map(|dir| dir.join(name))) +} + +/// Resolve the directory `dir` of a layer entry within the resolved `target_path`, see +/// [`resolve_in_target`]. +fn resolve_dir_in_target(target_path: &Path, dir: &Path) -> io::Result> { + let resolved = match target_path.join(dir).canonicalize() { + Ok(resolved) => resolved, + Err(err) if matches!(err.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => { + return Ok(None); + } + Err(err) => return Err(err), + }; + + if !resolved.starts_with(target_path) { + return Err(io::Error::other(format!( + "illegal path {dir:?} in layer - a symlink leads outside of the target directory" + ))); + } + + Ok(Some(resolved)) +} + +/// Remove what a layer replaces with directories or whites out from previous layers. +fn remove_replaced_paths(reader: &mut R, target_path: &Path) -> io::Result<()> { + let mut archive = Archive::new(reader); + + let mut directories = Vec::new(); + let mut whiteouts = Vec::new(); + for entry in archive.entries()? { + let entry = entry?; + let path = entry.path()?; + validate_path(&path)?; + + match entry.header().entry_type() { + EntryType::Directory => directories.push(path.into_owned()), + EntryType::Regular if is_whiteout(&path) => whiteouts.push(path.into_owned()), + _ => {} + } + } + + // Parents first and before whiteouts, so nothing resolves through a symlink being replaced. + directories.sort(); + for dir in directories { + if let Some(dir_path_abs) = resolve_in_target(target_path, &dir)? + && dir_path_abs.symlink_metadata().is_ok_and(|m| !m.is_dir()) + { + remove_file(dir_path_abs)?; + } + } + + for whiteout in whiteouts { + apply_whiteout(target_path, &whiteout)?; + } + + Ok(()) +} + +fn is_whiteout(path: &Path) -> bool { + path.file_name() + .is_some_and(|name| name.as_bytes().starts_with(WHITEOUT_PREFIX)) +} + +/// Apply the whiteout file at `path` on previous layers. +fn apply_whiteout(target_path: &Path, path: &Path) -> io::Result<()> { + let Some(filename) = path.file_name() else { + return Ok(()); + }; + + if filename == OPAQUE_WHITEOUT_NAME { + if let Some(parent) = path.parent() + && let Some(whiteout_abs_path) = resolve_dir_in_target(target_path, parent)? + { + for direntry in read_dir(whiteout_abs_path)? { + remove_path(&direntry?.path())?; + } + } + } else if let Some(filename_stripped) = filename.as_bytes().strip_prefix(WHITEOUT_PREFIX) { + let whiteout_path = match filename_stripped { + b"." => match path.parent() { + Some(p) if p.parent().is_some() => p, + _ => return Ok(()), // Prevent whiteout of root directory + }, + b".." => return Ok(()), // Prevent whiteout of grandparent directory + fname => &path.with_file_name(OsStr::from_bytes(fname)), + }; + if let Some(whiteout_abs_path) = resolve_in_target(target_path, whiteout_path)? { + remove_path(&whiteout_abs_path)?; + } + } + + Ok(()) +} + +fn extract_archive(reader: &mut R, target_path: &Path) -> io::Result<()> { + let mut archive = Archive::new(reader); + archive.set_preserve_ownerships(true); + archive.set_preserve_permissions(true); + archive.set_unpack_xattrs(true); + + // Delay directory entries until the end (they will be created if needed by descendants), + // to ensure that directory permissions do not interfere with descendant extraction. + let mut directories = Vec::new(); + for entry in archive.entries()? { + let mut file = entry?; + + let filepath = file.path()?; + validate_path(&filepath)?; + + if file.header().entry_type() == EntryType::Directory { + directories.push(file); + continue; + } else if file.header().entry_type() == EntryType::Regular && is_whiteout(&filepath) { + continue; + } + + // If a file or directory already exists at this path, remove it first. + if let Some(file_path_abs) = resolve_in_target(target_path, &filepath)? { + remove_path(&file_path_abs)?; + } + + file.unpack_in(target_path)?; + } + + // Apply the directories in reverse topological order, + // to avoid failure on restrictive parent directory permissions. + directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes())); + for mut dir in directories { + // Skip a directory replaced by a later entry, also to not apply it through a symlink. + if let Some(dir_path_abs) = resolve_in_target(target_path, &dir.path()?)? + && dir_path_abs.symlink_metadata().is_ok_and(|m| !m.is_dir()) + { + continue; + } + + dir.unpack_in(target_path)?; + } + + Ok(()) +} + +/// Remove whatever exists at `path`, without following a symlink there. +fn remove_path(path: &Path) -> io::Result<()> { + match path.symlink_metadata() { + Ok(metadata) if metadata.is_dir() => remove_dir_all(path), + Ok(_) => remove_file(path), + Err(err) if matches!(err.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => Ok(()), + Err(err) => Err(err), + } +} diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs index 7182b008..d5814915 100644 --- a/proxmox-oci/src/lib.rs +++ b/proxmox-oci/src/lib.rs @@ -1,25 +1,17 @@ -use std::ffi::OsStr; -use std::fs::{File, read_dir, remove_dir_all, remove_file}; -use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; -use std::os::unix::ffi::OsStrExt; -use std::path::{Path, PathBuf}; -use std::str::FromStr; +use std::path::Path; -use flate2::read::GzDecoder; -use oci_spec::OciSpecError; pub use oci_spec::image::{Arch, Config}; -use oci_spec::image::{ImageConfiguration, ImageManifest, MediaType}; -use tar::{Archive, EntryType}; use thiserror::Error; mod digest; -use digest::{DigestError, VerifyingReader}; -mod oci_tar_image; -use oci_tar_image::{OciTarImage, OciTarImageBlob}; +mod extract; +use extract::{ExtractError, extract_image_rootfs}; + +mod parse; +use parse::{ParseError, parse_image}; -const WHITEOUT_PREFIX: &[u8] = b".wh."; -const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq"; +mod oci_tar_image; #[derive(Debug, Error)] pub enum ProxmoxOciError { @@ -53,299 +45,3 @@ pub fn parse_and_extract_image>( Ok(image_config.config().clone()) } - -#[derive(Debug, Error)] -pub enum ParseError { - #[error("OCI spec error: {0}")] - OciSpec(#[from] OciSpecError), - #[error("Wrong media type")] - WrongMediaType, - #[error("IO error: {0}")] - Io(#[from] io::Error), - #[error("Unsupported CPU architecture")] - UnsupportedArchitecture, - #[error("Missing image config")] - MissingImageConfig, -} - -fn parse_image>( - oci_tar_path: P, - arch: Option<&Arch>, -) -> Result<(OciTarImage, ImageManifest, ImageConfiguration), ParseError> { - let oci_tar_file = File::open(oci_tar_path)?; - let mut oci_tar_image = OciTarImage::new(oci_tar_file)?; - - let image_manifest = oci_tar_image - .image_manifest(arch) - .ok_or(ParseError::UnsupportedArchitecture)??; - - let image_config_descriptor = image_manifest.config(); - - if image_config_descriptor.media_type() != &MediaType::ImageConfig { - return Err(ParseError::WrongMediaType); - } - - let mut image_config_file = oci_tar_image - .open_blob(image_config_descriptor.digest()) - .ok_or(ParseError::MissingImageConfig)?; - let image_config = ImageConfiguration::from_reader(&mut image_config_file)?; - - Ok(( - image_config_file.into_oci_tar_image(), - image_manifest, - image_config, - )) -} - -#[derive(Debug, Error)] -pub enum ExtractError { - #[error("Incorrectly formatted digest: \"{0}\"")] - InvalidDigest(String), - #[error(transparent)] - DigestError(#[from] DigestError), - #[error("Layer file {0} mentioned in image manifest is missing")] - MissingLayerFile(oci_spec::image::Digest), - #[error("IO error: {0}")] - Io(#[from] io::Error), - #[error("Layer has wrong media type: {0}")] - WrongMediaType(String), -} - -fn extract_image_rootfs>( - mut oci_tar_image: OciTarImage, - image_manifest: &ImageManifest, - image_config: &ImageConfiguration, - target_path: P, -) -> Result<(), ExtractError> { - let target_path = target_path.as_ref().canonicalize()?; - - for (layer_descriptor, diffid_str) in image_manifest - .layers() - .iter() - .zip(image_config.rootfs().diff_ids()) - { - let layer_diffid = oci_spec::image::Digest::from_str(diffid_str) - .map_err(|_| ExtractError::InvalidDigest(diffid_str.to_string()))?; - let mut layer_file = oci_tar_image.open_blob(layer_descriptor.digest()).ok_or( - ExtractError::MissingLayerFile(layer_descriptor.digest().clone()), - )?; - - type DecodeFn = Box Fn(&'a mut T) -> io::Result>>; - let decode_fn: DecodeFn> = match layer_descriptor.media_type() { - MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => { - Box::new(|file| Ok(Box::new(file))) - } - MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => { - Box::new(|file| Ok(Box::new(GzDecoder::new(file)))) - } - MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => { - Box::new(|file| Ok(Box::new(zstd::Decoder::new(file)?))) - } - // Error on any other non-ImageLayer related media types. - // Match explicitly to avoid missing new image layer types when oci-spec updates. - media_type @ (MediaType::Descriptor - | MediaType::LayoutHeader - | MediaType::ImageManifest - | MediaType::ImageIndex - | MediaType::ImageConfig - | MediaType::ArtifactManifest - | MediaType::EmptyJSON - | MediaType::Other(_)) => { - return Err(ExtractError::WrongMediaType(media_type.to_string())); - } - }; - - let mut verified = VerifyingReader::new(decode_fn(&mut layer_file)?, layer_diffid)?; - remove_replaced_paths(&mut verified, &target_path)?; - - // Force an EOF so VerifyingReader actually verifies. - io::copy(&mut verified, &mut io::sink())?; - drop(verified); - - layer_file.seek(SeekFrom::Start(0))?; - extract_archive(&mut decode_fn(&mut layer_file)?, &target_path)?; - - oci_tar_image = layer_file.into_oci_tar_image(); - } - - Ok(()) -} - -/// Validate a path previously received from a `tar::Entry`. -/// -/// Tar files may contain arbitrary paths, so reject absolute paths and paths containing parent -/// directory references, as joining them onto the target directory would lead outside of it. -pub(crate) fn validate_path(path: &Path) -> io::Result<()> { - use std::path::Component; - - if !path - .components() - .all(|c| matches!(c, Component::Normal(_) | Component::CurDir)) - { - return Err(io::Error::other(format!( - "illegal path {path:?} in layer - absolute paths and '..' components are not allowed" - ))); - } - - Ok(()) -} - -/// Join the `path` of a layer entry onto the resolved `target_path`, resolving its parent only. -/// -/// Fails if the parent leads outside of the target through a symlink, like the tar crate checks -/// before unpacking, assuming that nothing else modifies the target meanwhile. Returns `None` if -/// the parent does not exist or `path` has no last component. -fn resolve_in_target(target_path: &Path, path: &Path) -> io::Result> { - let (Some(parent), Some(name)) = (path.parent(), path.file_name()) else { - return Ok(None); - }; - Ok(resolve_dir_in_target(target_path, parent)?.map(|dir| dir.join(name))) -} - -/// Resolve the directory `dir` of a layer entry within the resolved `target_path`, see -/// [`resolve_in_target`]. -fn resolve_dir_in_target(target_path: &Path, dir: &Path) -> io::Result> { - let resolved = match target_path.join(dir).canonicalize() { - Ok(resolved) => resolved, - Err(err) if matches!(err.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => { - return Ok(None); - } - Err(err) => return Err(err), - }; - - if !resolved.starts_with(target_path) { - return Err(io::Error::other(format!( - "illegal path {dir:?} in layer - a symlink leads outside of the target directory" - ))); - } - - Ok(Some(resolved)) -} - -/// Remove what a layer replaces with directories or whites out from previous layers. -fn remove_replaced_paths(reader: &mut R, target_path: &Path) -> io::Result<()> { - let mut archive = Archive::new(reader); - - let mut directories = Vec::new(); - let mut whiteouts = Vec::new(); - for entry in archive.entries()? { - let entry = entry?; - let path = entry.path()?; - validate_path(&path)?; - - match entry.header().entry_type() { - EntryType::Directory => directories.push(path.into_owned()), - EntryType::Regular if is_whiteout(&path) => whiteouts.push(path.into_owned()), - _ => {} - } - } - - // Parents first and before whiteouts, so nothing resolves through a symlink being replaced. - directories.sort(); - for dir in directories { - if let Some(dir_path_abs) = resolve_in_target(target_path, &dir)? - && dir_path_abs.symlink_metadata().is_ok_and(|m| !m.is_dir()) - { - remove_file(dir_path_abs)?; - } - } - - for whiteout in whiteouts { - apply_whiteout(target_path, &whiteout)?; - } - - Ok(()) -} - -fn is_whiteout(path: &Path) -> bool { - path.file_name() - .is_some_and(|name| name.as_bytes().starts_with(WHITEOUT_PREFIX)) -} - -/// Apply the whiteout file at `path` on previous layers. -fn apply_whiteout(target_path: &Path, path: &Path) -> io::Result<()> { - let Some(filename) = path.file_name() else { - return Ok(()); - }; - - if filename == OPAQUE_WHITEOUT_NAME { - if let Some(parent) = path.parent() - && let Some(whiteout_abs_path) = resolve_dir_in_target(target_path, parent)? - { - for direntry in read_dir(whiteout_abs_path)? { - remove_path(&direntry?.path())?; - } - } - } else if let Some(filename_stripped) = filename.as_bytes().strip_prefix(WHITEOUT_PREFIX) { - let whiteout_path = match filename_stripped { - b"." => match path.parent() { - Some(p) if p.parent().is_some() => p, - _ => return Ok(()), // Prevent whiteout of root directory - }, - b".." => return Ok(()), // Prevent whiteout of grandparent directory - fname => &path.with_file_name(OsStr::from_bytes(fname)), - }; - if let Some(whiteout_abs_path) = resolve_in_target(target_path, whiteout_path)? { - remove_path(&whiteout_abs_path)?; - } - } - - Ok(()) -} - -fn extract_archive(reader: &mut R, target_path: &Path) -> io::Result<()> { - let mut archive = Archive::new(reader); - archive.set_preserve_ownerships(true); - archive.set_preserve_permissions(true); - archive.set_unpack_xattrs(true); - - // Delay directory entries until the end (they will be created if needed by descendants), - // to ensure that directory permissions do not interfere with descendant extraction. - let mut directories = Vec::new(); - for entry in archive.entries()? { - let mut file = entry?; - - let filepath = file.path()?; - validate_path(&filepath)?; - - if file.header().entry_type() == EntryType::Directory { - directories.push(file); - continue; - } else if file.header().entry_type() == EntryType::Regular && is_whiteout(&filepath) { - continue; - } - - // If a file or directory already exists at this path, remove it first. - if let Some(file_path_abs) = resolve_in_target(target_path, &filepath)? { - remove_path(&file_path_abs)?; - } - - file.unpack_in(target_path)?; - } - - // Apply the directories in reverse topological order, - // to avoid failure on restrictive parent directory permissions. - directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes())); - for mut dir in directories { - // Skip a directory replaced by a later entry, also to not apply it through a symlink. - if let Some(dir_path_abs) = resolve_in_target(target_path, &dir.path()?)? - && dir_path_abs.symlink_metadata().is_ok_and(|m| !m.is_dir()) - { - continue; - } - - dir.unpack_in(target_path)?; - } - - Ok(()) -} - -/// Remove whatever exists at `path`, without following a symlink there. -fn remove_path(path: &Path) -> io::Result<()> { - match path.symlink_metadata() { - Ok(metadata) if metadata.is_dir() => remove_dir_all(path), - Ok(_) => remove_file(path), - Err(err) if matches!(err.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => Ok(()), - Err(err) => Err(err), - } -} diff --git a/proxmox-oci/src/parse.rs b/proxmox-oci/src/parse.rs new file mode 100644 index 00000000..681bd2d8 --- /dev/null +++ b/proxmox-oci/src/parse.rs @@ -0,0 +1,51 @@ +use std::fs::File; +use std::path::Path; + +use oci_spec::OciSpecError; +use oci_spec::image::{Arch, ImageConfiguration, ImageManifest, MediaType}; +use thiserror::Error; + +use crate::oci_tar_image::OciTarImage; + +#[derive(Debug, Error)] +pub enum ParseError { + #[error(transparent)] + OciSpec(#[from] OciSpecError), + #[error("Wrong media type")] + WrongMediaType, + #[error(transparent)] + Io(#[from] std::io::Error), + #[error("Unsupported CPU architecture")] + UnsupportedArchitecture, + #[error("Missing image config")] + MissingImageConfig, +} + +pub(crate) fn parse_image>( + oci_tar_path: P, + arch: Option<&Arch>, +) -> Result<(OciTarImage, ImageManifest, ImageConfiguration), ParseError> { + let oci_tar_file = File::open(oci_tar_path)?; + let mut oci_tar_image = OciTarImage::new(oci_tar_file)?; + + let image_manifest = oci_tar_image + .image_manifest(arch) + .ok_or(ParseError::UnsupportedArchitecture)??; + + let image_config_descriptor = image_manifest.config(); + + if image_config_descriptor.media_type() != &MediaType::ImageConfig { + return Err(ParseError::WrongMediaType); + } + + let mut image_config_file = oci_tar_image + .open_blob(image_config_descriptor.digest()) + .ok_or(ParseError::MissingImageConfig)?; + let image_config = ImageConfiguration::from_reader(&mut image_config_file)?; + + Ok(( + image_config_file.into_oci_tar_image(), + image_manifest, + image_config, + )) +} -- 2.47.3