···
1
1
+
{
2
2
+
"editor.defaultFormatter": "denoland.vscode-deno"
3
3
+
}
···
1
1
+
Permission is hereby granted, free of charge, to any person obtaining a copy
2
2
+
of this software and associated documentation files (the "Software"), to deal
3
3
+
in the Software without restriction, including without limitation the rights
4
4
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5
5
+
copies of the Software, and to permit persons to whom the Software is
6
6
+
furnished to do so, subject to the following conditions:
7
7
+
8
8
+
The above copyright notice and this permission notice shall be included in all
9
9
+
copies or substantial portions of the Software.
10
10
+
11
11
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12
12
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13
13
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14
14
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15
15
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16
16
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
17
17
+
SOFTWARE.
···
1
1
+
# tar
2
2
+
3
3
+
Read and write tar archives.
4
4
+
5
5
+
```ts
6
6
+
// `writeTarEntry` is designed to be very simplistic, as the name suggests, it
7
7
+
// only spits out a buffer for one file entry. Good for streaming writes, but
8
8
+
// means you'd have to concatenate it yourself if you're doing it in one go.
9
9
+
10
10
+
const buffer = writeTarEntry({
11
11
+
name: 'README.md',
12
12
+
data: `Hello, **world**!`,
13
13
+
});
14
14
+
15
15
+
// `untar` expects a Reader interface, the package `@mary/reader` provides this
16
16
+
// by letting you convert a Uint8Array async iterable to one.
17
17
+
18
18
+
for await (const entry of untar(reader)) {
19
19
+
// If it's a file in the blobs directory...
20
20
+
if (entry.name.startsWith('blobs/')) {
21
21
+
// Read the contents...
22
22
+
const buffer = new Uint8Array(entry.size);
23
23
+
await entry.read(buffer);
24
24
+
}
25
25
+
}
26
26
+
```
···
1
1
+
{
2
2
+
"name": "@mary/tar",
3
3
+
"version": "0.1.0",
4
4
+
"exports": "./mod.ts",
5
5
+
"fmt": {
6
6
+
"useTabs": true,
7
7
+
"indentWidth": 2,
8
8
+
"lineWidth": 110,
9
9
+
"semiColons": true,
10
10
+
"singleQuote": true
11
11
+
},
12
12
+
"publish": {
13
13
+
"exclude": [".vscode/"]
14
14
+
}
15
15
+
}
···
1
1
+
export * from './tar.ts';
2
2
+
export * from './untar.ts';
···
1
1
+
import { getChecksum, RECORD_SIZE } from './utils.ts';
2
2
+
3
3
+
export interface TarFileAttributes {
4
4
+
/** @default 0o664 */
5
5
+
mode?: number;
6
6
+
/** @default 1000 */
7
7
+
uid?: number;
8
8
+
/** @default 1000 */
9
9
+
gid?: number;
10
10
+
/** @default Date.now() */
11
11
+
mtime?: number;
12
12
+
/** @default "" */
13
13
+
user?: string;
14
14
+
/** @default "" */
15
15
+
group?: string;
16
16
+
}
17
17
+
18
18
+
export interface TarFileEntry {
19
19
+
/** Entry name */
20
20
+
filename: string;
21
21
+
/** Entry data */
22
22
+
data: string | Uint8Array | ArrayBuffer;
23
23
+
/** Entry file attributes */
24
24
+
attrs?: TarFileAttributes;
25
25
+
}
26
26
+
27
27
+
const encoder = new TextEncoder();
28
28
+
29
29
+
const DEFAULT_ATTRS: TarFileAttributes = {};
30
30
+
31
31
+
/**
32
32
+
* Writes a single file entry in tar format, returns an array buffer which can
33
33
+
* then be concatenated or written to a stream.
34
34
+
*/
35
35
+
export function writeTarEntry(entry: TarFileEntry): ArrayBuffer {
36
36
+
const { filename, data, attrs = DEFAULT_ATTRS } = entry;
37
37
+
38
38
+
let name = filename;
39
39
+
let prefix = '';
40
40
+
41
41
+
if (name.length > 100) {
42
42
+
let i = 0;
43
43
+
while (name.length > 100) {
44
44
+
i = filename.indexOf('/', i);
45
45
+
46
46
+
if (i === -1) {
47
47
+
break;
48
48
+
}
49
49
+
50
50
+
prefix = filename.slice(0, i);
51
51
+
name = filename.slice(i + 1);
52
52
+
}
53
53
+
54
54
+
if (name.length > 100 || prefix.length > 155) {
55
55
+
const total_length = (prefix.length && prefix.length + 1) + name.length;
56
56
+
throw new Error(`filename is too long (${total_length})`);
57
57
+
}
58
58
+
}
59
59
+
60
60
+
const data_buf = normalizeData(data);
61
61
+
const data_size = data_buf.byteLength;
62
62
+
63
63
+
const padding_size = RECORD_SIZE - (data_size % RECORD_SIZE || RECORD_SIZE);
64
64
+
65
65
+
const buf = new ArrayBuffer(512 + data_size + padding_size);
66
66
+
67
67
+
// File name
68
68
+
writeString(buf, name, 0, 100);
69
69
+
70
70
+
// File mode
71
71
+
writeString(buf, formatOctal(attrs.mode ?? 0o664, 7), 100, 8);
72
72
+
73
73
+
// UID
74
74
+
writeString(buf, formatOctal(attrs.uid ?? 1000, 7), 108, 8);
75
75
+
76
76
+
// GID
77
77
+
writeString(buf, formatOctal(attrs.gid ?? 1000, 7), 116, 8);
78
78
+
79
79
+
// File size
80
80
+
writeString(buf, formatOctal(data_size, 11), 124, 12);
81
81
+
82
82
+
// Modified time
83
83
+
writeString(buf, formatOctal(attrs.mtime ?? Date.now(), 11), 136, 12);
84
84
+
85
85
+
// File type
86
86
+
writeString(buf, '0', 156, 12);
87
87
+
88
88
+
// Ustar
89
89
+
writeString(buf, 'ustar00', 257, 8);
90
90
+
91
91
+
// User ownership
92
92
+
writeString(buf, attrs.user ?? '', 265, 32);
93
93
+
94
94
+
// User group
95
95
+
writeString(buf, attrs.group ?? '', 297, 32);
96
96
+
97
97
+
// File prefix
98
98
+
writeString(buf, prefix, 345, 155);
99
99
+
100
100
+
// Checksum
101
101
+
{
102
102
+
const header = new Uint8Array(buf, 0, 512);
103
103
+
const chksum = getChecksum(header);
104
104
+
105
105
+
writeString(buf, formatOctal(chksum, 8), 148, 8);
106
106
+
}
107
107
+
108
108
+
// Actual data
109
109
+
{
110
110
+
const dest = new Uint8Array(buf, 512, data_size);
111
111
+
dest.set(data_buf, 0);
112
112
+
}
113
113
+
114
114
+
return buf;
115
115
+
}
116
116
+
117
117
+
function normalizeData(data: string | ArrayBuffer | Uint8Array): Uint8Array {
118
118
+
if (typeof data === 'string') {
119
119
+
return encoder.encode(data);
120
120
+
}
121
121
+
122
122
+
if (data instanceof ArrayBuffer) {
123
123
+
return new Uint8Array(data);
124
124
+
}
125
125
+
126
126
+
return data;
127
127
+
}
128
128
+
129
129
+
function writeString(buf: ArrayBuffer, str: string, offset: number, size: number) {
130
130
+
const view = new Uint8Array(buf, offset, size);
131
131
+
encoder.encodeInto(str, view);
132
132
+
}
133
133
+
134
134
+
function formatOctal(input: number, length: number) {
135
135
+
return input.toString(8).padStart(length, '0');
136
136
+
}
···
1
1
+
import { getChecksum, INITIAL_CHKSUM, RECORD_SIZE } from './utils.ts';
2
2
+
3
3
+
/**
4
4
+
* Provides the ability to read variably-sized buffers
5
5
+
*/
6
6
+
export interface Reader {
7
7
+
/**
8
8
+
* Reads up to `p.byteLength` into `p`
9
9
+
*/
10
10
+
read(p: Uint8Array): Promise<number | null>;
11
11
+
/**
12
12
+
* Skip reading `n` amount of bytes
13
13
+
*/
14
14
+
seek(n: number): Promise<number>;
15
15
+
}
16
16
+
17
17
+
const decoder = new TextDecoder();
18
18
+
19
19
+
const FILE_TYPES: Record<number, string> = {
20
20
+
0: 'file',
21
21
+
1: 'link',
22
22
+
2: 'symlink',
23
23
+
3: 'character_device',
24
24
+
4: 'block_device',
25
25
+
5: 'directory',
26
26
+
6: 'fifo',
27
27
+
7: 'contiguous_file',
28
28
+
};
29
29
+
30
30
+
export async function* untar(reader: Reader): AsyncGenerator<TarEntry> {
31
31
+
const header = new Uint8Array(512);
32
32
+
33
33
+
let entry: TarEntry | undefined;
34
34
+
35
35
+
while (true) {
36
36
+
if (entry) {
37
37
+
await entry.discard();
38
38
+
}
39
39
+
40
40
+
const res = await reader.read(header);
41
41
+
42
42
+
if (res === null) {
43
43
+
break;
44
44
+
}
45
45
+
46
46
+
// validate checksum
47
47
+
{
48
48
+
const expected = readOctal(header, 148, 8);
49
49
+
const actual = getChecksum(header);
50
50
+
if (expected !== actual) {
51
51
+
if (actual === INITIAL_CHKSUM) {
52
52
+
break;
53
53
+
}
54
54
+
55
55
+
throw new Error(`invalid checksum, expected ${expected} got ${actual}`);
56
56
+
}
57
57
+
}
58
58
+
59
59
+
// validate magic
60
60
+
{
61
61
+
const magic = readString(header, 257, 8);
62
62
+
if (!magic.startsWith('ustar')) {
63
63
+
throw new Error(`unsupported archive format: ${magic}`);
64
64
+
}
65
65
+
}
66
66
+
67
67
+
entry = new TarEntry(header, reader);
68
68
+
yield entry;
69
69
+
}
70
70
+
}
71
71
+
72
72
+
class TarEntry {
73
73
+
#reader: Reader;
74
74
+
#read: number = 0;
75
75
+
76
76
+
readonly name: string;
77
77
+
readonly mode: number;
78
78
+
readonly uid: number;
79
79
+
readonly gid: number;
80
80
+
readonly size: number;
81
81
+
readonly mtime: number;
82
82
+
readonly type: string;
83
83
+
readonly linkName: string;
84
84
+
readonly owner: string;
85
85
+
readonly group: string;
86
86
+
readonly entrySize: number;
87
87
+
88
88
+
constructor(header: Uint8Array, reader: Reader) {
89
89
+
const name = readString(header, 0, 100);
90
90
+
const mode = readOctal(header, 100, 8);
91
91
+
const uid = readOctal(header, 108, 8);
92
92
+
const gid = readOctal(header, 116, 8);
93
93
+
const size = readOctal(header, 124, 12);
94
94
+
const mtime = readOctal(header, 136, 12);
95
95
+
const type = readOctal(header, 156, 1);
96
96
+
const link_name = readString(header, 157, 100);
97
97
+
const owner = readString(header, 265, 32);
98
98
+
const group = readString(header, 297, 32);
99
99
+
const prefix = readString(header, 345, 155);
100
100
+
101
101
+
this.name = prefix.length > 0 ? prefix + '/' + name : name;
102
102
+
this.mode = mode;
103
103
+
this.uid = uid;
104
104
+
this.gid = gid;
105
105
+
this.size = size;
106
106
+
this.mtime = mtime;
107
107
+
this.type = FILE_TYPES[type] ?? '' + type;
108
108
+
this.linkName = link_name;
109
109
+
this.owner = owner;
110
110
+
this.group = group;
111
111
+
this.entrySize = Math.ceil(this.size / RECORD_SIZE) * RECORD_SIZE;
112
112
+
113
113
+
this.#reader = reader;
114
114
+
}
115
115
+
116
116
+
async read(p: Uint8Array): Promise<number | null> {
117
117
+
const remaining = this.size - this.#read;
118
118
+
119
119
+
if (remaining <= 0) {
120
120
+
return null;
121
121
+
}
122
122
+
123
123
+
if (p.byteLength <= remaining) {
124
124
+
this.#read += p.byteLength;
125
125
+
return await this.#reader.read(p);
126
126
+
}
127
127
+
128
128
+
// User exceeded the remaining size of this entry, we can't fulfill that
129
129
+
// directly because it means reading partially into the next entry
130
130
+
this.#read += remaining;
131
131
+
132
132
+
const block = new Uint8Array(remaining);
133
133
+
const n = await this.#reader.read(block);
134
134
+
135
135
+
p.set(block, 0);
136
136
+
return n;
137
137
+
}
138
138
+
139
139
+
async discard(): Promise<void> {
140
140
+
const remaining = this.entrySize - this.#read;
141
141
+
142
142
+
if (remaining <= 0) {
143
143
+
return;
144
144
+
}
145
145
+
146
146
+
this.#read += remaining;
147
147
+
148
148
+
await this.#reader.seek(remaining);
149
149
+
}
150
150
+
}
151
151
+
152
152
+
function readString(arr: Uint8Array, offset: number, size: number): string {
153
153
+
let input = arr.subarray(offset, offset + size);
154
154
+
155
155
+
for (let idx = 0, len = input.length; idx < len; idx++) {
156
156
+
const code = input[idx];
157
157
+
158
158
+
if (code === 0) {
159
159
+
input = input.subarray(0, idx);
160
160
+
break;
161
161
+
}
162
162
+
}
163
163
+
164
164
+
return decoder.decode(input);
165
165
+
}
166
166
+
167
167
+
function readOctal(arr: Uint8Array, offset: number, size: number): number {
168
168
+
const res = readString(arr, offset, size);
169
169
+
return res ? parseInt(res, 8) : 0;
170
170
+
}
···
1
1
+
/** Initial checksum value, includes the 8 bytes in the checksum field itself */
2
2
+
export const INITIAL_CHKSUM = 8 ** 32;
3
3
+
export const RECORD_SIZE = 512;
4
4
+
5
5
+
/** Calculates the checksum from the first 512 bytes of a buffer */
6
6
+
export function getChecksum(buf: Uint8Array): number {
7
7
+
let checksum = INITIAL_CHKSUM;
8
8
+
9
9
+
for (let i = 0; i < RECORD_SIZE; i++) {
10
10
+
// Ignore own checksum field
11
11
+
if (i >= 148 && i < 156) {
12
12
+
continue;
13
13
+
}
14
14
+
15
15
+
checksum += buf[i];
16
16
+
}
17
17
+
18
18
+
return checksum;
19
19
+
}