Skip to content

Commit 166c2e9

Browse files
committed
feat: added options to tar generator
1 parent d706025 commit 166c2e9

File tree

3 files changed

+135
-38
lines changed

3 files changed

+135
-38
lines changed

src/Generator.ts

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,62 @@
1-
import type { TarType, DirectoryContent } from './types';
1+
import type {
2+
EntryType,
3+
DirectoryContent,
4+
HeaderOptions,
5+
ReadFileOptions,
6+
WalkDirectoryOptions,
7+
TarOptions,
8+
} from './types';
29
import fs from 'fs';
310
import path from 'path';
4-
import { TarTypes } from './types';
11+
import { EntryTypes } from './types';
512
import * as errors from './errors';
613

7-
/**
8-
* The size for each tar block. This is usually 512 bytes.
9-
*/
10-
const BLOCK_SIZE = 512;
14+
// Set defaults to the options used by the generators
15+
const defaultHeaderOptions: HeaderOptions = {
16+
fileNameEncoding: 'utf8',
17+
blockSize: 512,
18+
};
19+
const defaultReadFileOptions: ReadFileOptions = {
20+
fs: fs.promises,
21+
blockSize: 512,
22+
};
23+
const defaultWalkDirectoryOptions: WalkDirectoryOptions = {
24+
fs: fs.promises,
25+
blockSize: 512,
26+
};
27+
const defaultTarOptions: TarOptions = {
28+
fs: fs.promises,
29+
blockSize: 512,
30+
fileNameEncoding: 'utf8',
31+
};
1132

1233
function computeChecksum(header: Buffer): number {
13-
let sum = 0;
14-
for (let i = 0; i < BLOCK_SIZE; i++) {
15-
sum += i >= 148 && i < 156 ? 32 : header[i]; // Fill checksum with spaces
34+
if (!header.subarray(148, 156).every((byte) => byte === 32)) {
35+
throw new errors.ErrorVirtualTarInvalidHeader(
36+
'Checksum field is not properly initialized with spaces',
37+
);
1638
}
17-
return sum;
39+
return header.reduce((sum, byte) => sum + byte, 0);
1840
}
1941

20-
function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
42+
function createHeader(
43+
filePath: string,
44+
stat: fs.Stats,
45+
type: EntryType,
46+
options: Partial<HeaderOptions> = defaultHeaderOptions,
47+
): Buffer {
2148
if (filePath.length < 1 || filePath.length > 255) {
2249
throw new errors.ErrorVirtualTarInvalidFileName(
2350
'The file name must be longer than 1 character and shorter than 255 characters',
2451
);
2552
}
2653

27-
const size = type === TarTypes.FILE ? stat.size : 0;
28-
const header = Buffer.alloc(BLOCK_SIZE, 0);
54+
// Merge the defaults with the provided options
55+
const opts: HeaderOptions = { ...defaultHeaderOptions, ...options };
56+
57+
const size = type === EntryTypes.FILE ? stat.size : 0;
58+
const time = parseInt((stat.mtime.getTime() / 1000).toFixed(0)); // Unix time
59+
const header = Buffer.alloc(opts.blockSize, 0);
2960

3061
// The TAR headers follow this structure
3162
// Start Size Description
@@ -48,12 +79,17 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
4879
// 345 155 File name (last 155 bytes, total 255 bytes, null-padded)
4980
// 500 12 '\0' (unused)
5081

51-
header.write(filePath.slice(0, 99).padEnd(100, '\0'), 0, 100, 'utf8');
82+
header.write(
83+
filePath.slice(0, 99).padEnd(100, '\0'),
84+
0,
85+
100,
86+
opts.fileNameEncoding,
87+
);
5288
header.write(stat.mode.toString(8).padStart(7, '0') + '\0', 100, 12, 'ascii');
5389
header.write(stat.uid.toString(8).padStart(7, '0') + '\0', 108, 12, 'ascii');
5490
header.write(stat.gid.toString(8).padStart(7, '0') + '\0', 116, 12, 'ascii');
5591
header.write(size.toString(8).padStart(7, '0') + '\0', 124, 12, 'ascii');
56-
// Mtime will be null
92+
header.write(time.toString(8).padStart(7, '0') + '\0', 136, 12, 'ascii');
5793
header.write(' ', 148, 8, 'ascii'); // Placeholder for checksum
5894
header.write(type, 156, 1, 'ascii');
5995
// File owner name will be null
@@ -63,7 +99,12 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
6399
// Owner group name will be null
64100
// Device major will be null
65101
// Device minor will be null
66-
header.write(filePath.slice(100).padEnd(155, '\0'), 345, 155, 'utf8');
102+
header.write(
103+
filePath.slice(100).padEnd(155, '\0'),
104+
345,
105+
155,
106+
opts.fileNameEncoding,
107+
);
67108

68109
// Updating with the new checksum
69110
const checksum = computeChecksum(header);
@@ -72,19 +113,23 @@ function createHeader(filePath: string, stat: fs.Stats, type: TarType): Buffer {
72113
return header;
73114
}
74115

75-
async function* readFile(filePath: string): AsyncGenerator<Buffer, void, void> {
76-
const fileHandle = await fs.promises.open(filePath, 'r');
77-
const buffer = Buffer.alloc(BLOCK_SIZE);
116+
async function* readFile(
117+
filePath: string,
118+
options: Partial<ReadFileOptions> = defaultReadFileOptions,
119+
): AsyncGenerator<Buffer, void, void> {
120+
const opts: ReadFileOptions = { ...defaultReadFileOptions, ...options };
121+
const fileHandle = await opts.fs.open(filePath, 'r');
122+
const buffer = Buffer.alloc(opts.blockSize);
78123
let bytesRead = -1; // Initialisation value
79124

80125
try {
81126
while (bytesRead !== 0) {
82127
buffer.fill(0);
83-
const result = await fileHandle.read(buffer, 0, BLOCK_SIZE, null);
128+
const result = await fileHandle.read(buffer, 0, opts.blockSize, null);
84129
bytesRead = result.bytesRead;
85130

86131
if (bytesRead === 0) break; // EOF reached
87-
if (bytesRead < 512) buffer.fill(0, bytesRead, BLOCK_SIZE);
132+
if (bytesRead < 512) buffer.fill(0, bytesRead, opts.blockSize);
88133

89134
yield buffer;
90135
}
@@ -99,38 +144,53 @@ async function* readFile(filePath: string): AsyncGenerator<Buffer, void, void> {
99144
async function* walkDirectory(
100145
baseDir: string,
101146
relativePath: string = '',
147+
options: Partial<WalkDirectoryOptions> = defaultWalkDirectoryOptions,
102148
): AsyncGenerator<DirectoryContent> {
103-
const entries = await fs.promises.readdir(path.join(baseDir, relativePath));
149+
const opts: WalkDirectoryOptions = {
150+
...defaultWalkDirectoryOptions,
151+
...options,
152+
};
153+
const entries = await opts.fs.readdir(path.join(baseDir, relativePath));
104154

105155
// Sort the entries lexicographically
106156
for (const entry of entries.sort()) {
107157
const fullPath = path.join(baseDir, relativePath, entry);
108-
const stat = await fs.promises.stat(fullPath);
158+
const stat = await opts.fs.stat(fullPath);
109159
const tarPath = path.join(relativePath, entry);
110160

111161
if (stat.isDirectory()) {
112-
yield { path: tarPath + '/', stat: stat, type: TarTypes.DIRECTORY };
162+
yield { path: tarPath + '/', stat: stat, type: EntryTypes.DIRECTORY };
113163
yield* walkDirectory(baseDir, path.join(relativePath, entry));
114164
} else if (stat.isFile()) {
115-
yield { path: tarPath, stat: stat, type: TarTypes.FILE };
165+
yield { path: tarPath, stat: stat, type: EntryTypes.FILE };
116166
}
117167
}
118168
}
119169

120-
async function* createTar(baseDir: string): AsyncGenerator<Buffer, void, void> {
121-
for await (const entry of walkDirectory(baseDir)) {
122-
// Create header
170+
async function* createTar(
171+
baseDir: string,
172+
options: Partial<TarOptions> = defaultTarOptions,
173+
): AsyncGenerator<Buffer, void, void> {
174+
const opts = { ...defaultTarOptions, ...options };
175+
const entryGen = walkDirectory(baseDir, '', {
176+
fs: opts.fs,
177+
blockSize: opts.blockSize,
178+
});
179+
180+
for await (const entry of entryGen) {
123181
yield createHeader(entry.path, entry.stat, entry.type);
124182

125-
if (entry.type === TarTypes.FILE) {
126-
// Get file contents
127-
yield* readFile(path.join(baseDir, entry.path));
183+
if (entry.type === EntryTypes.FILE) {
184+
yield* readFile(path.join(baseDir, entry.path), {
185+
fs: opts.fs,
186+
blockSize: opts.blockSize,
187+
});
128188
}
129189
}
130190

131191
// End-of-archive marker - two 512-byte null blocks
132-
yield Buffer.alloc(BLOCK_SIZE, 0);
133-
yield Buffer.alloc(BLOCK_SIZE, 0);
192+
yield Buffer.alloc(opts.blockSize, 0);
193+
yield Buffer.alloc(opts.blockSize, 0);
134194
}
135195

136196
export { createHeader, readFile, createTar };

src/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,13 @@ class ErrorVirtualTarInvalidFileName<T> extends ErrorVirtualTar<T> {
1212
static description = 'The provided file name is invalid';
1313
}
1414

15+
class ErrorVirtualTarInvalidHeader<T> extends ErrorVirtualTar<T> {
16+
static description = 'The header has invalid data';
17+
}
18+
1519
export {
1620
ErrorVirtualTar,
1721
ErrorVirtualTarUndefinedBehaviour,
1822
ErrorVirtualTarInvalidFileName,
23+
ErrorVirtualTarInvalidHeader,
1924
};

src/types.ts

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
11
import type { Stats } from 'fs';
22

3-
const TarTypes = {
3+
const EntryTypes = {
44
FILE: '0',
55
DIRECTORY: '5',
66
} as const;
77

8-
type TarType = (typeof TarTypes)[keyof typeof TarTypes];
8+
type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes];
99

1010
type DirectoryContent = {
1111
path: string;
1212
stat: Stats;
13-
type: TarType;
13+
type: EntryType;
1414
};
1515

16-
export type { TarType, DirectoryContent };
17-
export { TarTypes };
16+
type HeaderOptions = {
17+
fileNameEncoding: 'ascii' | 'utf8';
18+
blockSize: number;
19+
};
20+
21+
// An actual type for `fs` doesn't exist
22+
type ReadFileOptions = {
23+
fs: any;
24+
blockSize: number;
25+
};
26+
27+
// An actual type for `fs` doesn't exist
28+
type WalkDirectoryOptions = {
29+
fs: any;
30+
blockSize: number;
31+
};
32+
33+
// An actual type for `fs` doesn't exist
34+
type TarOptions = {
35+
fs: any;
36+
blockSize: number;
37+
fileNameEncoding: 'ascii' | 'utf8';
38+
};
39+
40+
export type {
41+
EntryType,
42+
DirectoryContent,
43+
HeaderOptions,
44+
ReadFileOptions,
45+
WalkDirectoryOptions,
46+
TarOptions,
47+
};
48+
49+
export { EntryTypes };

0 commit comments

Comments
 (0)