Skip to content

Commit e785b18

Browse files
committed
Add tests
1 parent 9a7c981 commit e785b18

File tree

7 files changed

+393
-22
lines changed

7 files changed

+393
-22
lines changed

site/jest.setup.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ import { cleanup } from "@testing-library/react"
33
import crypto from "crypto"
44
import { server } from "./src/testHelpers/server"
55
import "jest-location-mock"
6+
import { TextEncoder, TextDecoder } from "util"
7+
import { Blob } from "buffer"
8+
9+
global.TextEncoder = TextEncoder
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
11+
global.TextDecoder = TextDecoder as any
12+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
13+
global.Blob = Blob as any
614

715
// Polyfill the getRandomValues that is used on utils/random.ts
816
Object.defineProperty(global.self, "crypto", {

site/js-untar.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ declare module "js-untar" {
66
gid: number
77
uid: number
88
mtime: number
9+
gname: string
10+
uname: string
911
type: "0" | "1" | "2" | "3" | "4" | "5" //https://en.wikipedia.org/wiki/Tar_(computing) on Type flag field
1012
}
1113

site/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
"remark-gfm": "3.0.1",
7979
"rollup-plugin-visualizer": "5.9.0",
8080
"sourcemapped-stacktrace": "1.1.11",
81-
"tar-js": "^0.3.0",
8281
"ts-prune": "0.10.3",
8382
"tzdata": "1.0.30",
8483
"ua-parser-js": "1.0.33",

site/src/util/tar.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { TarReader, TarWriter, ITarFileInfo } from "./tar"
2+
3+
const mtime = 1666666666666
4+
5+
test("tar", async () => {
6+
// Write
7+
const writer = new TarWriter()
8+
writer.addFile("a.txt", "hello", { mtime })
9+
writer.addFile("b.txt", new Blob(["world"]), { mtime })
10+
writer.addFile("c.txt", "", { mtime })
11+
const blob = await writer.write()
12+
13+
// Read
14+
const reader = new TarReader()
15+
const fileInfos = await reader.readFile(blob)
16+
verifyFile(fileInfos[0], reader.getTextFile(fileInfos[0].name) as string, {
17+
name: "a.txt",
18+
content: "hello",
19+
})
20+
verifyFile(fileInfos[1], reader.getTextFile(fileInfos[1].name) as string, {
21+
name: "b.txt",
22+
content: "world",
23+
})
24+
verifyFile(fileInfos[2], reader.getTextFile(fileInfos[2].name) as string, {
25+
name: "c.txt",
26+
content: "",
27+
})
28+
})
29+
30+
function verifyFile(
31+
info: ITarFileInfo,
32+
content: string,
33+
expected: { name: string; content: string },
34+
) {
35+
expect(info.name).toEqual(expected.name)
36+
expect(info.size).toEqual(expected.content.length)
37+
expect(content).toEqual(expected.content)
38+
}

site/src/util/tar.ts

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
// Based on https://github.com/gera2ld/tarjs
2+
// and https://github.com/ankitrohatgi/tarballjs/blob/master/tarball.js
3+
export enum TarFileType {
4+
File = "0",
5+
Dir = "5",
6+
}
7+
const encoder = new TextEncoder()
8+
const utf8Encode = (input: string) => encoder.encode(input)
9+
const decoder = new TextDecoder()
10+
const utf8Decode = (input: Uint8Array) => decoder.decode(input)
11+
12+
export interface ITarFileInfo {
13+
name: string
14+
type: TarFileType
15+
size: number
16+
headerOffset: number
17+
}
18+
19+
export interface ITarWriteItem {
20+
name: string
21+
type: TarFileType
22+
data: ArrayBuffer | Promise<ArrayBuffer> | null
23+
size: number
24+
opts?: Partial<ITarWriteOptions>
25+
}
26+
27+
export interface ITarWriteOptions {
28+
uid: number
29+
gid: number
30+
mode: number
31+
mtime: number
32+
user: string
33+
group: string
34+
}
35+
36+
export class TarReader {
37+
private fileInfo: ITarFileInfo[] = []
38+
private _buffer: ArrayBuffer | null = null
39+
40+
constructor() {
41+
this.reset()
42+
}
43+
44+
get buffer() {
45+
if (!this._buffer) {
46+
throw new Error("Buffer is not set")
47+
}
48+
49+
return this._buffer
50+
}
51+
52+
reset() {
53+
this.fileInfo = []
54+
this._buffer = null
55+
}
56+
57+
async readFile(file: ArrayBuffer | Uint8Array | Blob) {
58+
this.reset()
59+
this._buffer = await getArrayBuffer(file)
60+
this.readFileInfo()
61+
return this.fileInfo
62+
}
63+
64+
private readFileInfo() {
65+
this.fileInfo = []
66+
let offset = 0
67+
let fileSize = 0
68+
let fileName = ""
69+
let fileType: TarFileType
70+
while (offset < this.buffer.byteLength - 512) {
71+
fileName = this.readFileName(offset)
72+
if (!fileName) {
73+
break
74+
}
75+
fileType = this.readFileType(offset)
76+
fileSize = this.readFileSize(offset)
77+
78+
this.fileInfo.push({
79+
name: fileName,
80+
type: fileType,
81+
size: fileSize,
82+
headerOffset: offset,
83+
})
84+
85+
offset += 512 + 512 * Math.floor((fileSize + 511) / 512)
86+
}
87+
}
88+
89+
private readString(offset: number, maxSize: number) {
90+
let size = 0
91+
let view = new Uint8Array(this.buffer, offset, maxSize)
92+
while (size < maxSize && view[size]) {
93+
size += 1
94+
}
95+
view = new Uint8Array(this.buffer, offset, size)
96+
return utf8Decode(view)
97+
}
98+
99+
private readFileName(offset: number) {
100+
return this.readString(offset, 100)
101+
}
102+
103+
private readFileType(offset: number) {
104+
const typeView = new Uint8Array(this.buffer, offset + 156, 1)
105+
const typeStr = String.fromCharCode(typeView[0])
106+
if (typeStr === "0") {
107+
return TarFileType.File
108+
} else if (typeStr === "5") {
109+
return TarFileType.Dir
110+
} else {
111+
throw new Error("No supported file type")
112+
}
113+
}
114+
115+
private readFileSize(offset: number) {
116+
// offset = 124, length = 12
117+
const view = new Uint8Array(this.buffer, offset + 124, 12)
118+
const sizeStr = utf8Decode(view)
119+
return parseInt(sizeStr, 8)
120+
}
121+
122+
private readFileBlob(offset: number, size: number, mimetype: string) {
123+
const view = new Uint8Array(this.buffer, offset, size)
124+
return new Blob([view], { type: mimetype })
125+
}
126+
127+
private readTextFile(offset: number, size: number) {
128+
const view = new Uint8Array(this.buffer, offset, size)
129+
return utf8Decode(view)
130+
}
131+
132+
getTextFile(filename: string) {
133+
const item = this.fileInfo.find((info) => info.name === filename)
134+
if (item) {
135+
return this.readTextFile(item.headerOffset + 512, item.size)
136+
}
137+
}
138+
139+
getFileBlob(filename: string, mimetype = "") {
140+
const item = this.fileInfo.find((info) => info.name === filename)
141+
if (item) {
142+
return this.readFileBlob(item.headerOffset + 512, item.size, mimetype)
143+
}
144+
}
145+
}
146+
147+
export class TarWriter {
148+
private fileData: ITarWriteItem[] = []
149+
private _buffer: ArrayBuffer | null = null
150+
151+
get buffer() {
152+
if (!this._buffer) {
153+
throw new Error("Buffer is not set")
154+
}
155+
return this._buffer
156+
}
157+
158+
addFile(
159+
name: string,
160+
file: string | ArrayBuffer | Uint8Array | Blob,
161+
opts?: Partial<ITarWriteOptions>,
162+
) {
163+
const data = getArrayBuffer(file)
164+
const size = (data as ArrayBuffer).byteLength ?? (file as Blob).size
165+
const item: ITarWriteItem = {
166+
name,
167+
type: TarFileType.File,
168+
data,
169+
size,
170+
opts,
171+
}
172+
this.fileData.push(item)
173+
}
174+
175+
addFolder(name: string, opts?: Partial<ITarWriteOptions>) {
176+
this.fileData.push({
177+
name,
178+
type: TarFileType.Dir,
179+
data: null,
180+
size: 0,
181+
opts,
182+
})
183+
}
184+
185+
private createBuffer() {
186+
const dataSize = this.fileData.reduce(
187+
(prev, item) => prev + 512 + 512 * Math.floor((item.size + 511) / 512),
188+
0,
189+
)
190+
const bufSize = 10240 * Math.floor((dataSize + 10240 - 1) / 10240)
191+
this._buffer = new ArrayBuffer(bufSize)
192+
}
193+
194+
async write() {
195+
this.createBuffer()
196+
const view = new Uint8Array(this.buffer)
197+
let offset = 0
198+
for (const item of this.fileData) {
199+
// write header
200+
this.writeFileName(item.name, offset)
201+
this.writeFileType(item.type, offset)
202+
this.writeFileSize(item.size, offset)
203+
this.fillHeader(offset, item.opts as Partial<ITarWriteOptions>, item.type)
204+
this.writeChecksum(offset)
205+
206+
// write data
207+
const data = new Uint8Array((await item.data) as ArrayBuffer)
208+
view.set(data, offset + 512)
209+
offset += 512 + 512 * Math.floor((item.size + 511) / 512)
210+
}
211+
return new Blob([this.buffer], { type: "application/x-tar" })
212+
}
213+
214+
private writeString(str: string, offset: number, size: number) {
215+
const strView = utf8Encode(str)
216+
const view = new Uint8Array(this.buffer, offset, size)
217+
for (let i = 0; i < size; i += 1) {
218+
view[i] = i < strView.length ? strView[i] : 0
219+
}
220+
}
221+
222+
private writeFileName(name: string, offset: number) {
223+
// offset: 0
224+
this.writeString(name, offset, 100)
225+
}
226+
227+
private writeFileType(type: TarFileType, offset: number) {
228+
// offset: 156
229+
const typeView = new Uint8Array(this.buffer, offset + 156, 1)
230+
typeView[0] = type.charCodeAt(0)
231+
}
232+
233+
private writeFileSize(size: number, offset: number) {
234+
// offset: 124
235+
const sizeStr = size.toString(8).padStart(11, "0")
236+
this.writeString(sizeStr, offset + 124, 12)
237+
}
238+
239+
private writeFileMode(mode: number, offset: number) {
240+
// offset: 100
241+
this.writeString(mode.toString(8).padStart(7, "0"), offset + 100, 8)
242+
}
243+
244+
private writeFileUid(uid: number, offset: number) {
245+
// offset: 108
246+
this.writeString(uid.toString(8).padStart(7, "0"), offset + 108, 8)
247+
}
248+
249+
private writeFileGid(gid: number, offset: number) {
250+
// offset: 116
251+
this.writeString(gid.toString(8).padStart(7, "0"), offset + 116, 8)
252+
}
253+
254+
private writeFileMtime(mtime: number, offset: number) {
255+
// offset: 136
256+
this.writeString(mtime.toString(8).padStart(11, "0"), offset + 136, 12)
257+
}
258+
259+
private writeFileUser(user: string, offset: number) {
260+
// offset: 265
261+
this.writeString(user, offset + 265, 32)
262+
}
263+
264+
private writeFileGroup(group: string, offset: number) {
265+
// offset: 297
266+
this.writeString(group, offset + 297, 32)
267+
}
268+
269+
private writeChecksum(offset: number) {
270+
// offset: 148
271+
this.writeString(" ", offset + 148, 8) // first fill with spaces
272+
273+
// add up header bytes
274+
const header = new Uint8Array(this.buffer, offset, 512)
275+
let chksum = 0
276+
for (let i = 0; i < 512; i += 1) {
277+
chksum += header[i]
278+
}
279+
this.writeString(chksum.toString(8), offset + 148, 8)
280+
}
281+
282+
private fillHeader(
283+
offset: number,
284+
opts: Partial<ITarWriteOptions>,
285+
fileType: TarFileType,
286+
) {
287+
const { uid, gid, mode, mtime, user, group } = {
288+
uid: 1000,
289+
gid: 1000,
290+
mode: fileType === TarFileType.File ? 0o664 : 0o775,
291+
mtime: ~~(Date.now() / 1000),
292+
user: "tarballjs",
293+
group: "tarballjs",
294+
...opts,
295+
}
296+
297+
this.writeFileMode(mode, offset)
298+
this.writeFileUid(uid, offset)
299+
this.writeFileGid(gid, offset)
300+
this.writeFileMtime(mtime, offset)
301+
302+
this.writeString("ustar", offset + 257, 6) // magic string
303+
this.writeString("00", offset + 263, 2) // magic version
304+
305+
this.writeFileUser(user, offset)
306+
this.writeFileGroup(group, offset)
307+
}
308+
}
309+
310+
function getArrayBuffer(file: string | ArrayBuffer | Uint8Array | Blob) {
311+
if (typeof file === "string") {
312+
return utf8Encode(file).buffer
313+
}
314+
if (file instanceof ArrayBuffer) {
315+
return file
316+
}
317+
if (ArrayBuffer.isView(file)) {
318+
return new Uint8Array(file).buffer
319+
}
320+
return file.arrayBuffer()
321+
}

0 commit comments

Comments
 (0)