Skip to content

Commit ab866a4

Browse files
bennyzclaude
authored andcommitted
Validate media type on all get_single_layer() return paths
Address review feedback: the single-layer and artifact_type paths were returning layers without checking if they have a flashable media type (application/vnd.automotive.disk.*). Now all paths use a shared is_flashable closure for consistent validation. Also add unit tests for: artifact_type selection, single non-disk layer rejection, single flashable layer acceptance, and no-match error. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent eab5e55 commit ab866a4

1 file changed

Lines changed: 125 additions & 8 deletions

File tree

src/fls/oci/manifest.rs

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,23 +142,33 @@ impl Manifest {
142142
return Err("Manifest has no layers".to_string());
143143
}
144144

145+
let is_flashable =
146+
|l: &&Descriptor| l.media_type.starts_with("application/vnd.automotive.disk");
147+
145148
if m.layers.len() == 1 {
146-
return Ok(&m.layers[0]);
149+
let layer = &m.layers[0];
150+
if is_flashable(&&layer) {
151+
return Ok(layer);
152+
}
153+
return Err(format!(
154+
"Single layer has non-flashable media type: {}",
155+
layer.media_type
156+
));
147157
}
148158

149-
// If artifactType is set, find the layer matching it
159+
// If artifactType is set, find the layer matching it (must also be flashable)
150160
if let Some(ref artifact_type) = m.artifact_type {
151-
if let Some(layer) = m.layers.iter().find(|l| l.media_type == *artifact_type) {
161+
if let Some(layer) = m
162+
.layers
163+
.iter()
164+
.find(|l| l.media_type == *artifact_type && is_flashable(l))
165+
{
152166
return Ok(layer);
153167
}
154168
}
155169

156170
// Fall back to the first disk image layer
157-
if let Some(layer) = m
158-
.layers
159-
.iter()
160-
.find(|l| l.media_type.starts_with("application/vnd.automotive.disk"))
161-
{
171+
if let Some(layer) = m.layers.iter().find(|l| is_flashable(l)) {
162172
return Ok(layer);
163173
}
164174

@@ -279,6 +289,113 @@ mod tests {
279289
}
280290
}
281291

292+
#[test]
293+
fn test_single_flashable_layer() {
294+
let json = r#"{
295+
"schemaVersion": 2,
296+
"config": {
297+
"mediaType": "application/vnd.oci.image.config.v1+json",
298+
"digest": "sha256:config123",
299+
"size": 100
300+
},
301+
"layers": [
302+
{
303+
"mediaType": "application/vnd.automotive.disk.raw",
304+
"digest": "sha256:disk123",
305+
"size": 9999
306+
}
307+
]
308+
}"#;
309+
let manifest = Manifest::parse(json.as_bytes(), None).unwrap();
310+
let layer = manifest.get_single_layer().unwrap();
311+
assert_eq!(layer.digest, "sha256:disk123");
312+
}
313+
314+
#[test]
315+
fn test_single_non_disk_layer_rejected() {
316+
let json = r#"{
317+
"schemaVersion": 2,
318+
"config": {
319+
"mediaType": "application/vnd.oci.image.config.v1+json",
320+
"digest": "sha256:config123",
321+
"size": 100
322+
},
323+
"layers": [
324+
{
325+
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
326+
"digest": "sha256:layer123",
327+
"size": 5678
328+
}
329+
]
330+
}"#;
331+
let manifest = Manifest::parse(json.as_bytes(), None).unwrap();
332+
let err = manifest.get_single_layer().unwrap_err();
333+
assert!(
334+
err.contains("non-flashable"),
335+
"Expected non-flashable error, got: {}",
336+
err
337+
);
338+
}
339+
340+
#[test]
341+
fn test_artifact_type_selection() {
342+
let json = r#"{
343+
"schemaVersion": 2,
344+
"artifactType": "application/vnd.automotive.disk.raw",
345+
"config": {
346+
"mediaType": "application/vnd.oci.image.config.v1+json",
347+
"digest": "sha256:config123",
348+
"size": 100
349+
},
350+
"layers": [
351+
{
352+
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
353+
"digest": "sha256:layer1",
354+
"size": 1000
355+
},
356+
{
357+
"mediaType": "application/vnd.automotive.disk.raw",
358+
"digest": "sha256:disk1",
359+
"size": 9999
360+
}
361+
]
362+
}"#;
363+
let manifest = Manifest::parse(json.as_bytes(), None).unwrap();
364+
let layer = manifest.get_single_layer().unwrap();
365+
assert_eq!(layer.digest, "sha256:disk1");
366+
}
367+
368+
#[test]
369+
fn test_no_flashable_layer_error() {
370+
let json = r#"{
371+
"schemaVersion": 2,
372+
"config": {
373+
"mediaType": "application/vnd.oci.image.config.v1+json",
374+
"digest": "sha256:config123",
375+
"size": 100
376+
},
377+
"layers": [
378+
{
379+
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
380+
"digest": "sha256:layer1",
381+
"size": 1000
382+
},
383+
{
384+
"mediaType": "application/vnd.oci.image.layer.v1.tar+zstd",
385+
"digest": "sha256:layer2",
386+
"size": 2000
387+
}
388+
]
389+
}"#;
390+
let manifest = Manifest::parse(json.as_bytes(), None).unwrap();
391+
let err = manifest.get_single_layer().unwrap_err();
392+
assert!(
393+
err.contains("No disk image layer found"),
394+
"Expected no-match error, got: {}",
395+
err
396+
);
397+
}
398+
282399
#[test]
283400
fn test_parse_index() {
284401
let json = r#"{

0 commit comments

Comments
 (0)