fix(tar): ignore non-tar file portion of a stream in UntarStream (#6064)

This commit is contained in:
Doctor 2024-09-30 13:31:41 +10:00 committed by GitHub
parent 361be4569d
commit e43a7df4b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 106 deletions

View File

@ -1,12 +1,13 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { concat } from "@std/bytes";
import { import {
assertValidTarStreamOptions, assertValidTarStreamOptions,
TarStream, TarStream,
type TarStreamInput, type TarStreamInput,
} from "./tar_stream.ts"; } from "./tar_stream.ts";
import { assertEquals, assertRejects, assertThrows } from "../assert/mod.ts";
import { UntarStream } from "./untar_stream.ts"; import { UntarStream } from "./untar_stream.ts";
import { concat } from "../bytes/mod.ts";
Deno.test("TarStream() with default stream", async () => { Deno.test("TarStream() with default stream", async () => {
const text = new TextEncoder().encode("Hello World!"); const text = new TextEncoder().encode("Hello World!");

View File

@ -176,7 +176,8 @@ export class UntarStream
implements TransformStream<Uint8Array, TarStreamEntry> { implements TransformStream<Uint8Array, TarStreamEntry> {
#readable: ReadableStream<TarStreamEntry>; #readable: ReadableStream<TarStreamEntry>;
#writable: WritableStream<Uint8Array>; #writable: WritableStream<Uint8Array>;
#gen: AsyncGenerator<Uint8Array>; #reader: ReadableStreamDefaultReader<Uint8Array>;
#buffer: Uint8Array[] = [];
#lock = false; #lock = false;
constructor() { constructor() {
const { readable, writable } = new TransformStream< const { readable, writable } = new TransformStream<
@ -185,43 +186,50 @@ export class UntarStream
>(); >();
this.#readable = ReadableStream.from(this.#untar()); this.#readable = ReadableStream.from(this.#untar());
this.#writable = writable; this.#writable = writable;
this.#reader = readable.pipeThrough(new FixedChunkStream(512)).getReader();
}
this.#gen = async function* () { async #read(): Promise<Uint8Array | undefined> {
const buffer: Uint8Array[] = []; const { done, value } = await this.#reader.read();
for await ( if (done) return undefined;
const chunk of readable.pipeThrough(new FixedChunkStream(512)) if (value.length !== 512) {
) { throw new RangeError(
if (chunk.length !== 512) { `Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (${value.length})`,
throw new RangeError( );
`Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (${chunk.length})`, }
); this.#buffer.push(value);
} return this.#buffer.shift();
}
buffer.push(chunk); async *#untar(): AsyncGenerator<TarStreamEntry> {
if (buffer.length > 2) yield buffer.shift()!; for (let i = 0; i < 2; ++i) {
} const { done, value } = await this.#reader.read();
if (buffer.length < 2) { if (done || value.length !== 512) {
throw new RangeError( throw new RangeError(
"Cannot extract the tar archive: The tarball is too small to be valid", "Cannot extract the tar archive: The tarball is too small to be valid",
); );
} }
if (!buffer.every((value) => value.every((x) => x === 0))) { this.#buffer.push(value);
throw new TypeError( }
"Cannot extract the tar archive: The tarball has invalid ending",
);
}
}();
}
async *#untar(): AsyncGenerator<TarStreamEntry> {
const decoder = new TextDecoder(); const decoder = new TextDecoder();
while (true) { while (true) {
while (this.#lock) { while (this.#lock) {
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
} }
const { done, value } = await this.#gen.next(); // Check for premature ending
if (done) break; if (this.#buffer.every((value) => value.every((x) => x === 0))) {
await this.#reader.cancel("Tar stream finished prematurely");
return;
}
const value = await this.#read();
if (value == undefined) {
if (this.#buffer.every((value) => value.every((x) => x === 0))) break;
throw new TypeError(
"Cannot extract the tar archive: The tarball has invalid ending",
);
}
// Validate Checksum // Validate Checksum
const checksum = parseInt( const checksum = parseInt(
@ -286,8 +294,8 @@ export class UntarStream
async *#genFile(size: number): AsyncGenerator<Uint8Array> { async *#genFile(size: number): AsyncGenerator<Uint8Array> {
for (let i = Math.ceil(size / 512); i > 0; --i) { for (let i = Math.ceil(size / 512); i > 0; --i) {
const { done, value } = await this.#gen.next(); const value = await this.#read();
if (done) { if (value == undefined) {
throw new SyntaxError( throw new SyntaxError(
"Cannot extract the tar archive: Unexpected end of Tarball", "Cannot extract the tar archive: Unexpected end of Tarball",
); );

View File

@ -1,12 +1,13 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { concat } from "../bytes/mod.ts";
import { assertEquals, assertRejects } from "@std/assert";
import { toBytes } from "@std/streams/unstable-to-bytes";
import { TarStream, type TarStreamInput } from "./tar_stream.ts"; import { TarStream, type TarStreamInput } from "./tar_stream.ts";
import { import {
type OldStyleFormat, type OldStyleFormat,
type PosixUstarFormat, type PosixUstarFormat,
UntarStream, UntarStream,
} from "./untar_stream.ts"; } from "./untar_stream.ts";
import { assertEquals, assertRejects } from "../assert/mod.ts";
Deno.test("expandTarArchiveCheckingHeaders", async () => { Deno.test("expandTarArchiveCheckingHeaders", async () => {
const text = new TextEncoder().encode("Hello World!"); const text = new TextEncoder().encode("Hello World!");
@ -39,9 +40,9 @@ Deno.test("expandTarArchiveCheckingHeaders", async () => {
.pipeThrough(new UntarStream()); .pipeThrough(new UntarStream());
const headers: (OldStyleFormat | PosixUstarFormat)[] = []; const headers: (OldStyleFormat | PosixUstarFormat)[] = [];
for await (const item of readable) { for await (const entry of readable) {
headers.push(item.header); headers.push(entry.header);
await item.readable?.cancel(); await entry.readable?.cancel();
} }
assertEquals(headers, [{ assertEquals(headers, [{
name: "./potato", name: "./potato",
@ -98,9 +99,7 @@ Deno.test("expandTarArchiveCheckingBodies", async () => {
let buffer = new Uint8Array(); let buffer = new Uint8Array();
for await (const item of readable) { for await (const item of readable) {
if (item.readable) { if (item.readable) buffer = await toBytes(item.readable);
buffer = concat(await Array.fromAsync(item.readable));
}
} }
assertEquals(buffer, text); assertEquals(buffer, text);
}); });
@ -125,59 +124,47 @@ Deno.test("UntarStream() with size equals to multiple of 512", async () => {
let buffer = new Uint8Array(); let buffer = new Uint8Array();
for await (const entry of readable) { for await (const entry of readable) {
if (entry.readable) { if (entry.readable) buffer = await toBytes(entry.readable);
buffer = concat(await Array.fromAsync(entry.readable));
}
} }
assertEquals(buffer, data); assertEquals(buffer, data);
}); });
Deno.test("UntarStream() with invalid size", async () => { Deno.test("UntarStream() with invalid size", async () => {
const readable = ReadableStream.from<TarStreamInput>([ const bytes = (await toBytes(
{ ReadableStream.from<TarStreamInput>([
type: "file", {
path: "newFile.txt", type: "file",
size: 512, path: "newFile.txt",
readable: ReadableStream.from([new Uint8Array(512).fill(97)]), size: 512,
}, readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
]) },
.pipeThrough(new TarStream()) ])
.pipeThrough( .pipeThrough(new TarStream()),
new TransformStream<Uint8Array, Uint8Array>({ )).slice(0, -100);
flush(controller) {
controller.enqueue(new Uint8Array(100)); const readable = ReadableStream.from([bytes])
},
}),
)
.pipeThrough(new UntarStream()); .pipeThrough(new UntarStream());
await assertRejects( await assertRejects(
async () => { async () => {
for await (const entry of readable) { for await (const entry of readable) await entry.readable?.cancel();
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
}, },
RangeError, RangeError,
"Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (100)", "Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (412)",
); );
}); });
Deno.test("UntarStream() with invalid ending", async () => { Deno.test("UntarStream() with invalid ending", async () => {
const tarBytes = concat( const tarBytes = await toBytes(
await Array.fromAsync( ReadableStream.from<TarStreamInput>([
ReadableStream.from<TarStreamInput>([ {
{ type: "file",
type: "file", path: "newFile.txt",
path: "newFile.txt", size: 512,
size: 512, readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
readable: ReadableStream.from([new Uint8Array(512).fill(97)]), },
}, ])
]) .pipeThrough(new TarStream()),
.pipeThrough(new TarStream()),
),
); );
tarBytes[tarBytes.length - 1] = 1; tarBytes[tarBytes.length - 1] = 1;
@ -186,12 +173,7 @@ Deno.test("UntarStream() with invalid ending", async () => {
await assertRejects( await assertRejects(
async () => { async () => {
for await (const entry of readable) { for await (const entry of readable) await entry.readable?.cancel();
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
}, },
TypeError, TypeError,
"Cannot extract the tar archive: The tarball has invalid ending", "Cannot extract the tar archive: The tarball has invalid ending",
@ -204,12 +186,7 @@ Deno.test("UntarStream() with too small size", async () => {
await assertRejects( await assertRejects(
async () => { async () => {
for await (const entry of readable) { for await (const entry of readable) await entry.readable?.cancel();
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
}, },
RangeError, RangeError,
"Cannot extract the tar archive: The tarball is too small to be valid", "Cannot extract the tar archive: The tarball is too small to be valid",
@ -217,18 +194,16 @@ Deno.test("UntarStream() with too small size", async () => {
}); });
Deno.test("UntarStream() with invalid checksum", async () => { Deno.test("UntarStream() with invalid checksum", async () => {
const tarBytes = concat( const tarBytes = await toBytes(
await Array.fromAsync( ReadableStream.from<TarStreamInput>([
ReadableStream.from<TarStreamInput>([ {
{ type: "file",
type: "file", path: "newFile.txt",
path: "newFile.txt", size: 512,
size: 512, readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
readable: ReadableStream.from([new Uint8Array(512).fill(97)]), },
}, ])
]) .pipeThrough(new TarStream()),
.pipeThrough(new TarStream()),
),
); );
tarBytes[148] = 97; tarBytes[148] = 97;
@ -237,14 +212,32 @@ Deno.test("UntarStream() with invalid checksum", async () => {
await assertRejects( await assertRejects(
async () => { async () => {
for await (const entry of readable) { for await (const entry of readable) await entry.readable?.cancel();
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
}, },
Error, Error,
"Cannot extract the tar archive: An archive entry has invalid header checksum", "Cannot extract the tar archive: An archive entry has invalid header checksum",
); );
}); });
Deno.test("UntarStream() with extra bytes", async () => {
const readable = ReadableStream.from<TarStreamInput>([
{
type: "directory",
path: "a",
},
])
.pipeThrough(new TarStream())
.pipeThrough(
new TransformStream({
flush(controller) {
controller.enqueue(new Uint8Array(512 * 2).fill(1));
},
}),
)
.pipeThrough(new UntarStream());
for await (const entry of readable) {
assertEquals(entry.path, "a");
entry.readable?.cancel();
}
});