1
1
import type { FileStat } from './types' ;
2
- import { EntryType , HeaderSize , HeaderOffset } from './types' ;
2
+ import { GeneratorState , EntryType , HeaderSize , HeaderOffset } from './types' ;
3
3
import * as errors from './errors' ;
4
4
import * as utils from './utils' ;
5
5
import * as constants from './constants' ;
6
6
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' ,
16
11
) ;
17
12
}
18
13
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
-
27
14
// The time can be undefined, which would be referring to epoch 0.
28
15
const time = utils . dateToUnixTime ( stat . mtime ?? new Date ( ) ) ;
29
16
30
17
const header = new Uint8Array ( constants . BLOCK_SIZE ) ;
31
18
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
+ }
62
47
63
48
// The file permissions, or the mode, is stored in the next chunk. This is
64
49
// stored in an octal number format.
@@ -89,7 +74,7 @@ function generateHeader(
89
74
// directories, and it must be set for files.
90
75
utils . writeBytesToArray (
91
76
header ,
92
- utils . pad ( size ?? '' , HeaderSize . FILE_SIZE , '0' , '\0' ) ,
77
+ utils . pad ( stat . size ?? '' , HeaderSize . FILE_SIZE , '0' , '\0' ) ,
93
78
HeaderOffset . FILE_SIZE ,
94
79
HeaderSize . FILE_SIZE ,
95
80
) ;
@@ -115,7 +100,7 @@ function generateHeader(
115
100
HeaderSize . TYPE_FLAG ,
116
101
) ;
117
102
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
119
104
// information.
120
105
121
106
// This value is the USTAR magic string which makes this file appear as
@@ -147,19 +132,6 @@ function generateHeader(
147
132
// Device minor will be null, as this specific to linux kernel knowing what
148
133
// drivers to use for executing certain files, and is irrelevant here.
149
134
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
-
163
135
// Updating with the new checksum
164
136
const checksum = utils . calculateChecksum ( header ) ;
165
137
@@ -168,19 +140,147 @@ function generateHeader(
168
140
// instead of null, which is why it is used like this here.
169
141
utils . writeBytesToArray (
170
142
header ,
171
- utils . pad ( checksum , HeaderSize . CHECKSUM , '0' , '\0 ' ) ,
143
+ utils . pad ( checksum , HeaderSize . CHECKSUM , '0' , '\0' ) ,
172
144
HeaderOffset . CHECKSUM ,
173
145
HeaderSize . CHECKSUM ,
174
146
) ;
175
147
176
148
return header ;
177
149
}
178
150
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
+ }
184
284
}
185
285
186
- export { generateHeader , generateNullChunk } ;
286
+ export default Generator ;
0 commit comments