Skip to content

Commit c7b94e3

Browse files
authored
fix(archive/untar.ts): cannot access symlinks in archives (#3686)
1 parent 39c551d commit c7b94e3

File tree

3 files changed

+78
-11
lines changed

3 files changed

+78
-11
lines changed

archive/testdata/with_link.tar

10 KB
Binary file not shown.

archive/untar.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ import {
3939
import { readAll } from "../streams/read_all.ts";
4040
import type { Reader } from "../types.d.ts";
4141

42+
/**
43+
* Extend TarMeta with the `linkName` property so that readers can access
44+
* symbolic link values without polluting the world of archive writers.
45+
*/
46+
export interface TarMetaWithLinkName extends TarMeta {
47+
linkName?: string;
48+
}
49+
4250
export interface TarHeader {
4351
[key: string]: Uint8Array;
4452
}
@@ -73,7 +81,7 @@ function parseHeader(buffer: Uint8Array): { [key: string]: Uint8Array } {
7381
}
7482

7583
// deno-lint-ignore no-empty-interface
76-
export interface TarEntry extends TarMeta {}
84+
export interface TarEntry extends TarMetaWithLinkName {}
7785

7886
export class TarEntry implements Reader {
7987
#header: TarHeader;
@@ -83,7 +91,7 @@ export class TarEntry implements Reader {
8391
#consumed = false;
8492
#entrySize: number;
8593
constructor(
86-
meta: TarMeta,
94+
meta: TarMetaWithLinkName,
8795
header: TarHeader,
8896
reader: Reader | (Reader & Deno.Seeker),
8997
) {
@@ -244,22 +252,19 @@ export class Untar {
244252
return header;
245253
}
246254

247-
#getMetadata(header: TarHeader): TarMeta {
255+
#getMetadata(header: TarHeader): TarMetaWithLinkName {
248256
const decoder = new TextDecoder();
249257
// get meta data
250-
const meta: TarMeta = {
258+
const meta: TarMetaWithLinkName = {
251259
fileName: decoder.decode(trim(header.fileName)),
252260
};
253261
const fileNamePrefix = trim(header.fileNamePrefix);
254262
if (fileNamePrefix.byteLength > 0) {
255263
meta.fileName = decoder.decode(fileNamePrefix) + "/" + meta.fileName;
256264
}
257-
(["fileMode", "mtime", "uid", "gid"] as [
258-
"fileMode",
259-
"mtime",
260-
"uid",
261-
"gid",
262-
]).forEach((key) => {
265+
(
266+
["fileMode", "mtime", "uid", "gid"] as ["fileMode", "mtime", "uid", "gid"]
267+
).forEach((key) => {
263268
const arr = trim(header[key]);
264269
if (arr.byteLength > 0) {
265270
meta[key] = parseInt(decoder.decode(arr), 8);
@@ -277,6 +282,12 @@ export class Untar {
277282
meta.fileSize = parseInt(decoder.decode(header.fileSize), 8);
278283
meta.type = FileTypes[parseInt(meta.type!)] ?? meta.type;
279284

285+
// Only create the `linkName` property for symbolic links to minimize
286+
// the effect on existing code that only deals with non-links.
287+
if (meta.type === "symlink") {
288+
meta.linkName = decoder.decode(trim(header.linkName));
289+
}
290+
280291
return meta;
281292
}
282293

archive/untar_test.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import { assert, assertEquals, assertExists } from "../assert/mod.ts";
33
import { resolve } from "../path/mod.ts";
44
import { Tar, type TarMeta } from "./tar.ts";
5-
import { TarEntry, type TarHeader, Untar } from "./untar.ts";
5+
import {
6+
TarEntry,
7+
type TarHeader,
8+
TarMetaWithLinkName,
9+
Untar,
10+
} from "./untar.ts";
611
import { Buffer } from "../io/buffer.ts";
712
import { copy } from "../streams/copy.ts";
813
import { readAll } from "../streams/read_all.ts";
@@ -348,3 +353,54 @@ Deno.test({
348353
assertExists(tarEntry);
349354
},
350355
});
356+
357+
Deno.test("untarArchiveWithLink", async function () {
358+
const filePath = resolve(testdataDir, "with_link.tar");
359+
const file = await Deno.open(filePath, { read: true });
360+
361+
type ExpectedEntry = TarMetaWithLinkName & { content?: Uint8Array };
362+
363+
const expectedEntries: ExpectedEntry[] = [
364+
{
365+
fileName: "hello.txt",
366+
fileMode: 436,
367+
fileSize: 14,
368+
mtime: 1696384910,
369+
uid: 1000,
370+
gid: 1000,
371+
owner: "user",
372+
group: "user",
373+
type: "file",
374+
content: new TextEncoder().encode("Hello World!\n\n"),
375+
},
376+
{
377+
fileName: "link_to_hello.txt",
378+
linkName: "./hello.txt",
379+
fileMode: 511,
380+
fileSize: 0,
381+
mtime: 1696384945,
382+
uid: 1000,
383+
gid: 1000,
384+
owner: "user",
385+
group: "user",
386+
type: "symlink",
387+
},
388+
];
389+
390+
const untar = new Untar(file);
391+
392+
for await (const entry of untar) {
393+
const expected = expectedEntries.shift();
394+
assert(expected);
395+
const content = expected.content;
396+
delete expected.content;
397+
398+
assertEquals({ ...entry }, expected);
399+
400+
if (content) {
401+
assertEquals(content, await readAll(entry));
402+
}
403+
}
404+
405+
file.close();
406+
});

0 commit comments

Comments
 (0)