Skip to content

Commit c1fd659

Browse files
committed
feat: added support for extended metadata
1 parent 76fe731 commit c1fd659

11 files changed

+935
-348
lines changed

src/Generator.ts

+170-70
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,49 @@
11
import type { FileStat } from './types';
2-
import { EntryType, HeaderSize, HeaderOffset } from './types';
2+
import { GeneratorState, EntryType, HeaderSize, HeaderOffset } from './types';
33
import * as errors from './errors';
44
import * as utils from './utils';
55
import * as constants from './constants';
66

7-
function generateHeader(
8-
filePath: string,
9-
type: EntryType,
10-
stat: FileStat,
11-
): Uint8Array {
12-
// TODO: implement long-file-name headers
13-
if (filePath.length < 1 || filePath.length > 255) {
14-
throw new errors.ErrorTarGeneratorInvalidFileName(
15-
'The file name must be longer than 1 character and shorter than 255 characters',
7+
function generateHeader(filePath: string, type: EntryType, stat: FileStat) {
8+
if (filePath.length > 255) {
9+
throw new errors.ErrorVirtualTarGeneratorInvalidFileName(
10+
'The file name must shorter than 255 characters',
1611
);
1712
}
1813

19-
// As the size does not matter for directories, it can be undefined. However,
20-
// if the header is being generated for a file, then it needs to have a valid
21-
// size.
22-
if (stat.size == null && type === EntryType.FILE) {
23-
throw new errors.ErrorTarGeneratorInvalidStat('Size must be set for files');
24-
}
25-
const size = type === EntryType.FILE ? stat.size : 0;
26-
2714
// The time can be undefined, which would be referring to epoch 0.
2815
const time = utils.dateToUnixTime(stat.mtime ?? new Date());
2916

3017
const header = new Uint8Array(constants.BLOCK_SIZE);
3118

32-
// The TAR headers follow this structure
33-
// Start Size Description
34-
// ------------------------------
35-
// 0 100 File name (first 100 bytes)
36-
// 100 8 File permissions (null-padded octal)
37-
// 108 8 Owner UID (null-padded octal)
38-
// 116 8 Owner GID (null-padded octal)
39-
// 124 12 File size (null-padded octal, 0 for directories)
40-
// 136 12 Mtime (null-padded octal)
41-
// 148 8 Checksum (fill with ASCII spaces for computation)
42-
// 156 1 Type flag ('0' for file, '5' for directory)
43-
// 157 100 File owner name (null-terminated ASCII/UTF-8)
44-
// 257 6 'ustar\0' (magic string)
45-
// 263 2 '00' (ustar version)
46-
// 265 32 Owner user name (null-terminated ASCII/UTF-8)
47-
// 297 32 Owner group name (null-terminated ASCII/UTF-8)
48-
// 329 8 Device major (unset in this implementation)
49-
// 337 8 Device minor (unset in this implementation)
50-
// 345 155 File name (last 155 bytes, total 255 bytes, null-padded)
51-
// 500 12 '\0' (unused)
52-
//
53-
// Note that all numbers are in stringified octal format.
54-
55-
// The first half of the file name (upto 100 bytes) is stored here.
56-
utils.writeBytesToArray(
57-
header,
58-
utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME),
59-
HeaderOffset.FILE_NAME,
60-
HeaderSize.FILE_NAME,
61-
);
19+
// If the length of the file path is less than 100 bytes, then we write it to
20+
// the file name. Otherwise, we write it into the file name prefix and append
21+
// file name to it.
22+
if (filePath.length < HeaderSize.FILE_NAME) {
23+
utils.writeBytesToArray(
24+
header,
25+
utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME),
26+
HeaderOffset.FILE_NAME,
27+
HeaderSize.FILE_NAME,
28+
);
29+
} else {
30+
utils.writeBytesToArray(
31+
header,
32+
utils.splitFileName(
33+
filePath,
34+
HeaderSize.FILE_NAME,
35+
HeaderSize.FILE_NAME_PREFIX,
36+
),
37+
HeaderOffset.FILE_NAME,
38+
HeaderSize.FILE_NAME,
39+
);
40+
utils.writeBytesToArray(
41+
header,
42+
utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME),
43+
HeaderOffset.FILE_NAME_PREFIX,
44+
HeaderSize.FILE_NAME_PREFIX,
45+
);
46+
}
6247

6348
// The file permissions, or the mode, is stored in the next chunk. This is
6449
// stored in an octal number format.
@@ -89,7 +74,7 @@ function generateHeader(
8974
// directories, and it must be set for files.
9075
utils.writeBytesToArray(
9176
header,
92-
utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'),
77+
utils.pad(stat.size ?? '', HeaderSize.FILE_SIZE, '0', '\0'),
9378
HeaderOffset.FILE_SIZE,
9479
HeaderSize.FILE_SIZE,
9580
);
@@ -115,7 +100,7 @@ function generateHeader(
115100
HeaderSize.TYPE_FLAG,
116101
);
117102

118-
// File owner name will be null, as regular stat-ing cannot extract that
103+
// Link name will be null, as regular stat-ing cannot extract that
119104
// information.
120105

121106
// This value is the USTAR magic string which makes this file appear as
@@ -147,19 +132,6 @@ function generateHeader(
147132
// Device minor will be null, as this specific to linux kernel knowing what
148133
// drivers to use for executing certain files, and is irrelevant here.
149134

150-
// The second half of the file name is entered here. This chunk handles file
151-
// names ranging 100 to 255 characters.
152-
utils.writeBytesToArray(
153-
header,
154-
utils.splitFileName(
155-
filePath,
156-
HeaderSize.FILE_NAME,
157-
HeaderSize.FILE_NAME_EXTRA,
158-
),
159-
HeaderOffset.FILE_NAME_EXTRA,
160-
HeaderSize.FILE_NAME_EXTRA,
161-
);
162-
163135
// Updating with the new checksum
164136
const checksum = utils.calculateChecksum(header);
165137

@@ -168,19 +140,147 @@ function generateHeader(
168140
// instead of null, which is why it is used like this here.
169141
utils.writeBytesToArray(
170142
header,
171-
utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '),
143+
utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0'),
172144
HeaderOffset.CHECKSUM,
173145
HeaderSize.CHECKSUM,
174146
);
175147

176148
return header;
177149
}
178150

179-
// Creates a single null block. A null block is a block filled with all zeros.
180-
// This is needed to end the archive, as two of these blocks mark the end of
181-
// archive.
182-
function generateNullChunk() {
183-
return new Uint8Array(constants.BLOCK_SIZE);
151+
/**
152+
* The TAR headers follow this structure
153+
* Start Size Description
154+
* ------------------------------
155+
* 0 100 File name (first 100 bytes)
156+
* 100 8 File mode (null-padded octal)
157+
* 108 8 Owner user id (null-padded octal)
158+
* 116 8 Owner group id (null-padded octal)
159+
* 124 12 File size in bytes (null-padded octal, 0 for directories)
160+
* 136 12 Mtime (null-padded octal)
161+
* 148 8 Checksum (fill with ASCII spaces for computation)
162+
* 156 1 Type flag ('0' for file, '5' for directory)
163+
* 157 100 Link name (null-terminated ASCII/UTF-8)
164+
* 257 6 'ustar\0' (magic string)
165+
* 263 2 '00' (ustar version)
166+
* 265 32 Owner user name (null-terminated ASCII/UTF-8)
167+
* 297 32 Owner group name (null-terminated ASCII/UTF-8)
168+
* 329 8 Device major (unset in this implementation)
169+
* 337 8 Device minor (unset in this implementation)
170+
* 345 155 File name (last 155 bytes, total 255 bytes, null-padded)
171+
* 500 12 '\0' (unused)
172+
*
173+
* Note that all numbers are in stringified octal format.
174+
*/
175+
class Generator {
176+
protected state: GeneratorState = GeneratorState.READY;
177+
protected remainingBytes = 0;
178+
179+
generateFile(filePath: string, stat: FileStat): Uint8Array {
180+
if (this.state === GeneratorState.READY) {
181+
// Make sure the size is valid
182+
if (stat.size == null) {
183+
throw new errors.ErrorVirtualTarGeneratorInvalidStat(
184+
'Files should have valid file sizes',
185+
);
186+
}
187+
188+
const generatedBlock = generateHeader(filePath, EntryType.FILE, stat);
189+
190+
// If no data is in the file, then there is no need of a data block. It
191+
// will remain as READY.
192+
if (stat.size !== 0) this.state = GeneratorState.DATA;
193+
this.remainingBytes = stat.size;
194+
195+
return generatedBlock;
196+
}
197+
throw new errors.ErrorVirtualTarGeneratorInvalidState(
198+
`Expected state ${GeneratorState[GeneratorState.READY]} but got ${
199+
GeneratorState[this.state]
200+
}`,
201+
);
202+
}
203+
204+
generateDirectory(filePath: string, stat: FileStat): Uint8Array {
205+
if (this.state === GeneratorState.READY) {
206+
const directoryStat: FileStat = {
207+
size: 0,
208+
mode: stat.mode,
209+
mtime: stat.mtime,
210+
uid: stat.uid,
211+
gid: stat.gid,
212+
};
213+
return generateHeader(filePath, EntryType.DIRECTORY, directoryStat);
214+
}
215+
throw new errors.ErrorVirtualTarGeneratorInvalidState(
216+
`Expected state ${GeneratorState[GeneratorState.READY]} but got ${
217+
GeneratorState[this.state]
218+
}`,
219+
);
220+
}
221+
222+
generateExtended(size: number): Uint8Array {
223+
if (this.state === GeneratorState.READY) {
224+
this.state = GeneratorState.DATA;
225+
this.remainingBytes = size;
226+
return generateHeader('', EntryType.EXTENDED, { size });
227+
}
228+
throw new errors.ErrorVirtualTarGeneratorInvalidState(
229+
`Expected state ${GeneratorState[GeneratorState.READY]} but got ${
230+
GeneratorState[this.state]
231+
}`,
232+
);
233+
}
234+
235+
generateData(data: Uint8Array): Uint8Array {
236+
if (data.byteLength > constants.BLOCK_SIZE) {
237+
throw new errors.ErrorVirtualTarGeneratorBlockSize(
238+
`Expected data to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`,
239+
);
240+
}
241+
242+
if (this.state === GeneratorState.DATA) {
243+
if (this.remainingBytes >= constants.BLOCK_SIZE) {
244+
this.remainingBytes -= constants.BLOCK_SIZE;
245+
if (this.remainingBytes === 0) this.state = GeneratorState.READY;
246+
return data;
247+
} else {
248+
// Update state
249+
this.remainingBytes = 0;
250+
this.state = GeneratorState.READY;
251+
252+
// Pad the remaining data with nulls
253+
const paddedData = new Uint8Array(constants.BLOCK_SIZE);
254+
paddedData.set(data, 0);
255+
return paddedData;
256+
}
257+
}
258+
259+
throw new errors.ErrorVirtualTarGeneratorInvalidState(
260+
`Expected state ${GeneratorState[GeneratorState.DATA]} but got ${
261+
GeneratorState[this.state]
262+
}`,
263+
);
264+
}
265+
266+
// Creates a single null block. A null block is a block filled with all zeros.
267+
// This is needed to end the archive, as two of these blocks mark the end of
268+
// archive.
269+
generateEnd() {
270+
switch (this.state) {
271+
case GeneratorState.READY:
272+
this.state = GeneratorState.NULL;
273+
break;
274+
case GeneratorState.NULL:
275+
this.state = GeneratorState.ENDED;
276+
break;
277+
default:
278+
throw new errors.ErrorVirtualTarGeneratorEndOfArchive(
279+
'Exactly two null chunks should be generated consecutively to end archive',
280+
);
281+
}
282+
return new Uint8Array(constants.BLOCK_SIZE);
283+
}
184284
}
185285

186-
export { generateHeader, generateNullChunk };
286+
export default Generator;

0 commit comments

Comments
 (0)