From: Filip Schauer <f.schauer@proxmox.com>
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 [thread overview]
Message-ID: <20261103151153.290754-4-f.schauer@proxmox.com> (raw)
In-Reply-To: <20261103151153.290754-1-f.schauer@proxmox.com>
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
| 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
--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<R: Read + Seek, P: AsRef<Path>>(
+ mut oci_tar_image: OciTarImage<R>,
+ 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<T> = Box<dyn for<'a> Fn(&'a mut T) -> io::Result<Box<dyn Read + 'a>>>;
+ let decode_fn: DecodeFn<OciTarImageBlob<R>> = 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<Option<PathBuf>> {
+ 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<Option<PathBuf>> {
+ 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<R: Read>(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<R: Read>(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<P: AsRef<Path>>(
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<P: AsRef<Path>>(
- oci_tar_path: P,
- arch: Option<&Arch>,
-) -> Result<(OciTarImage<File>, 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<R: Read + Seek, P: AsRef<Path>>(
- mut oci_tar_image: OciTarImage<R>,
- 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<T> = Box<dyn for<'a> Fn(&'a mut T) -> io::Result<Box<dyn Read + 'a>>>;
- let decode_fn: DecodeFn<OciTarImageBlob<R>> = 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<Option<PathBuf>> {
- 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<Option<PathBuf>> {
- 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<R: Read>(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<R: Read>(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<P: AsRef<Path>>(
+ oci_tar_path: P,
+ arch: Option<&Arch>,
+) -> Result<(OciTarImage<File>, 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
prev parent reply other threads:[~2026-11-03 15:13 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-03 15:11 [PATCH proxmox 0/3] oci: refactor code Filip Schauer
2026-11-03 15:11 ` [PATCH proxmox 1/3] oci: add a digest helper module Filip Schauer
2026-11-03 15:11 ` [PATCH proxmox 2/3] oci: reduce redundant layer decompression during extraction Filip Schauer
2026-11-03 15:11 ` Filip Schauer [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261103151153.290754-4-f.schauer@proxmox.com \
--to=f.schauer@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox