11import type { FileStat } from './types' ;
2- import { EntryType , HeaderSize , HeaderOffset } from './types' ;
2+ import { GeneratorState , EntryType , HeaderSize , HeaderOffset } from './types' ;
33import * as errors from './errors' ;
44import * as utils from './utils' ;
55import * 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