Skip to content

Commit 987ebfb

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

File tree

4 files changed

+97
-12
lines changed

4 files changed

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

0 commit comments

Comments
 (0)