Skip to content

Commit c3ae26e

Browse files
committed
fix: adds support for OCI images with embedded attestations
1 parent 0ce2f96 commit c3ae26e

File tree

4 files changed

+95
-12
lines changed

4 files changed

+95
-12
lines changed

lib/extractor/decompress-maybe.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ export function decompressMaybe(): Transform {
4040
compressionType = "gzip";
4141
headerRead = true;
4242

43-
// Setup gzip decompressor
4443
gzipStream = createGunzip();
4544
gzipStream.on("data", (data: Buffer) => transform.push(data));
4645
gzipStream.on("error", (err: Error) => transform.destroy(err));
4746

48-
// Write buffered data
49-
gzipStream.write(combined);
47+
try {
48+
gzipStream.write(combined);
49+
} catch (err) {
50+
callback(err instanceof Error ? err : new Error(String(err)));
51+
return;
52+
}
5053
buffer.length = 0;
5154
callback();
5255
}
@@ -61,14 +64,12 @@ export function decompressMaybe(): Transform {
6164
compressionType = "zstd";
6265
headerRead = true;
6366

64-
// Setup zstd decompressor with streaming API
6567
zstdStream = new ZstdDecompress(
6668
(data: Uint8Array, final?: boolean) => {
6769
transform.push(Buffer.from(data));
6870
},
6971
);
7072

71-
// Write buffered data
7273
try {
7374
zstdStream.push(new Uint8Array(combined), false);
7475
} catch (err) {
@@ -100,7 +101,12 @@ export function decompressMaybe(): Transform {
100101
} else {
101102
// Header already read
102103
if (compressionType === "gzip" && gzipStream) {
103-
gzipStream.write(chunk);
104+
try {
105+
gzipStream.write(chunk);
106+
} catch (err) {
107+
callback(err instanceof Error ? err : new Error(String(err)));
108+
return;
109+
}
104110
callback();
105111
} else if (compressionType === "zstd" && zstdStream) {
106112
try {
@@ -122,12 +128,12 @@ export function decompressMaybe(): Transform {
122128
}
123129
},
124130

125-
async flush(callback) {
131+
flush(callback) {
126132
if (compressionType === "gzip" && gzipStream) {
127133
gzipStream.once("end", () => callback());
134+
gzipStream.once("error", (err) => callback(err));
128135
gzipStream.end();
129136
} else if (compressionType === "zstd" && zstdStream) {
130-
// Signal end of zstd stream
131137
try {
132138
zstdStream.push(new Uint8Array(0), true);
133139
callback();

lib/extractor/oci-archive/layer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ export async function extractArchive(
6767
stream.pipe(layerStream);
6868

6969
const promises = [
70-
streamToJson(jsonStream).catch(() => undefined),
71-
extractImageLayer(layerStream, extractActions).catch(
72-
() => undefined,
73-
),
70+
streamToJson(jsonStream).catch(() => {
71+
jsonStream.destroy();
72+
return undefined;
73+
}),
74+
extractImageLayer(layerStream, extractActions).catch(() => {
75+
layerStream.destroy();
76+
return undefined;
77+
}),
7478
];
7579
const [manifest, layer] = await Promise.all(promises);
7680

298 KB
Binary file not shown.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { extractImageContent } from "../../../lib/extractor";
2+
import { ImageType } from "../../../lib/types";
3+
import { getFixture } from "../../util/index";
4+
5+
describe("OCI archive with attestation blobs", () => {
6+
const fixture = getFixture("oci-archives/oci-with-attestations.tar");
7+
const opts = { platform: "linux/amd64" };
8+
9+
it("successfully extracts layers without deadlocking on large attestation blobs", async () => {
10+
const result = await extractImageContent(
11+
ImageType.OciArchive,
12+
fixture,
13+
[],
14+
opts,
15+
);
16+
expect(result.manifestLayers.length).toBe(1);
17+
expect(result.extractedLayers).toBeDefined();
18+
expect(result.imageId).toContain("sha256:");
19+
});
20+
21+
it("extracts when image type is unset (fallback path)", async () => {
22+
await expect(
23+
extractImageContent(0, fixture, [], opts),
24+
).resolves.not.toThrow();
25+
});
26+
27+
it("returns correct platform from image config", async () => {
28+
const result = await extractImageContent(
29+
ImageType.OciArchive,
30+
fixture,
31+
[],
32+
opts,
33+
);
34+
expect(result.platform).toBe("linux/amd64");
35+
});
36+
37+
it("returns rootfs layer diff IDs", async () => {
38+
const result = await extractImageContent(
39+
ImageType.OciArchive,
40+
fixture,
41+
[],
42+
opts,
43+
);
44+
expect(result.rootFsLayers).toBeDefined();
45+
expect(result.rootFsLayers!.length).toBe(1);
46+
});
47+
48+
it("extracts layer file content via extract actions", async () => {
49+
const extractActions = [
50+
{
51+
actionName: "read_hello",
52+
filePathMatches: (filePath: string) => filePath.endsWith("hello.txt"),
53+
callback: async (stream: NodeJS.ReadableStream) => {
54+
const chunks: Buffer[] = [];
55+
for await (const chunk of stream) {
56+
chunks.push(chunk as Buffer);
57+
}
58+
return chunks.join("");
59+
},
60+
},
61+
];
62+
63+
const result = await extractImageContent(
64+
ImageType.OciArchive,
65+
fixture,
66+
extractActions,
67+
opts,
68+
);
69+
70+
const helloContent = result.extractedLayers["/hello.txt"].read_hello;
71+
expect(helloContent).toBe("hello\n");
72+
});
73+
});

0 commit comments

Comments
 (0)