1- import type { FileStat } from './types' ;
2- import { GeneratorState , EntryType , HeaderSize , HeaderOffset } from './types' ;
1+ import type { FileType , FileStat } from './types' ;
2+ import { GeneratorState , EntryType } from './types' ;
33import * as errors from './errors' ;
44import * as utils from './utils' ;
55import * as constants from './constants' ;
66
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' ,
11- ) ;
12- }
13-
14- // The time can be undefined, which would be referring to epoch 0.
15- const time = utils . dateToUnixTime ( stat . mtime ?? new Date ( 0 ) ) ;
16-
17- const header = new Uint8Array ( constants . BLOCK_SIZE ) ;
18-
19- // Every directory in tar must have a trailing /
20- if ( type === EntryType . DIRECTORY ) {
21- filePath = filePath . endsWith ( '/' ) ? filePath : filePath + '/' ;
22- }
23-
24- // If the length of the file path is less than 100 bytes, then we write it to
25- // the file name. Otherwise, we write it into the file name prefix and append
26- // file name to it.
27- if ( filePath . length < HeaderSize . FILE_NAME ) {
28- utils . writeBytesToArray (
29- header ,
30- utils . splitFileName ( filePath , 0 , HeaderSize . FILE_NAME ) ,
31- HeaderOffset . FILE_NAME ,
32- HeaderSize . FILE_NAME ,
33- ) ;
34- } else {
35- utils . writeBytesToArray (
36- header ,
37- utils . splitFileName (
38- filePath ,
39- HeaderSize . FILE_NAME ,
40- HeaderSize . FILE_NAME_PREFIX ,
41- ) ,
42- HeaderOffset . FILE_NAME ,
43- HeaderSize . FILE_NAME ,
44- ) ;
45- utils . writeBytesToArray (
46- header ,
47- utils . splitFileName ( filePath , 0 , HeaderSize . FILE_NAME ) ,
48- HeaderOffset . FILE_NAME_PREFIX ,
49- HeaderSize . FILE_NAME_PREFIX ,
50- ) ;
51- }
52-
53- // The file permissions, or the mode, is stored in the next chunk. This is
54- // stored in an octal number format.
55- utils . writeBytesToArray (
56- header ,
57- utils . pad ( stat . mode ?? '' , HeaderSize . FILE_MODE , '0' , '\0' ) ,
58- HeaderOffset . FILE_MODE ,
59- HeaderSize . FILE_MODE ,
60- ) ;
61-
62- // The owner UID is stored in this chunk
63- utils . writeBytesToArray (
64- header ,
65- utils . pad ( stat . uid ?? '' , HeaderSize . OWNER_UID , '0' , '\0' ) ,
66- HeaderOffset . OWNER_UID ,
67- HeaderSize . OWNER_UID ,
68- ) ;
69-
70- // The owner GID is stored in this chunk
71- utils . writeBytesToArray (
72- header ,
73- utils . pad ( stat . gid ?? '' , HeaderSize . OWNER_GID , '0' , '\0' ) ,
74- HeaderOffset . OWNER_GID ,
75- HeaderSize . OWNER_GID ,
76- ) ;
77-
78- // The file size is stored in this chunk. The file size must be zero for
79- // directories, and it must be set for files.
80- utils . writeBytesToArray (
81- header ,
82- utils . pad ( stat . size ?? '' , HeaderSize . FILE_SIZE , '0' , '\0' ) ,
83- HeaderOffset . FILE_SIZE ,
84- HeaderSize . FILE_SIZE ,
85- ) ;
86-
87- // The file mtime is stored in this chunk. As the mtime is not modified when
88- // extracting a TAR file, the mtime can be preserved while still getting
89- // deterministic archives.
90- utils . writeBytesToArray (
91- header ,
92- utils . pad ( time , HeaderSize . FILE_MTIME , '0' , '\0' ) ,
93- HeaderOffset . FILE_MTIME ,
94- HeaderSize . FILE_MTIME ,
95- ) ;
96-
97- // The checksum is calculated as the sum of all bytes in the header. It is
98- // left blank for later calculation.
99-
100- // The type of file is written as a single byte in the header.
101- utils . writeBytesToArray (
102- header ,
103- type ,
104- HeaderOffset . TYPE_FLAG ,
105- HeaderSize . TYPE_FLAG ,
106- ) ;
107-
108- // Link name will be null, as regular stat-ing cannot extract that
109- // information.
110-
111- // This value is the USTAR magic string which makes this file appear as
112- // a tar file. Without this, the file cannot be parsed and extracted.
113- utils . writeBytesToArray (
114- header ,
115- constants . USTAR_NAME ,
116- HeaderOffset . USTAR_NAME ,
117- HeaderSize . USTAR_NAME ,
118- ) ;
119-
120- // This chunk stores the version of USTAR, which is '00' in this case.
121- utils . writeBytesToArray (
122- header ,
123- constants . USTAR_VERSION ,
124- HeaderOffset . USTAR_VERSION ,
125- HeaderSize . USTAR_VERSION ,
126- ) ;
127-
128- // Owner user name will be null, as regular stat-ing cannot extract this
129- // information.
130-
131- // Owner group name will be null, as regular stat-ing cannot extract this
132- // information.
133-
134- // Device major will be null, as this specific to linux kernel knowing what
135- // drivers to use for executing certain files, and is irrelevant here.
136-
137- // Device minor will be null, as this specific to linux kernel knowing what
138- // drivers to use for executing certain files, and is irrelevant here.
139-
140- // Updating with the new checksum
141- const checksum = utils . calculateChecksum ( header ) ;
142-
143- // Note the extra space in the padding for the checksum value. It is
144- // intentionally placed there. The padding for checksum is ASCII spaces
145- // instead of null, which is why it is used like this here.
146- utils . writeBytesToArray (
147- header ,
148- utils . pad ( checksum , HeaderSize . CHECKSUM , '0' , '\0' ) ,
149- HeaderOffset . CHECKSUM ,
150- HeaderSize . CHECKSUM ,
151- ) ;
152-
153- return header ;
154- }
155-
1567/**
157- * The TAR headers follow this structure
8+ * The TAR headers follow this structure:
1589 * Start Size Description
15910 * ------------------------------
16011 * 0 100 File name (first 100 bytes)
@@ -176,11 +27,55 @@ function generateHeader(filePath: string, type: EntryType, stat: FileStat) {
17627 * 500 12 '\0' (unused)
17728 *
17829 * Note that all numbers are in stringified octal format.
30+ *
31+ * The following data will be left blank (null):
32+ * - Link name
33+ * - Owner user name
34+ * - Owner group name
35+ * - Device major
36+ * - Device minor
37+ *
38+ * This is because this implementation does not interact with linked files.
39+ * Owner user name and group name cannot be extracted via regular stat-ing,
40+ * so it is left blank. In virtual situations, this field won't be useful
41+ * anyways. The device major and minor are specific to linux kernel, which
42+ * is not relevant to this virtual tar implementation. This is the reason
43+ * these fields have been left blank.
17944 */
18045class Generator {
18146 protected state : GeneratorState = GeneratorState . READY ;
18247 protected remainingBytes = 0 ;
18348
49+ protected generateHeader ( filePath : string , type : FileType , stat : FileStat ) {
50+ if ( filePath . length > 255 ) {
51+ throw new errors . ErrorVirtualTarGeneratorInvalidFileName (
52+ 'The file name must shorter than 255 characters' ,
53+ ) ;
54+ }
55+
56+ const header = new Uint8Array ( constants . BLOCK_SIZE ) ;
57+
58+ // Every directory in tar must have a trailing slash
59+ if ( type === 'directory' ) {
60+ filePath = filePath . endsWith ( '/' ) ? filePath : filePath + '/' ;
61+ }
62+
63+ utils . writeUstarMagic ( header ) ;
64+ utils . writeFileType ( header , type ) ;
65+ utils . writeFilePath ( header , filePath ) ;
66+ utils . writeFileMode ( header , stat . mode ) ;
67+ utils . writeOwnerUid ( header , stat . uid ) ;
68+ utils . writeOwnerGid ( header , stat . gid ) ;
69+ utils . writeFileSize ( header , stat . size ) ;
70+ utils . writeFileMtime ( header , stat . mtime ) ;
71+
72+ // The checksum can only be calculated once the entire header has been
73+ // written. This is why the checksum is calculated and written at the end.
74+ utils . writeChecksum ( header , utils . calculateChecksum ( header ) ) ;
75+
76+ return header ;
77+ }
78+
18479 generateFile ( filePath : string , stat : FileStat ) : Uint8Array {
18580 if ( this . state === GeneratorState . READY ) {
18681 // Make sure the size is valid
@@ -190,7 +85,7 @@ class Generator {
19085 ) ;
19186 }
19287
193- const generatedBlock = generateHeader ( filePath , EntryType . FILE , stat ) ;
88+ const generatedBlock = this . generateHeader ( filePath , 'file' , stat ) ;
19489
19590 // If no data is in the file, then there is no need of a data block. It
19691 // will remain as READY.
@@ -217,7 +112,7 @@ class Generator {
217112 uid : stat . uid ,
218113 gid : stat . gid ,
219114 } ;
220- return generateHeader ( filePath , EntryType . DIRECTORY , directoryStat ) ;
115+ return this . generateHeader ( filePath , 'directory' , directoryStat ) ;
221116 }
222117 throw new errors . ErrorVirtualTarGeneratorInvalidState (
223118 `Expected state ${ GeneratorState [ GeneratorState . READY ] } but got ${
@@ -230,7 +125,7 @@ class Generator {
230125 if ( this . state === GeneratorState . READY ) {
231126 this . state = GeneratorState . DATA ;
232127 this . remainingBytes = size ;
233- return generateHeader ( './PaxHeader' , EntryType . EXTENDED , { size } ) ;
128+ return this . generateHeader ( './PaxHeader' , 'extended' , { size } ) ;
234129 }
235130 throw new errors . ErrorVirtualTarGeneratorInvalidState (
236131 `Expected state ${ GeneratorState [ GeneratorState . READY ] } but got ${
0 commit comments