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: [pve-devel] [PATCH proxmox v4 02/15] add proxmox-oci crate
Date: Mon,  8 Sep 2025 17:02:05 +0200	[thread overview]
Message-ID: <20250908150224.155373-3-f.schauer@proxmox.com> (raw)
In-Reply-To: <20250908150224.155373-1-f.schauer@proxmox.com>

This crate can parse an OCI image tarball and extract its rootfs. Layers
are applied in sequence, but an overlay filesystem is currently not
used.

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
---
Changed since v3:
* correctly handle whiteout edge cases
* parse_and_extract_image: add argument for CPU architecture

Changed since v2:
* remove reachable unwraps & refactor code
* increase hasher buffer size from 4096 to 32768 (matching internal
  sha2::Digest buffering)
* preserve permissions and xattrs during rootfs extraction
* handle whiteouts & opaque whiteouts

 Cargo.toml                       |   1 +
 proxmox-oci/Cargo.toml           |  22 +++
 proxmox-oci/debian/changelog     |   5 +
 proxmox-oci/debian/control       |  45 +++++
 proxmox-oci/debian/debcargo.toml |   7 +
 proxmox-oci/src/lib.rs           | 324 +++++++++++++++++++++++++++++++
 proxmox-oci/src/oci_tar_image.rs | 144 ++++++++++++++
 7 files changed, 548 insertions(+)
 create mode 100644 proxmox-oci/Cargo.toml
 create mode 100644 proxmox-oci/debian/changelog
 create mode 100644 proxmox-oci/debian/control
 create mode 100644 proxmox-oci/debian/debcargo.toml
 create mode 100644 proxmox-oci/src/lib.rs
 create mode 100644 proxmox-oci/src/oci_tar_image.rs

diff --git a/Cargo.toml b/Cargo.toml
index ce249371..ff12d7b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ members = [
     "proxmox-network-api",
     "proxmox-network-types",
     "proxmox-notify",
+    "proxmox-oci",
     "proxmox-openid",
     "proxmox-product-config",
     "proxmox-resource-scheduling",
diff --git a/proxmox-oci/Cargo.toml b/proxmox-oci/Cargo.toml
new file mode 100644
index 00000000..4daff6ab
--- /dev/null
+++ b/proxmox-oci/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "proxmox-oci"
+description = "OCI image parsing and extraction"
+version = "0.1.0"
+
+authors.workspace = true
+edition.workspace = true
+exclude.workspace = true
+homepage.workspace = true
+license.workspace = true
+repository.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+flate2.workspace = true
+oci-spec = "0.8.1"
+sha2 = "0.10"
+tar.workspace = true
+thiserror = "1"
+zstd.workspace = true
+
+proxmox-io.workspace = true
diff --git a/proxmox-oci/debian/changelog b/proxmox-oci/debian/changelog
new file mode 100644
index 00000000..754d06c1
--- /dev/null
+++ b/proxmox-oci/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-oci (0.1.0-1) bookworm; urgency=medium
+
+  * Initial release.
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 28 Apr 2025 12:34:56 +0200
diff --git a/proxmox-oci/debian/control b/proxmox-oci/debian/control
new file mode 100644
index 00000000..f33331c5
--- /dev/null
+++ b/proxmox-oci/debian/control
@@ -0,0 +1,45 @@
+Source: rust-proxmox-oci
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.82) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-flate2-1+default-dev <!nocheck>,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~) <!nocheck>,
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~) <!nocheck>,
+ librust-sha2-0.10+default-dev <!nocheck>,
+ librust-tar-0.4+default-dev <!nocheck>,
+ librust-thiserror-1+default-dev <!nocheck>,
+ librust-zstd-0.13+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.0
+Vcs-Git: git://git.proxmox.com/git/proxmox.git
+Vcs-Browser: https://git.proxmox.com/?p=proxmox.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: proxmox-oci
+Rules-Requires-Root: no
+
+Package: librust-proxmox-oci-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-flate2-1+default-dev,
+ librust-oci-spec-0.8+default-dev (>= 0.8.1-~~),
+ librust-proxmox-io-1+default-dev (>= 1.2.0-~~),
+ librust-sha2-0.10+default-dev,
+ librust-tar-0.4+default-dev,
+ librust-thiserror-1+default-dev,
+ librust-zstd-0.13+default-dev
+Provides:
+ librust-proxmox-oci+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-oci-0.1.0+default-dev (= ${binary:Version})
+Description: OCI image parsing and extraction - Rust source code
+ Source code for Debianized Rust crate "proxmox-oci"
diff --git a/proxmox-oci/debian/debcargo.toml b/proxmox-oci/debian/debcargo.toml
new file mode 100644
index 00000000..b7864cdb
--- /dev/null
+++ b/proxmox-oci/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-oci/src/lib.rs b/proxmox-oci/src/lib.rs
new file mode 100644
index 00000000..cf0e4271
--- /dev/null
+++ b/proxmox-oci/src/lib.rs
@@ -0,0 +1,324 @@
+use std::collections::HashMap;
+use std::fs::{read_dir, remove_dir_all, remove_file, File};
+use std::io::{Read, Seek, SeekFrom};
+use std::path::{Path, PathBuf};
+use std::str::FromStr;
+
+use flate2::read::GzDecoder;
+pub use oci_spec::image::{Arch, Config};
+use oci_spec::image::{ImageConfiguration, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use sha2::digest::generic_array::GenericArray;
+use sha2::{Digest, Sha256};
+use tar::{Archive, EntryType};
+use thiserror::Error;
+
+mod oci_tar_image;
+use oci_tar_image::{OciTarImage, OciTarImageBlob};
+
+const WHITEOUT_PREFIX: &str = ".wh.";
+const OPAQUE_WHITEOUT_NAME: &str = ".wh..wh..opq";
+
+fn compute_digest<R: Read, H: Digest>(
+    mut reader: R,
+    mut hasher: H,
+) -> std::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) -> std::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}")]
+    ParseError(#[from] ParseError),
+    #[error("Error while extracting OCI image: {0}")]
+    ExtractError(#[from] ExtractError),
+}
+
+/// Extract the rootfs of an OCI image tar and return the image config.
+///
+/// # Arguments
+///
+/// * `oci_tar_path` - Path to the OCI image tar archive
+/// * `rootfs_path` - Destination path where the rootfs will be extracted to
+/// * `arch` - Optional CPU architecture used to pick the first matching manifest from a multi-arch
+///   image index. If `None`, the first manifest will be used.
+pub fn parse_and_extract_image<P: AsRef<Path>>(
+    oci_tar_path: P,
+    rootfs_path: P,
+    arch: Option<&Arch>,
+) -> Result<Option<Config>, ProxmoxOciError> {
+    let (oci_tar_image, image_manifest, image_config) = parse_image(oci_tar_path, arch)?;
+
+    extract_image_rootfs(oci_tar_image, &image_manifest, &image_config, rootfs_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] std::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("Unknown layer digest {0} found in rootfs.diff_ids")]
+    UnknownLayerDigest(oci_spec::image::Digest),
+    #[error("Layer file {0} mentioned in image manifest is missing")]
+    MissingLayerFile(oci_spec::image::Digest),
+    #[error("IO error: {0}")]
+    Io(#[from] std::io::Error),
+    #[error("Layer has wrong media type: {0}")]
+    WrongMediaType(String),
+}
+
+fn extract_image_rootfs<R: Read + Seek, P: AsRef<Path>>(
+    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)?;
+
+    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))?;
+
+        type DecodeFn<T> = Box<dyn for<'a> Fn(&'a mut T) -> std::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()))
+            }
+        };
+
+        apply_whiteouts(&mut decode_fn(&mut layer_file)?, &target_path)?;
+        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(())
+}
+
+/// Apply whiteouts on previous layers
+fn apply_whiteouts<R: Read, P: AsRef<Path>>(reader: &mut R, target_path: P) -> std::io::Result<()> {
+    let mut archive = Archive::new(reader);
+
+    for entry in archive.entries()? {
+        let file = entry?;
+        if file.header().entry_type() != EntryType::Regular {
+            continue;
+        }
+
+        let filepath = file.path()?;
+        if let Some(filename) = filepath.file_name() {
+            if filename == OPAQUE_WHITEOUT_NAME {
+                if let Some(parent) = filepath.parent() {
+                    let whiteout_abs_path = target_path.as_ref().join(parent);
+                    if whiteout_abs_path.exists() {
+                        for direntry in read_dir(whiteout_abs_path)? {
+                            remove_path(direntry?.path())?;
+                        }
+                    }
+                }
+            } else if let Some(filename) = filename.to_str() {
+                // TODO: Simplify this once OsStr::strip_prefix is implemented
+                if let Some(filename_stripped) = filename.strip_prefix(WHITEOUT_PREFIX) {
+                    let whiteout_path = match filename_stripped {
+                        "." => match filepath.parent() {
+                            Some(p) if p.parent().is_some() => p,
+                            _ => continue, // Prevent whiteout of root directory
+                        },
+                        ".." => continue, // Prevent whiteout of grandparent directory
+                        fname => &filepath.with_file_name(fname),
+                    };
+                    let whiteout_abs_path = target_path.as_ref().join(whiteout_path);
+                    if whiteout_abs_path.exists() {
+                        remove_path(whiteout_abs_path)?;
+                    }
+                }
+            }
+        }
+    }
+
+    Ok(())
+}
+
+fn extract_archive<R: Read, P: AsRef<Path>>(reader: &mut R, target_path: P) -> std::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?;
+        if file.header().entry_type() == EntryType::Directory {
+            directories.push(file);
+            continue;
+        } else if file.header().entry_type() == EntryType::Regular {
+            // Skip whiteout files
+            if let Some(filename) = file.path()?.file_name() {
+                if filename == OPAQUE_WHITEOUT_NAME {
+                    continue;
+                } else if let Some(filename) = filename.to_str() {
+                    if filename.starts_with(WHITEOUT_PREFIX) {
+                        continue;
+                    }
+                }
+            }
+        }
+
+        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 {
+        dir.unpack_in(&target_path)?;
+    }
+
+    Ok(())
+}
+
+fn remove_path(path: PathBuf) -> std::io::Result<()> {
+    if path.metadata()?.is_dir() {
+        remove_dir_all(path)
+    } else {
+        remove_file(path)
+    }
+}
diff --git a/proxmox-oci/src/oci_tar_image.rs b/proxmox-oci/src/oci_tar_image.rs
new file mode 100644
index 00000000..23e1bfe0
--- /dev/null
+++ b/proxmox-oci/src/oci_tar_image.rs
@@ -0,0 +1,144 @@
+use std::collections::HashMap;
+use std::io::{Read, Seek, SeekFrom};
+use std::ops::Range;
+use std::path::{Path, PathBuf};
+
+use oci_spec::image::{Arch, Digest, ImageIndex, ImageManifest, MediaType};
+use oci_spec::OciSpecError;
+use tar::Archive;
+
+use proxmox_io::RangeReader;
+
+#[derive(Clone)]
+struct TarEntry {
+    range: Range<u64>,
+}
+
+impl TarEntry {
+    fn new(range: Range<u64>) -> Self {
+        Self { range }
+    }
+}
+
+pub struct OciTarImage<R: Read + Seek> {
+    reader: R,
+    entries: HashMap<PathBuf, TarEntry>,
+    image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImage<R> {
+    pub fn new(reader: R) -> oci_spec::Result<Self> {
+        let mut archive = Archive::new(reader);
+        let entries = archive.entries_with_seek()?;
+        let mut entries_index = HashMap::new();
+        let mut image_index = None;
+
+        for entry in entries {
+            let mut entry = entry?;
+            let offset = entry.raw_file_position();
+            let size = entry.size();
+            let path = entry.path()?.into_owned();
+
+            if path.as_path() == Path::new("index.json") {
+                image_index = Some(ImageIndex::from_reader(&mut entry)?);
+            }
+
+            let tar_entry = TarEntry::new(offset..(offset + size));
+            entries_index.insert(path, tar_entry);
+        }
+
+        if let Some(image_index) = image_index {
+            Ok(Self {
+                reader: archive.into_inner(),
+                entries: entries_index,
+                image_index,
+            })
+        } else {
+            Err(OciSpecError::Other("Missing index.json file".into()))
+        }
+    }
+
+    pub fn image_index(&self) -> &ImageIndex {
+        &self.image_index
+    }
+
+    fn get_blob_entry(&self, digest: &Digest) -> Option<TarEntry> {
+        let path = get_blob_path(digest);
+        self.entries.get(&path).cloned()
+    }
+
+    pub fn open_blob(self, digest: &Digest) -> Option<OciTarImageBlob<R>> {
+        if let Some(entry) = self.get_blob_entry(digest) {
+            Some(OciTarImageBlob::new(self, entry.range))
+        } else {
+            None
+        }
+    }
+
+    pub fn image_manifest(
+        &mut self,
+        architecture: Option<&Arch>,
+    ) -> Option<oci_spec::Result<ImageManifest>> {
+        let digest = match self.image_index.manifests().iter().find(|d| {
+            d.media_type() == &MediaType::ImageManifest
+                && architecture
+                    .is_none_or(|a| d.platform().as_ref().is_none_or(|p| p.architecture() == a))
+        }) {
+            Some(descriptor) => descriptor.digest(),
+            None => return None,
+        };
+
+        if let Some(entry) = self.get_blob_entry(digest) {
+            let mut range_reader = RangeReader::new(&mut self.reader, entry.range);
+            Some(ImageManifest::from_reader(&mut range_reader))
+        } else {
+            Some(Err(OciSpecError::Other(format!(
+                "Image manifest with digest {digest} mentioned in image index is missing"
+            ))))
+        }
+    }
+}
+
+fn get_blob_path(digest: &Digest) -> PathBuf {
+    let algorithm = digest.algorithm();
+    let digest = digest.digest();
+    format!("blobs/{algorithm}/{digest}").into()
+}
+
+pub struct OciTarImageBlob<R: Read + Seek> {
+    range_reader: RangeReader<R>,
+    entries: HashMap<PathBuf, TarEntry>,
+    image_index: ImageIndex,
+}
+
+impl<R: Read + Seek> OciTarImageBlob<R> {
+    fn new(archive: OciTarImage<R>, range: Range<u64>) -> Self {
+        let range_reader = RangeReader::new(archive.reader, range);
+
+        Self {
+            range_reader,
+            entries: archive.entries,
+            image_index: archive.image_index,
+        }
+    }
+
+    pub fn into_oci_tar_image(self) -> OciTarImage<R> {
+        OciTarImage {
+            reader: self.range_reader.into_inner(),
+            entries: self.entries,
+            image_index: self.image_index,
+        }
+    }
+}
+
+impl<R: Read + Seek> Read for OciTarImageBlob<R> {
+    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
+        self.range_reader.read(buf)
+    }
+}
+
+impl<R: Read + Seek> Seek for OciTarImageBlob<R> {
+    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
+        self.range_reader.seek(pos)
+    }
+}
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


  parent reply	other threads:[~2025-09-08 15:04 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-09-08 15:02 [pve-devel] [PATCH container/docs/lxc/manager/proxmox{, -perl-rs}/storage v4 00/15] support OCI images as container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox v4 01/15] io: introduce RangeReader for bounded reads Filip Schauer
2025-09-08 15:02 ` Filip Schauer [this message]
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox v4 03/15] proxmox-oci: add tests for whiteout handling Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH proxmox-perl-rs v4 04/15] add Perl mapping for OCI container image parser/extractor Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH lxc v4 05/15] lxc: conf: split `lxc.environment` into `runtime` and `hooks` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 06/15] config: add `lxc.environment.runtime`/`hooks` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 07/15] add support for OCI images as container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 08/15] config: add entrypoint parameter Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 09/15] configure static IP in LXC config for custom entrypoint Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 10/15] setup: debian: create /etc/network path if missing Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 11/15] setup: recursively mkdir /etc/systemd/{network, system-preset} Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH container v4 12/15] implement host-managed DHCP for containers with `ipmanagehost` Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH storage v4 13/15] allow .tar container templates Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH manager v4 14/15] ui: storage upload: accept *.tar files as vztmpl Filip Schauer
2025-09-08 15:02 ` [pve-devel] [PATCH docs v4 15/15] ct: add OCI image docs 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=20250908150224.155373-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