Skip to content

Commit 92de4eb

Browse files
committed
feat: added higher level virtualtar api
1 parent 26043c3 commit 92de4eb

File tree

13 files changed

+570
-216
lines changed

13 files changed

+570
-216
lines changed

src/Generator.ts

Lines changed: 18 additions & 10 deletions
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

Lines changed: 3 additions & 3 deletions
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

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

Lines changed: 5 additions & 0 deletions
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)