Skip to content

Commit 8510ca1

Browse files
committed
chore: switched Buffer usage to Uint8Arrays
1 parent a36bb9c commit 8510ca1

File tree

5 files changed

+84
-56
lines changed

5 files changed

+84
-56
lines changed

src/Generator.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,20 @@ import * as utils from './utils';
55
import * as constants from './constants';
66

77
// Computes the checksum by summing up all the bytes in the header
8-
function computeChecksum(header: Buffer): number {
9-
if (!header.subarray(148, 156).every((byte) => byte === 32)) {
8+
function computeChecksum(header: Uint8Array): number {
9+
if (!header.slice(148, 156).every((byte) => byte === 32)) {
1010
throw new errors.ErrorVirtualTarInvalidHeader(
1111
'Checksum field is not properly initialized with spaces',
1212
);
1313
}
1414
return header.reduce((sum, byte) => sum + byte, 0);
1515
}
1616

17-
// TODO: Should logging be included?
1817
function createHeader(
1918
filePath: string,
2019
stat: FileStat,
2120
type: EntryType,
22-
): Buffer {
21+
): Uint8Array {
2322
// TODO: implement long-file-name headers
2423
if (filePath.length < 1 || filePath.length > 255) {
2524
throw new errors.ErrorVirtualTarInvalidFileName(
@@ -48,7 +47,7 @@ function createHeader(
4847

4948
// Make sure to initialise the header with zeros to avoid writing nullish
5049
// blocks.
51-
const header = Buffer.alloc(constants.BLOCK_SIZE, 0);
50+
const header = new Uint8Array(constants.BLOCK_SIZE);
5251

5352
// The TAR headers follow this structure
5453
// Start Size Description
@@ -71,97 +70,95 @@ function createHeader(
7170
// 345 155 File name (last 155 bytes, total 255 bytes, null-padded)
7271
// 500 12 '\0' (unused)
7372
//
74-
// Note that all values are in ASCII format, which is different from the
75-
// default formatting of UTF-8 for Buffer.write(). All numbers are also in
76-
// octal format as opposed to decimal or hexadecimal.
73+
// Note that all numbers are in stringified octal format.
7774

7875
// The first half of the file name (upto 100 bytes) is stored here.
79-
header.write(
76+
utils.writeBytesToArray(
77+
header,
8078
utils.splitFileName(filePath, 0, HeaderSize.FILE_NAME),
8179
HeaderOffset.FILE_NAME,
8280
HeaderSize.FILE_NAME,
83-
constants.TEXT_ENCODING,
8481
);
8582

8683
// The file permissions, or the mode, is stored in the next chunk. This is
8784
// stored in an octal number format.
88-
header.write(
85+
utils.writeBytesToArray(
86+
header,
8987
utils.pad(stat.mode ?? '', HeaderSize.FILE_MODE, '0', '\0'),
9088
HeaderOffset.FILE_MODE,
9189
HeaderSize.FILE_MODE,
92-
constants.TEXT_ENCODING,
9390
);
9491

9592
// The owner UID is stored in this chunk
96-
header.write(
93+
utils.writeBytesToArray(
94+
header,
9795
utils.pad(stat.uid ?? '', HeaderSize.OWNER_UID, '0', '\0'),
9896
HeaderOffset.OWNER_UID,
9997
HeaderSize.OWNER_UID,
100-
constants.TEXT_ENCODING,
10198
);
10299

103100
// The owner GID is stored in this chunk
104-
header.write(
101+
utils.writeBytesToArray(
102+
header,
105103
utils.pad(stat.gid ?? '', HeaderSize.OWNER_GID, '0', '\0'),
106104
HeaderOffset.OWNER_GID,
107105
HeaderSize.OWNER_GID,
108-
constants.TEXT_ENCODING,
109106
);
110107

111108
// The file size is stored in this chunk. The file size must be zero for
112109
// directories, and it must be set for files.
113-
header.write(
110+
utils.writeBytesToArray(
111+
header,
114112
utils.pad(size ?? '', HeaderSize.FILE_SIZE, '0', '\0'),
115113
HeaderOffset.FILE_SIZE,
116114
HeaderSize.FILE_SIZE,
117-
constants.TEXT_ENCODING,
118115
);
119116

120117
// The file mtime is stored in this chunk. As the mtime is not modified when
121118
// extracting a TAR file, the mtime can be preserved while still getting
122119
// deterministic archives.
123-
header.write(
120+
utils.writeBytesToArray(
121+
header,
124122
utils.pad(time, HeaderSize.FILE_MTIME, '0', '\0'),
125123
HeaderOffset.FILE_MTIME,
126124
HeaderSize.FILE_MTIME,
127-
constants.TEXT_ENCODING,
128125
);
129126

130127
// The checksum is calculated as the sum of all bytes in the header. It is
131128
// padded using ASCII spaces, as we currently don't have all the data yet.
132-
header.write(
129+
utils.writeBytesToArray(
130+
header,
133131
utils.pad('', HeaderSize.CHECKSUM, ' '),
134132
HeaderOffset.CHECKSUM,
135133
HeaderSize.CHECKSUM,
136-
constants.TEXT_ENCODING,
137134
);
138135

139136
// The type of file is written as a single byte in the header.
140-
header.write(
137+
utils.writeBytesToArray(
138+
header,
141139
type,
142140
HeaderOffset.TYPE_FLAG,
143141
HeaderSize.TYPE_FLAG,
144-
constants.TEXT_ENCODING,
145142
);
146143

147144
// File owner name will be null, as regular stat-ing cannot extract that
148145
// information.
149146

150147
// This value is the USTAR magic string which makes this file appear as
151148
// a tar file. Without this, the file cannot be parsed and extracted.
152-
header.write(
149+
utils.writeBytesToArray(
150+
header,
153151
constants.USTAR_NAME,
154152
HeaderOffset.USTAR_NAME,
155153
HeaderSize.USTAR_NAME,
156-
constants.TEXT_ENCODING,
157154
);
158155

159156
// This chunk stores the version of USTAR, which is '00' in this case.
160-
header.write(
157+
utils.writeBytesToArray(
158+
header,
161159
constants.USTAR_VERSION,
162160
HeaderOffset.USTAR_VERSION,
163161
HeaderSize.USTAR_VERSION,
164-
constants.TEXT_ENCODING,
165162
);
166163

167164
// Owner user name will be null, as regular stat-ing cannot extract this
@@ -178,15 +175,15 @@ function createHeader(
178175

179176
// The second half of the file name is entered here. This chunk handles file
180177
// names ranging 100 to 255 characters.
181-
header.write(
178+
utils.writeBytesToArray(
179+
header,
182180
utils.splitFileName(
183181
filePath,
184182
HeaderSize.FILE_NAME,
185183
HeaderSize.FILE_NAME_EXTRA,
186184
),
187185
HeaderOffset.FILE_NAME_EXTRA,
188186
HeaderSize.FILE_NAME_EXTRA,
189-
constants.TEXT_ENCODING,
190187
);
191188

192189
// Updating with the new checksum
@@ -195,11 +192,11 @@ function createHeader(
195192
// Note the extra space in the padding for the checksum value. It is
196193
// intentionally placed there. The padding for checksum is ASCII spaces
197194
// instead of null, which is why it is used like this here.
198-
header.write(
195+
utils.writeBytesToArray(
196+
header,
199197
utils.pad(checksum, HeaderSize.CHECKSUM, '0', '\0 '),
200198
HeaderOffset.CHECKSUM,
201199
HeaderSize.CHECKSUM,
202-
constants.TEXT_ENCODING,
203200
);
204201

205202
return header;
@@ -209,7 +206,10 @@ function createHeader(
209206
// bytes filled with nulls. This aligns with the tar end-of-archive marker
210207
// being two null-filled blocks.
211208
function generateEndMarker() {
212-
return [Buffer.alloc(512, 0), Buffer.alloc(512, 0)];
209+
return [
210+
new Uint8Array(constants.BLOCK_SIZE),
211+
new Uint8Array(constants.BLOCK_SIZE),
212+
];
213213
}
214214

215215
export { createHeader, generateEndMarker };

src/Parser.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ class Parser {
88
protected state: ParserState = 'ready';
99
protected remainingBytes = 0;
1010

11-
write(data: ArrayBuffer): Header | Data | End | undefined {
11+
write(data: Uint8Array): Header | Data | End | undefined {
1212
if (data.byteLength !== constants.BLOCK_SIZE) {
1313
throw new errors.ErrorVirtualTarBlockSize(
1414
`Expected block size ${constants.BLOCK_SIZE} but received ${data.byteLength}`,
1515
);
1616
}
1717

18-
// TODO: test if the first block is header by checking magic value
19-
const view = new DataView(data, 0, constants.BLOCK_SIZE);
18+
const view = new DataView(data.buffer, 0, constants.BLOCK_SIZE);
2019

2120
switch (this.state) {
2221
case 'ready': {
@@ -53,23 +52,23 @@ class Parser {
5352
HeaderOffset.OWNER_UID,
5453
HeaderSize.OWNER_UID,
5554
);
56-
const ownerName = utils.extractChars(
55+
const ownerName = utils.extractString(
5756
view,
5857
HeaderOffset.OWNER_NAME,
5958
HeaderSize.OWNER_NAME,
6059
);
61-
const ownerGroupName = utils.extractChars(
60+
const ownerGroupName = utils.extractString(
6261
view,
6362
HeaderOffset.OWNER_GROUPNAME,
6463
HeaderSize.OWNER_GROUPNAME,
6564
);
66-
const ownerUserName = utils.extractChars(
65+
const ownerUserName = utils.extractString(
6766
view,
6867
HeaderOffset.OWNER_USERNAME,
6968
HeaderSize.OWNER_USERNAME,
7069
);
7170
const fileType =
72-
utils.extractChars(
71+
utils.extractString(
7372
view,
7473
HeaderOffset.TYPE_FLAG,
7574
HeaderSize.TYPE_FLAG,

src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
export const BLOCK_SIZE = 512;
22
export const USTAR_NAME = 'ustar\0';
33
export const USTAR_VERSION = '00';
4-
export const TEXT_ENCODING = 'ascii';

src/utils.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,16 @@ function dateToUnixTime(date: Date): number {
3636

3737
// PARSER
3838

39-
const decoder = new TextDecoder(constants.TEXT_ENCODING);
39+
const decoder = new TextDecoder('ascii');
40+
41+
// WARN: redundant?
42+
function dataViewToUint8Array(dataView: DataView): Uint8Array {
43+
return new Uint8Array(
44+
dataView.buffer,
45+
dataView.byteOffset,
46+
dataView.byteLength,
47+
);
48+
}
4049

4150
function extractBytes(
4251
view: DataView,
@@ -46,7 +55,7 @@ function extractBytes(
4655
return new Uint8Array(view.buffer, offset, length);
4756
}
4857

49-
function extractChars(
58+
function extractString(
5059
view: DataView,
5160
offset?: number,
5261
length?: number,
@@ -61,17 +70,17 @@ function extractOctal(
6170
offset?: number,
6271
length?: number,
6372
): number {
64-
const value = extractChars(view, offset, length);
73+
const value = extractString(view, offset, length);
6574
return value.length > 0 ? parseInt(value, 8) : 0;
6675
}
6776

6877
function parseFileName(view: DataView) {
69-
const fileNameLower = extractChars(
78+
const fileNameLower = extractString(
7079
view,
7180
HeaderOffset.FILE_NAME,
7281
HeaderSize.FILE_NAME,
7382
);
74-
const fileNameUpper = extractChars(
83+
const fileNameUpper = extractString(
7584
view,
7685
HeaderOffset.FILE_NAME_EXTRA,
7786
HeaderSize.FILE_NAME_EXTRA,
@@ -86,14 +95,37 @@ function checkNullView(view: DataView): boolean {
8695
return true;
8796
}
8897

98+
function writeBytesToArray(
99+
array: Uint8Array,
100+
bytes: string | ArrayLike<number>,
101+
offset: number,
102+
length: number,
103+
): number {
104+
// Ensure indices are within valid bounds
105+
const start = Math.max(0, Math.min(offset, array.length));
106+
const end = Math.min(array.length, start + Math.max(0, length));
107+
const maxLength = end - start;
108+
109+
let i = 0;
110+
for (; i < bytes.length && i < maxLength; i++) {
111+
array[start + i] =
112+
typeof bytes === 'string' ? bytes.charCodeAt(i) : bytes[i];
113+
}
114+
115+
// Return number of bytes written
116+
return i;
117+
}
118+
89119
export {
90120
never,
91121
pad,
92122
splitFileName,
93123
dateToUnixTime,
124+
dataViewToUint8Array,
94125
extractBytes,
95-
extractChars,
126+
extractString,
96127
extractOctal,
97128
parseFileName,
98129
checkNullView,
130+
writeBytesToArray,
99131
};

tests/index.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import Parser from '@/Parser';
66
import { EntryType } from '@/types';
77

88
// TODO: actually write tests
9-
describe('index', () => {
10-
test('test', async () => {
9+
describe('local test', () => {
10+
test('gen', async () => {
1111
if (process.env['CI'] != null) {
1212
// Skip this test if on CI
1313
expect(true).toEqual(true);
1414
} else {
1515
// Otherwise, run the test which creates a test archive
1616

17-
const walkDir = async (walkPath: string, tokens: Array<Buffer>) => {
17+
const walkDir = async (walkPath: string, tokens: Array<Uint8Array>) => {
1818
const dirContent = await fs.promises.readdir(walkPath);
1919

2020
for (const dirPath of dirContent) {
@@ -78,19 +78,19 @@ describe('index', () => {
7878
).toResolve();
7979
}
8080
}, 60000);
81-
test.only('parsing', async () => {
82-
if (process.env['CI']) expect(true).toBeTruthy();
81+
test('parsing', async () => {
82+
if (process.env['CI'] != null) expect(true).toBeTruthy();
8383
const file = '/home/aryanj/Downloads/dir/archive.tar';
8484

8585
const fileHandle = await fs.promises.open(file, 'r');
86-
const buffer = Buffer.alloc(512, 0);
86+
const buffer = new Uint8Array(512);
8787
const parser = new Parser();
8888
let writeHandle: fs.promises.FileHandle | undefined = undefined;
8989
const root = '/home/aryanj/Downloads/dir';
9090

9191
while (true) {
9292
await fileHandle.read(buffer);
93-
const output = parser.write(buffer.buffer);
93+
const output = parser.write(buffer);
9494

9595
if (output == null) continue;
9696

@@ -117,7 +117,5 @@ describe('index', () => {
117117
}
118118
}
119119
await fileHandle.close();
120-
// Console.log(output);
121-
// expect(output).toBeObject();
122120
});
123121
});

0 commit comments

Comments
 (0)