-
Notifications
You must be signed in to change notification settings - Fork 3
fix: layer detection did not correctly detect +gzip suffixes #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,9 @@ pub mod media_types { | |
| "application/vnd.docker.distribution.manifest.list.v2+json"; | ||
|
|
||
| // Layer media types | ||
| #[cfg(test)] | ||
| pub const OCI_LAYER_GZIP: &str = "application/vnd.oci.image.layer.v1.tar+gzip"; | ||
| #[cfg(test)] | ||
| pub const OCI_LAYER_ZSTD: &str = "application/vnd.oci.image.layer.v1.tar+zstd"; | ||
| pub const DOCKER_LAYER: &str = "application/vnd.docker.image.rootfs.diff.tar.gzip"; | ||
| } | ||
|
|
@@ -152,7 +154,12 @@ impl Manifest { | |
|
|
||
| // If artifactType is set, find the layer matching it | ||
| if let Some(ref artifact_type) = m.artifact_type { | ||
| if let Some(layer) = m.layers.iter().find(|l| l.media_type == *artifact_type) { | ||
| let expected_base = split_media_type(artifact_type).0; | ||
| if let Some(layer) = m.layers.iter().find(|l| { | ||
| FlashableArtifact::is_flashable(&l.media_type) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Skip unsupported Zstd layers during selection.
Filter this search and the fallback by extraction-supported compression, or continue searching after an unsupported compression. Add a test with 🤖 Prompt for AI Agents |
||
| && (l.media_type == *artifact_type | ||
| || split_media_type(&l.media_type).0 == expected_base) | ||
| }) { | ||
| return Ok(layer); | ||
| } | ||
| } | ||
|
|
@@ -207,16 +214,31 @@ impl ImageIndex { | |
| } | ||
| } | ||
|
|
||
| /// Split a media type into its base media type and optional structured syntax compression suffix. | ||
| fn split_media_type(media_type: &str) -> (&str, Option<&str>) { | ||
| if media_type == media_types::DOCKER_LAYER { | ||
| (media_type, Some("gzip")) | ||
| } else if let Some((base, suffix)) = media_type.split_once('+') { | ||
| (base, Some(suffix)) | ||
| } else { | ||
| (media_type, None) | ||
| } | ||
| } | ||
|
|
||
| impl Descriptor { | ||
| /// Check if this is a gzip-compressed layer | ||
| pub fn is_gzip_layer(&self) -> bool { | ||
| self.media_type == media_types::OCI_LAYER_GZIP | ||
| || self.media_type == media_types::DOCKER_LAYER | ||
| self.compression() == LayerCompression::Gzip | ||
| } | ||
|
|
||
| /// Check if this is a zstd-compressed layer | ||
| pub fn is_zstd_layer(&self) -> bool { | ||
| self.media_type == media_types::OCI_LAYER_ZSTD | ||
| self.compression() == LayerCompression::Zstd | ||
| } | ||
|
|
||
| /// Check if this is an xz-compressed layer | ||
| pub fn is_xz_layer(&self) -> bool { | ||
| self.compression() == LayerCompression::Xz | ||
| } | ||
|
|
||
| #[allow(dead_code)] | ||
|
|
@@ -226,26 +248,26 @@ impl Descriptor { | |
|
|
||
| /// Get compression type | ||
| pub fn compression(&self) -> LayerCompression { | ||
| if self.is_gzip_layer() { | ||
| LayerCompression::Gzip | ||
| } else if self.is_zstd_layer() { | ||
| LayerCompression::Zstd | ||
| } else { | ||
| LayerCompression::None | ||
| match split_media_type(&self.media_type).1 { | ||
| Some("gzip") => LayerCompression::Gzip, | ||
| Some("zstd") => LayerCompression::Zstd, | ||
| Some("xz") => LayerCompression::Xz, | ||
| _ => LayerCompression::None, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Layer compression type | ||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum LayerCompression { | ||
| None, | ||
| Gzip, | ||
| Xz, | ||
| Zstd, | ||
| } | ||
|
|
||
| /// Flashable disk image artifact types | ||
| #[derive(Debug, Clone, Copy, PartialEq)] | ||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
| pub enum FlashableArtifact { | ||
| DiskRaw, | ||
| DiskQcow2, | ||
|
|
@@ -258,18 +280,26 @@ impl FlashableArtifact { | |
| "application/vnd.embedded.disk", | ||
| ]; | ||
|
|
||
| /// Parse a [`FlashableArtifact`] from an OCI layer media type string, validating supported format and compression. | ||
| pub fn from_media_type(media_type: &str) -> Option<Self> { | ||
| let suffix = Self::MEDIA_TYPE_PREFIXES | ||
| let (base, suffix) = split_media_type(media_type); | ||
| if let Some(s) = suffix { | ||
| if !matches!(s, "gzip" | "zstd" | "xz") { | ||
| return None; | ||
| } | ||
| } | ||
| let format = Self::MEDIA_TYPE_PREFIXES | ||
| .iter() | ||
| .find_map(|prefix| media_type.strip_prefix(prefix))?; | ||
| match suffix { | ||
| .find_map(|prefix| base.strip_prefix(prefix))?; | ||
| match format { | ||
| ".raw" => Some(Self::DiskRaw), | ||
| ".qcow2" => Some(Self::DiskQcow2), | ||
| ".simg" => Some(Self::DiskSimg), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| /// Return the standard file extension / format suffix for this artifact type (e.g., ".raw"). | ||
| pub fn format_suffix(&self) -> &'static str { | ||
| match self { | ||
| Self::DiskRaw => ".raw", | ||
|
|
@@ -278,10 +308,12 @@ impl FlashableArtifact { | |
| } | ||
| } | ||
|
|
||
| /// Check whether the given media type corresponds to a supported flashable artifact. | ||
| pub fn is_flashable(media_type: &str) -> bool { | ||
| Self::from_media_type(media_type).is_some() | ||
| } | ||
|
|
||
| /// Return all supported uncompressed media type strings for flashable artifacts. | ||
| pub fn supported_types() -> Vec<String> { | ||
| let mut types = Vec::new(); | ||
| for prefix in Self::MEDIA_TYPE_PREFIXES { | ||
|
|
@@ -298,6 +330,7 @@ impl From<LayerCompression> for crate::fls::compression::Compression { | |
| match layer_compression { | ||
| LayerCompression::None => crate::fls::compression::Compression::None, | ||
| LayerCompression::Gzip => crate::fls::compression::Compression::Gzip, | ||
| LayerCompression::Xz => crate::fls::compression::Compression::Xz, | ||
| LayerCompression::Zstd => crate::fls::compression::Compression::Zstd, | ||
| } | ||
| } | ||
|
|
@@ -561,5 +594,143 @@ mod tests { | |
| ); | ||
| } | ||
| } | ||
|
|
||
| // Compressed variants | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.automotive.disk.raw+gzip"), | ||
| Some(FlashableArtifact::DiskRaw) | ||
| ); | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.automotive.disk.raw+zstd"), | ||
| Some(FlashableArtifact::DiskRaw) | ||
| ); | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.automotive.disk.raw+xz"), | ||
| Some(FlashableArtifact::DiskRaw) | ||
| ); | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.embedded.disk.qcow2+gzip"), | ||
| Some(FlashableArtifact::DiskQcow2) | ||
| ); | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.embedded.disk.simg+zstd"), | ||
| Some(FlashableArtifact::DiskSimg) | ||
| ); | ||
|
|
||
| // Unrecognized compression suffixes are rejected | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.automotive.disk.raw+unknown"), | ||
| None | ||
| ); | ||
| assert_eq!( | ||
| FlashableArtifact::from_media_type("application/vnd.automotive.disk.raw+tar"), | ||
| None | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_descriptor_compression() { | ||
| let make_desc = |media_type: &str| Descriptor { | ||
| media_type: media_type.to_string(), | ||
| digest: "sha256:123".to_string(), | ||
| size: 100, | ||
| annotations: None, | ||
| platform: None, | ||
| }; | ||
|
|
||
| let raw = make_desc("application/vnd.automotive.disk.raw"); | ||
| assert_eq!(raw.compression(), LayerCompression::None); | ||
| assert!(!raw.is_gzip_layer()); | ||
| assert!(!raw.is_zstd_layer()); | ||
|
|
||
| let raw_gz = make_desc("application/vnd.automotive.disk.raw+gzip"); | ||
| assert_eq!(raw_gz.compression(), LayerCompression::Gzip); | ||
| assert!(raw_gz.is_gzip_layer()); | ||
| assert!(!raw_gz.is_zstd_layer()); | ||
|
|
||
| let raw_zstd = make_desc("application/vnd.automotive.disk.raw+zstd"); | ||
| assert_eq!(raw_zstd.compression(), LayerCompression::Zstd); | ||
| assert!(!raw_zstd.is_gzip_layer()); | ||
| assert!(raw_zstd.is_zstd_layer()); | ||
| assert!(!raw_zstd.is_xz_layer()); | ||
|
|
||
| let raw_xz = make_desc("application/vnd.automotive.disk.raw+xz"); | ||
| assert_eq!(raw_xz.compression(), LayerCompression::Xz); | ||
| assert!(!raw_xz.is_gzip_layer()); | ||
| assert!(!raw_xz.is_zstd_layer()); | ||
| assert!(raw_xz.is_xz_layer()); | ||
|
|
||
| let oci_gz = make_desc(media_types::OCI_LAYER_GZIP); | ||
| assert_eq!(oci_gz.compression(), LayerCompression::Gzip); | ||
| assert!(oci_gz.is_gzip_layer()); | ||
| assert!(!oci_gz.is_zstd_layer()); | ||
|
|
||
| let oci_zstd = make_desc(media_types::OCI_LAYER_ZSTD); | ||
| assert_eq!(oci_zstd.compression(), LayerCompression::Zstd); | ||
| assert!(!oci_zstd.is_gzip_layer()); | ||
| assert!(oci_zstd.is_zstd_layer()); | ||
|
|
||
| let docker = make_desc(media_types::DOCKER_LAYER); | ||
| assert_eq!(docker.compression(), LayerCompression::Gzip); | ||
| assert!(docker.is_gzip_layer()); | ||
| assert!(!docker.is_zstd_layer()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_artifact_type_selection_with_compression() { | ||
| let json = r#"{ | ||
| "schemaVersion": 2, | ||
| "artifactType": "application/vnd.automotive.disk.qcow2", | ||
| "config": { | ||
| "mediaType": "application/vnd.oci.image.config.v1+json", | ||
| "digest": "sha256:config123", | ||
| "size": 100 | ||
| }, | ||
| "layers": [ | ||
| { | ||
| "mediaType": "application/vnd.automotive.disk.raw+gzip", | ||
| "digest": "sha256:disk_raw", | ||
| "size": 1000 | ||
| }, | ||
| { | ||
| "mediaType": "application/vnd.automotive.disk.qcow2+gzip", | ||
| "digest": "sha256:disk_qcow2", | ||
| "size": 9999 | ||
| } | ||
| ] | ||
| }"#; | ||
| let manifest = Manifest::parse(json.as_bytes(), None).unwrap(); | ||
| let layer = manifest.get_single_layer().unwrap(); | ||
| assert_eq!(layer.digest, "sha256:disk_qcow2"); | ||
| assert_eq!(layer.compression(), LayerCompression::Gzip); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_unsupported_layer_preceding_supported_layer_with_same_base() { | ||
| let json = r#"{ | ||
| "schemaVersion": 2, | ||
| "artifactType": "application/vnd.automotive.disk.raw", | ||
| "config": { | ||
| "mediaType": "application/vnd.oci.image.config.v1+json", | ||
| "digest": "sha256:config123", | ||
| "size": 100 | ||
| }, | ||
| "layers": [ | ||
| { | ||
| "mediaType": "application/vnd.automotive.disk.raw+unknown", | ||
| "digest": "sha256:unsupported_raw", | ||
| "size": 1000 | ||
| }, | ||
| { | ||
| "mediaType": "application/vnd.automotive.disk.raw+gzip", | ||
| "digest": "sha256:supported_raw", | ||
| "size": 2000 | ||
| } | ||
| ] | ||
| }"#; | ||
| let manifest = Manifest::parse(json.as_bytes(), None).unwrap(); | ||
| let layer = manifest.get_single_layer().unwrap(); | ||
| assert_eq!(layer.digest, "sha256:supported_raw"); | ||
| assert_eq!(layer.compression(), LayerCompression::Gzip); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: jumpstarter-dev/fls
Length of output: 1551
🏁 Script executed:
Repository: jumpstarter-dev/fls
Length of output: 13921
🤖 get_repo_knowledge executed:
get_repo_knowledge jumpstarter-dev/fls /tmp/coderabbit-repo-knowledge/jumpstarter-dev-fls-6f8b2e22/architectureLength of output: 2683
Declare the MSRV or retain compatibility with Rust 1.87
Cargo.tomldeclares norust-version, and all CI and release jobs usestable.slice::as_chunks_mut::<4>()requires Rust 1.88 and can fail to compile on an older supported toolchain. If Rust versions before 1.88 are supported, retainchunks_exact_mut(4); otherwise declare the chosen MSRV inCargo.toml.🤖 Prompt for AI Agents
Source: MCP tools