Skip to content

Commit 718867f

Browse files
committed
feat: added higher level virtualtar api
1 parent f16adb3 commit 718867f

13 files changed

+570
-216
lines changed

src/Generator.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FileType, FileStat } from './types';
2-
import { GeneratorState, EntryType, HeaderSize } from './types';
2+
import { GeneratorState, HeaderSize } from './types';
33
import * as errors from './errors';
44
import * as utils from './utils';
55
import * as constants from './constants';
@@ -46,7 +46,11 @@ class Generator {
4646
protected state: GeneratorState = GeneratorState.HEADER;
4747
protected remainingBytes = 0;
4848

49-
protected generateHeader(filePath: string, type: FileType, stat: FileStat): Uint8Array {
49+
protected generateHeader(
50+
filePath: string,
51+
type: FileType,
52+
stat: FileStat,
53+
): Uint8Array {
5054
if (filePath.length > 255) {
5155
throw new errors.ErrorVirtualTarGeneratorInvalidFileName(
5256
'The file name must shorter than 255 characters',
@@ -59,18 +63,15 @@ class Generator {
5963
);
6064
}
6165

62-
if (
63-
stat?.username != null &&
64-
stat?.username.length > HeaderSize.OWNER_USERNAME
65-
) {
66+
if (stat?.uname != null && stat?.uname.length > HeaderSize.OWNER_USERNAME) {
6667
throw new errors.ErrorVirtualTarGeneratorInvalidStat(
6768
`The username must not exceed ${HeaderSize.OWNER_USERNAME} bytes`,
6869
);
6970
}
7071

7172
if (
72-
stat?.groupname != null &&
73-
stat?.groupname.length > HeaderSize.OWNER_GROUPNAME
73+
stat?.gname != null &&
74+
stat?.gname.length > HeaderSize.OWNER_GROUPNAME
7475
) {
7576
throw new errors.ErrorVirtualTarGeneratorInvalidStat(
7677
`The groupname must not exceed ${HeaderSize.OWNER_GROUPNAME} bytes`,
@@ -90,8 +91,8 @@ class Generator {
9091
utils.writeFileMode(header, stat.mode);
9192
utils.writeOwnerUid(header, stat.uid);
9293
utils.writeOwnerGid(header, stat.gid);
93-
utils.writeOwnerUserName(header, stat.username);
94-
utils.writeOwnerGroupName(header, stat.groupname);
94+
utils.writeOwnerUserName(header, stat.uname);
95+
utils.writeOwnerGroupName(header, stat.gname);
9596
utils.writeFileSize(header, stat.size);
9697
utils.writeFileMtime(header, stat.mtime);
9798

@@ -172,6 +173,13 @@ class Generator {
172173
if (this.remainingBytes === 0) this.state = GeneratorState.HEADER;
173174
return data;
174175
} else {
176+
// Make sure we don't attempt to write extra data
177+
if (data.byteLength !== this.remainingBytes) {
178+
throw new errors.ErrorVirtualTarGeneratorBlockSize(
179+
`Expected data to be ${this.remainingBytes} bytes but received ${data.byteLength} bytes`,
180+
);
181+
}
182+
175183
// Update state
176184
this.remainingBytes = 0;
177185
this.state = GeneratorState.HEADER;

src/Parser.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -129,14 +129,14 @@ class Parser {
129129

130130
protected parseData(array: Uint8Array, remainingBytes: number): TokenData {
131131
if (remainingBytes > 512) {
132-
return { type: 'data', data: utils.extractBytes(array) };
132+
return { type: 'data', data: utils.extractBytes(array), end: false };
133133
} else {
134134
const data = utils.extractBytes(array, 0, remainingBytes);
135-
return { type: 'data', data: data };
135+
return { type: 'data', data: data, end: true };
136136
}
137137
}
138138

139-
write(data: Uint8Array) {
139+
write(data: Uint8Array): TokenHeader | TokenData | TokenEnd | undefined {
140140
if (data.byteLength !== constants.BLOCK_SIZE) {
141141
throw new errors.ErrorVirtualTarParserBlockSize(
142142
`Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`,

src/VirtualTar.ts

+291
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
import type {
2+
FileStat,
3+
ParsedFile,
4+
ParsedDirectory,
5+
ParsedMetadata,
6+
ParsedEmpty,
7+
MetadataKeywords,
8+
} from './types';
9+
import { VirtualTarState } from './types';
10+
import Generator from './Generator';
11+
import Parser from './Parser';
12+
import * as constants from './constants';
13+
import * as errors from './errors';
14+
import * as utils from './utils';
15+
16+
class VirtualTar {
17+
protected state: VirtualTarState;
18+
protected generator: Generator;
19+
protected parser: Parser;
20+
protected chunks: Array<Uint8Array>;
21+
protected encoder = new TextEncoder();
22+
protected accumulator: Uint8Array;
23+
protected workingToken: ParsedFile | ParsedMetadata | undefined;
24+
protected workingData: Array<Uint8Array>;
25+
protected workingMetadata:
26+
| Partial<Record<MetadataKeywords, string>>
27+
| undefined;
28+
29+
protected addEntry(
30+
filePath: string,
31+
type: 'file' | 'directory',
32+
stat?: FileStat,
33+
data?: Uint8Array | string,
34+
): void;
35+
36+
protected addEntry(
37+
filePath: string,
38+
type: 'file' | 'directory',
39+
stat?: FileStat,
40+
callback?: (write: (chunk: string | Uint8Array) => void) => void,
41+
): void;
42+
43+
protected addEntry(
44+
filePath: string,
45+
type: 'file' | 'directory',
46+
stat: FileStat = {},
47+
dataOrCallback?:
48+
| Uint8Array
49+
| string
50+
| ((write: (chunk: string | Uint8Array) => void) => void),
51+
): void {
52+
if (filePath.length > constants.STANDARD_PATH_SIZE) {
53+
// Push the extended metadata header
54+
const data = utils.encodeExtendedHeader({ path: filePath });
55+
this.chunks.push(this.generator.generateExtended(data.byteLength));
56+
57+
// Push the content
58+
for (
59+
let offset = 0;
60+
offset < data.byteLength;
61+
offset += constants.BLOCK_SIZE
62+
) {
63+
this.chunks.push(
64+
this.generator.generateData(
65+
data.subarray(offset, offset + constants.BLOCK_SIZE),
66+
),
67+
);
68+
}
69+
}
70+
71+
filePath = filePath.length <= 255 ? filePath : '';
72+
73+
// Generate the header
74+
if (type === 'file') {
75+
this.chunks.push(this.generator.generateFile(filePath, stat));
76+
} else {
77+
this.chunks.push(this.generator.generateDirectory(filePath, stat));
78+
}
79+
80+
// Generate the data
81+
if (dataOrCallback == null) return;
82+
83+
const writeData = (data: string | Uint8Array) => {
84+
if (data instanceof Uint8Array) {
85+
for (
86+
let offset = 0;
87+
offset < data.byteLength;
88+
offset += constants.BLOCK_SIZE
89+
) {
90+
const chunk = data.slice(offset, offset + constants.BLOCK_SIZE);
91+
this.chunks.push(this.generator.generateData(chunk));
92+
}
93+
} else {
94+
while (data.length > 0) {
95+
const chunk = this.encoder.encode(
96+
data.slice(0, constants.BLOCK_SIZE),
97+
);
98+
this.chunks.push(this.generator.generateData(chunk));
99+
data = data.slice(constants.BLOCK_SIZE);
100+
}
101+
}
102+
};
103+
104+
if (typeof dataOrCallback === 'function') {
105+
const data: Array<Uint8Array> = [];
106+
const writer = (chunk: string | Uint8Array) => {
107+
if (chunk instanceof Uint8Array) data.push(chunk);
108+
else data.push(this.encoder.encode(chunk));
109+
};
110+
dataOrCallback(writer);
111+
writeData(utils.concatUint8Arrays(...data));
112+
} else {
113+
writeData(dataOrCallback);
114+
}
115+
}
116+
117+
constructor({ mode }: { mode: 'generate' | 'parse' } = { mode: 'parse' }) {
118+
if (mode === 'generate') {
119+
this.state = VirtualTarState.GENERATOR;
120+
this.generator = new Generator();
121+
this.chunks = [];
122+
} else {
123+
this.state = VirtualTarState.PARSER;
124+
this.parser = new Parser();
125+
this.workingData = [];
126+
}
127+
}
128+
129+
public addFile(
130+
filePath: string,
131+
stat: FileStat,
132+
data?: Uint8Array | string,
133+
): void {
134+
if (this.state !== VirtualTarState.GENERATOR) {
135+
throw new errors.ErrorVirtualTarInvalidState(
136+
'VirtualTar is not in generator mode',
137+
);
138+
}
139+
this.addEntry(filePath, 'file', stat, data);
140+
}
141+
142+
public addDirectory(filePath: string, stat?: FileStat): void {
143+
if (this.state !== VirtualTarState.GENERATOR) {
144+
throw new errors.ErrorVirtualTarInvalidState(
145+
'VirtualTar is not in generator mode',
146+
);
147+
}
148+
this.addEntry(filePath, 'directory', stat);
149+
}
150+
151+
public finalize(): Uint8Array {
152+
if (this.state !== VirtualTarState.GENERATOR) {
153+
throw new errors.ErrorVirtualTarInvalidState(
154+
'VirtualTar is not in generator mode',
155+
);
156+
}
157+
this.chunks.push(this.generator.generateEnd());
158+
this.chunks.push(this.generator.generateEnd());
159+
return utils.concatUint8Arrays(...this.chunks);
160+
}
161+
162+
public push(chunk: Uint8Array): void {
163+
if (this.state !== VirtualTarState.PARSER) {
164+
throw new errors.ErrorVirtualTarInvalidState(
165+
'VirtualTar is not in parser mode',
166+
);
167+
}
168+
this.accumulator = utils.concatUint8Arrays(this.accumulator, chunk);
169+
}
170+
171+
public next(): ParsedFile | ParsedDirectory | ParsedEmpty {
172+
if (this.state !== VirtualTarState.PARSER) {
173+
throw new errors.ErrorVirtualTarInvalidState(
174+
'VirtualTar is not in parser mode',
175+
);
176+
}
177+
if (this.accumulator.byteLength < constants.BLOCK_SIZE) {
178+
return { type: 'empty', awaitingData: true };
179+
}
180+
181+
const chunk = this.accumulator.slice(0, constants.BLOCK_SIZE);
182+
this.accumulator = this.accumulator.slice(constants.BLOCK_SIZE);
183+
const token = this.parser.write(chunk);
184+
185+
if (token == null) {
186+
return { type: 'empty', awaitingData: false };
187+
}
188+
189+
if (token.type === 'header') {
190+
if (token.fileType === 'metadata') {
191+
this.workingToken = { type: 'metadata' };
192+
return { type: 'empty', awaitingData: false };
193+
}
194+
195+
// If we have additional metadata, then use it to override token data
196+
let filePath = token.filePath;
197+
if (this.workingMetadata != null) {
198+
filePath = this.workingMetadata.path ?? filePath;
199+
this.workingMetadata = undefined;
200+
}
201+
202+
if (token.fileType === 'directory') {
203+
return {
204+
type: 'directory',
205+
path: filePath,
206+
stat: {
207+
size: token.fileSize,
208+
mode: token.fileMode,
209+
mtime: token.fileMtime,
210+
uid: token.ownerUid,
211+
gid: token.ownerGid,
212+
uname: token.ownerUserName,
213+
gname: token.ownerGroupName,
214+
},
215+
};
216+
} else if (token.fileType === 'file') {
217+
this.workingToken = {
218+
type: 'file',
219+
path: filePath,
220+
stat: {
221+
size: token.fileSize,
222+
mode: token.fileMode,
223+
mtime: token.fileMtime,
224+
uid: token.ownerUid,
225+
gid: token.ownerGid,
226+
uname: token.ownerUserName,
227+
gname: token.ownerGroupName,
228+
},
229+
content: new Uint8Array(token.fileSize),
230+
};
231+
}
232+
} else {
233+
if (this.workingToken == null) {
234+
throw new errors.ErrorVirtualTarInvalidState(
235+
'Received data token before header token',
236+
);
237+
}
238+
if (token.type === 'end') {
239+
throw new errors.ErrorVirtualTarInvalidState(
240+
'Received end token before header token',
241+
);
242+
}
243+
244+
// Token is of type 'data' after this
245+
const { data, end } = token;
246+
this.workingData.push(data);
247+
248+
if (end) {
249+
// Concat the working data into a single Uint8Array
250+
const data = utils.concatUint8Arrays(...this.workingData);
251+
this.workingData = [];
252+
253+
// If the current working token is a metadata token, then decode the
254+
// accumulated header. Otherwise, we have obtained all the data for
255+
// a file. Set the content of the file then return it.
256+
if (this.workingToken.type === 'metadata') {
257+
this.workingMetadata = utils.decodeExtendedHeader(data);
258+
return { type: 'empty', awaitingData: false };
259+
} else if (this.workingToken.type === 'file') {
260+
this.workingToken.content.set(data);
261+
const fileToken = this.workingToken;
262+
this.workingToken = undefined;
263+
return fileToken;
264+
}
265+
}
266+
}
267+
return { type: 'empty', awaitingData: false };
268+
}
269+
270+
public parseAvailable(): Array<
271+
ParsedFile | ParsedDirectory | ParsedMetadata | ParsedEmpty
272+
> {
273+
if (this.state !== VirtualTarState.PARSER) {
274+
throw new errors.ErrorVirtualTarInvalidState(
275+
'VirtualTar is not in parser mode',
276+
);
277+
}
278+
279+
const parsedTokens: Array<
280+
ParsedFile | ParsedDirectory | ParsedMetadata | ParsedEmpty
281+
> = [];
282+
let token;
283+
while (token.type !== 'empty' && !token.awaitingData) {
284+
token = this.next();
285+
parsedTokens.push(token);
286+
}
287+
return parsedTokens;
288+
}
289+
}
290+
291+
export default VirtualTar;

src/errors.ts

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ class ErrorVirtualTarUndefinedBehaviour<T> extends ErrorVirtualTar<T> {
88
static description = 'You should never see this error';
99
}
1010

11+
class ErrorVirtualTarInvalidState<T> extends ErrorVirtualTar<T> {
12+
static description = 'The state is incorrect for the desired operation';
13+
}
14+
1115
class ErrorVirtualTarGenerator<T> extends ErrorVirtualTar<T> {
1216
static description = 'VirtualTar genereator errors';
1317
}
@@ -59,6 +63,7 @@ class ErrorVirtualTarParserEndOfArchive<T> extends ErrorVirtualTarParser<T> {
5963
export {
6064
ErrorVirtualTar,
6165
ErrorVirtualTarUndefinedBehaviour,
66+
ErrorVirtualTarInvalidState,
6267
ErrorVirtualTarGenerator,
6368
ErrorVirtualTarGeneratorInvalidFileName,
6469
ErrorVirtualTarGeneratorInvalidStat,

0 commit comments

Comments
 (0)