Skip to content
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

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

Merged
merged 6 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions tar/tar_stream_test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals, assertRejects, assertThrows } from "@std/assert";
import { concat } from "@std/bytes";
import {
assertValidTarStreamOptions,
TarStream,
type TarStreamInput,
} from "./tar_stream.ts";
import { assertEquals, assertRejects, assertThrows } from "../assert/mod.ts";
import { UntarStream } from "./untar_stream.ts";
import { concat } from "../bytes/mod.ts";

Deno.test("TarStream() with default stream", async () => {
const text = new TextEncoder().encode("Hello World!");
Expand Down
64 changes: 36 additions & 28 deletions tar/untar_stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ export class UntarStream
implements TransformStream<Uint8Array, TarStreamEntry> {
#readable: ReadableStream<TarStreamEntry>;
#writable: WritableStream<Uint8Array>;
#gen: AsyncGenerator<Uint8Array>;
#reader: ReadableStreamDefaultReader<Uint8Array>;
#buffer: Uint8Array[] = [];
#lock = false;
constructor() {
const { readable, writable } = new TransformStream<
Expand All @@ -185,43 +186,50 @@ export class UntarStream
>();
this.#readable = ReadableStream.from(this.#untar());
this.#writable = writable;
this.#reader = readable.pipeThrough(new FixedChunkStream(512)).getReader();
}

this.#gen = async function* () {
const buffer: Uint8Array[] = [];
for await (
const chunk of readable.pipeThrough(new FixedChunkStream(512))
) {
if (chunk.length !== 512) {
throw new RangeError(
`Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (${chunk.length})`,
);
}
async #read(): Promise<Uint8Array | undefined> {
const { done, value } = await this.#reader.read();
if (done) return undefined;
if (value.length !== 512) {
throw new RangeError(
`Cannot extract the tar archive: The tarball chunk has an unexpected number of bytes (${value.length})`,
);
}
this.#buffer.push(value);
return this.#buffer.shift();
}

buffer.push(chunk);
if (buffer.length > 2) yield buffer.shift()!;
}
if (buffer.length < 2) {
async *#untar(): AsyncGenerator<TarStreamEntry> {
for (let i = 0; i < 2; ++i) {
const { done, value } = await this.#reader.read();
if (done || value.length !== 512) {
throw new RangeError(
"Cannot extract the tar archive: The tarball is too small to be valid",
);
}
if (!buffer.every((value) => value.every((x) => x === 0))) {
throw new TypeError(
"Cannot extract the tar archive: The tarball has invalid ending",
);
}
}();
}

async *#untar(): AsyncGenerator<TarStreamEntry> {
this.#buffer.push(value);
}
const decoder = new TextDecoder();
while (true) {
while (this.#lock) {
await new Promise((resolve) => setTimeout(resolve, 0));
}

const { done, value } = await this.#gen.next();
if (done) break;
// Check for premature ending
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
const checksum = parseInt(
Expand Down Expand Up @@ -286,8 +294,8 @@ export class UntarStream

async *#genFile(size: number): AsyncGenerator<Uint8Array> {
for (let i = Math.ceil(size / 512); i > 0; --i) {
const { done, value } = await this.#gen.next();
if (done) {
const value = await this.#read();
if (value == undefined) {
throw new SyntaxError(
"Cannot extract the tar archive: Unexpected end of Tarball",
);
Expand Down
145 changes: 69 additions & 76 deletions tar/untar_stream_test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// 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 {
type OldStyleFormat,
type PosixUstarFormat,
UntarStream,
} from "./untar_stream.ts";
import { assertEquals, assertRejects } from "../assert/mod.ts";

Deno.test("expandTarArchiveCheckingHeaders", async () => {
const text = new TextEncoder().encode("Hello World!");
Expand Down Expand Up @@ -39,9 +40,9 @@ Deno.test("expandTarArchiveCheckingHeaders", async () => {
.pipeThrough(new UntarStream());

const headers: (OldStyleFormat | PosixUstarFormat)[] = [];
for await (const item of readable) {
headers.push(item.header);
await item.readable?.cancel();
for await (const entry of readable) {
headers.push(entry.header);
await entry.readable?.cancel();
}
assertEquals(headers, [{
name: "./potato",
Expand Down Expand Up @@ -98,9 +99,7 @@ Deno.test("expandTarArchiveCheckingBodies", async () => {

let buffer = new Uint8Array();
for await (const item of readable) {
if (item.readable) {
buffer = concat(await Array.fromAsync(item.readable));
}
if (item.readable) buffer = await toBytes(item.readable);
}
assertEquals(buffer, text);
});
Expand All @@ -125,59 +124,47 @@ Deno.test("UntarStream() with size equals to multiple of 512", async () => {

let buffer = new Uint8Array();
for await (const entry of readable) {
if (entry.readable) {
buffer = concat(await Array.fromAsync(entry.readable));
}
if (entry.readable) buffer = await toBytes(entry.readable);
}
assertEquals(buffer, data);
});

Deno.test("UntarStream() with invalid size", async () => {
const readable = ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream())
.pipeThrough(
new TransformStream<Uint8Array, Uint8Array>({
flush(controller) {
controller.enqueue(new Uint8Array(100));
},
}),
)
const bytes = (await toBytes(
ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream()),
)).slice(0, -100);

const readable = ReadableStream.from([bytes])
.pipeThrough(new UntarStream());

await assertRejects(
async () => {
for await (const entry of readable) {
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
for await (const entry of readable) await entry.readable?.cancel();
},
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 () => {
const tarBytes = concat(
await Array.fromAsync(
ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream()),
),
const tarBytes = await toBytes(
ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream()),
);
tarBytes[tarBytes.length - 1] = 1;

Expand All @@ -186,12 +173,7 @@ Deno.test("UntarStream() with invalid ending", async () => {

await assertRejects(
async () => {
for await (const entry of readable) {
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
for await (const entry of readable) await entry.readable?.cancel();
},
TypeError,
"Cannot extract the tar archive: The tarball has invalid ending",
Expand All @@ -204,31 +186,24 @@ Deno.test("UntarStream() with too small size", async () => {

await assertRejects(
async () => {
for await (const entry of readable) {
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
for await (const entry of readable) await entry.readable?.cancel();
},
RangeError,
"Cannot extract the tar archive: The tarball is too small to be valid",
);
});

Deno.test("UntarStream() with invalid checksum", async () => {
const tarBytes = concat(
await Array.fromAsync(
ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream()),
),
const tarBytes = await toBytes(
ReadableStream.from<TarStreamInput>([
{
type: "file",
path: "newFile.txt",
size: 512,
readable: ReadableStream.from([new Uint8Array(512).fill(97)]),
},
])
.pipeThrough(new TarStream()),
);
tarBytes[148] = 97;

Expand All @@ -237,14 +212,32 @@ Deno.test("UntarStream() with invalid checksum", async () => {

await assertRejects(
async () => {
for await (const entry of readable) {
if (entry.readable) {
// deno-lint-ignore no-empty
for await (const _ of entry.readable) {}
}
}
for await (const entry of readable) await entry.readable?.cancel();
},
Error,
"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();
}
});