public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Filip Schauer <f.schauer@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH proxmox 2/3] oci: reduce redundant layer decompression during extraction
Date: Tue,  3 Nov 2026 16:11:45 +0100	[thread overview]
Message-ID: <20261103151153.290754-3-f.schauer@proxmox.com> (raw)
In-Reply-To: <20261103151153.290754-1-f.schauer@proxmox.com>

According to the OCI Image Specification, the order of layers in the
image manifest matches the order of the rootfs.diff_ids in the image
configuration.

Instead of redundantly decompressing and hashing every layer upfront to
build a lookup map, pair layer blobs directly with their corresponding
diff_ids and verify the digests on the fly.

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
 proxmox-oci/src/lib.rs | 112 ++++++++---------------------------------
 1 file changed, 21 insertions(+), 91 deletions(-)

diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
index 71970cd8..7182b008 100644
--- a/proxmox-oci/src/lib.rs
+++ b/proxmox-oci/src/lib.rs
@@ -1,4 +1,3 @@
-use std::collections::HashMap;
 use std::ffi::OsStr;
 use std::fs::{File, read_dir, remove_dir_all, remove_file};
 use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
@@ -10,12 +9,11 @@ use flate2::read::GzDecoder;
 use oci_spec::OciSpecError;
 pub use oci_spec::image::{Arch, Config};
 use oci_spec::image::{ImageConfiguration, ImageManifest, MediaType};
-use sha2::digest::generic_array::GenericArray;
-use sha2::{Digest, Sha256};
 use tar::{Archive, EntryType};
 use thiserror::Error;
 
 mod digest;
+use digest::{DigestError, VerifyingReader};
 
 mod oci_tar_image;
 use oci_tar_image::{OciTarImage, OciTarImageBlob};
@@ -23,80 +21,6 @@ use oci_tar_image::{OciTarImage, OciTarImageBlob};
 const WHITEOUT_PREFIX: &[u8] = b".wh.";
 const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq";
 
-fn compute_digest<R: Read, H: Digest>(
-    mut reader: R,
-    mut hasher: H,
-) -> io::Result<GenericArray<u8, H::OutputSize>> {
-    let mut buf = proxmox_io::boxed::zeroed(32768);
-
-    loop {
-        let bytes_read = reader.read(&mut buf)?;
-        if bytes_read == 0 {
-            break Ok(hasher.finalize());
-        }
-
-        hasher.update(&buf[..bytes_read]);
-    }
-}
-
-fn compute_sha256<R: Read>(reader: R) -> io::Result<oci_spec::image::Sha256Digest> {
-    let digest = compute_digest(reader, Sha256::new())?;
-    Ok(oci_spec::image::Sha256Digest::from_str(&format!("{digest:x}")).expect("valid digest"))
-}
-
-/// Build a mapping from uncompressed layer digests (as found in the image config's `rootfs.diff_ids`)
-/// to their corresponding compressed-layer digests (i.e. the filenames under `blobs/<algorithm>/<digest>`)
-fn build_layer_map<R: Read + Seek>(
-    mut oci_tar_image: OciTarImage<R>,
-    image_manifest: &ImageManifest,
-) -> Result<
-    (
-        OciTarImage<R>,
-        HashMap<oci_spec::image::Digest, oci_spec::image::Descriptor>,
-    ),
-    ExtractError,
-> {
-    let mut layer_mapping = HashMap::new();
-
-    for layer in image_manifest.layers() {
-        let digest = match layer.media_type() {
-            MediaType::ImageLayer | MediaType::ImageLayerNonDistributable => layer.digest().clone(),
-            MediaType::ImageLayerGzip | MediaType::ImageLayerNonDistributableGzip => {
-                let mut compressed_blob = oci_tar_image
-                    .open_blob(layer.digest())
-                    .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
-                let decoder = GzDecoder::new(&mut compressed_blob);
-                let hash = compute_sha256(decoder)?.into();
-                oci_tar_image = compressed_blob.into_oci_tar_image();
-                hash
-            }
-            MediaType::ImageLayerZstd | MediaType::ImageLayerNonDistributableZstd => {
-                let mut compressed_blob = oci_tar_image
-                    .open_blob(layer.digest())
-                    .ok_or(ExtractError::MissingLayerFile(layer.digest().clone()))?;
-                let decoder = zstd::Decoder::new(&mut compressed_blob)?;
-                let hash = compute_sha256(decoder)?.into();
-                oci_tar_image = compressed_blob.into_oci_tar_image();
-                hash
-            }
-            // Skip any other non-ImageLayer related media types.
-            // Match explicitly to avoid missing new image layer types when oci-spec updates.
-            MediaType::Descriptor
-            | MediaType::LayoutHeader
-            | MediaType::ImageManifest
-            | MediaType::ImageIndex
-            | MediaType::ImageConfig
-            | MediaType::ArtifactManifest
-            | MediaType::EmptyJSON
-            | MediaType::Other(_) => continue,
-        };
-
-        layer_mapping.insert(digest, layer.clone());
-    }
-
-    Ok((oci_tar_image, layer_mapping))
-}
-
 #[derive(Debug, Error)]
 pub enum ProxmoxOciError {
     #[error("Error while parsing OCI image: {0}")]
@@ -177,8 +101,8 @@ fn parse_image<P: AsRef<Path>>(
 pub enum ExtractError {
     #[error("Incorrectly formatted digest: \"{0}\"")]
     InvalidDigest(String),
-    #[error("Unknown layer digest {0} found in rootfs.diff_ids")]
-    UnknownLayerDigest(oci_spec::image::Digest),
+    #[error(transparent)]
+    DigestError(#[from] DigestError),
     #[error("Layer file {0} mentioned in image manifest is missing")]
     MissingLayerFile(oci_spec::image::Digest),
     #[error("IO error: {0}")]
@@ -188,23 +112,23 @@ pub enum ExtractError {
 }
 
 fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
-    oci_tar_image: OciTarImage<R>,
+    mut oci_tar_image: OciTarImage<R>,
     image_manifest: &ImageManifest,
     image_config: &ImageConfiguration,
     target_path: P,
 ) -> Result<(), ExtractError> {
-    let (mut oci_tar_image, layer_map) = build_layer_map(oci_tar_image, image_manifest)?;
     let target_path = target_path.as_ref().canonicalize()?;
 
-    for layer in image_config.rootfs().diff_ids() {
-        let layer_digest = oci_spec::image::Digest::from_str(layer)
-            .map_err(|_| ExtractError::InvalidDigest(layer.to_string()))?;
-        let layer_descriptor = layer_map
-            .get(&layer_digest)
-            .ok_or(ExtractError::UnknownLayerDigest(layer_digest.clone()))?;
-        let mut layer_file = oci_tar_image
-            .open_blob(layer_descriptor.digest())
-            .ok_or(ExtractError::MissingLayerFile(layer_digest))?;
+    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() {
@@ -231,7 +155,13 @@ fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
             }
         };
 
-        remove_replaced_paths(&mut decode_fn(&mut layer_file)?, &target_path)?;
+        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)?;
 
-- 
2.47.3





  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 ` Filip Schauer [this message]
2026-11-03 15:11 ` [PATCH proxmox 3/3] oci: factor out parsing and extraction into modules Filip Schauer

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-3-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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal