Skip to content

Commit

Permalink
chore: switched out dataview to uint8arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
aryanjassal committed Mar 3, 2025
1 parent 00233d1 commit 76fe731
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 114 deletions.
23 changes: 5 additions & 18 deletions src/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,29 @@ import * as errors from './errors';
import * as utils from './utils';
import * as constants from './constants';

// Computes the checksum by summing up all the bytes in the header
function computeChecksum(header: Uint8Array): number {
return header.reduce((sum, byte) => sum + byte, 0);
}

function generateHeader(
filePath: string,
type: EntryType,
stat: FileStat,
): Uint8Array {
// TODO: implement long-file-name headers
if (filePath.length < 1 || filePath.length > 255) {
throw new errors.ErrorVirtualTarInvalidFileName(
throw new errors.ErrorTarGeneratorInvalidFileName(
'The file name must be longer than 1 character and shorter than 255 characters',
);
}

// As the size does not matter for directories, it can be undefined. However,
// if the header is being generated for a file, then it needs to have a valid
// size. This guard checks that.
// size.
if (stat.size == null && type === EntryType.FILE) {
throw new errors.ErrorVirtualTarInvalidStat('Size must be set for files');
throw new errors.ErrorTarGeneratorInvalidStat('Size must be set for files');
}
const size = type === EntryType.FILE ? stat.size : 0;

// The time can be undefined, which would be referring to epoch 0.
const time = utils.dateToUnixTime(stat.mtime ?? new Date());

// Make sure to initialise the header with zeros to avoid writing nullish
// blocks.
const header = new Uint8Array(constants.BLOCK_SIZE);

// The TAR headers follow this structure
Expand Down Expand Up @@ -112,13 +105,7 @@ function generateHeader(
);

// The checksum is calculated as the sum of all bytes in the header. It is
// padded using ASCII spaces, as we currently don't have all the data yet.
utils.writeBytesToArray(
header,
utils.pad('', HeaderSize.CHECKSUM, ' '),
HeaderOffset.CHECKSUM,
HeaderSize.CHECKSUM,
);
// left blank for later calculation.

// The type of file is written as a single byte in the header.
utils.writeBytesToArray(
Expand Down Expand Up @@ -174,7 +161,7 @@ function generateHeader(
);

// Updating with the new checksum
const checksum = computeChecksum(header);
const checksum = utils.calculateChecksum(header);

// Note the extra space in the padding for the checksum value. It is
// intentionally placed there. The padding for checksum is ASCII spaces
Expand Down
82 changes: 58 additions & 24 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,86 @@ import * as constants from './constants';
import * as errors from './errors';
import * as utils from './utils';

function parseHeader(view: DataView): HeaderToken {
// TODO: confirm integrity by checking against checksum
const filePath = utils.parseFilePath(view);
function parseHeader(array: Uint8Array): HeaderToken {
// Validate header by checking checksum and magic string
const headerChecksum = utils.extractOctal(
array,
HeaderOffset.CHECKSUM,
HeaderSize.CHECKSUM,
);
const calculatedChecksum = utils.calculateChecksum(array);

if (headerChecksum !== calculatedChecksum) {
throw new errors.ErrorTarParserInvalidHeader(
`Expected checksum to be ${calculatedChecksum} but received ${headerChecksum}`,
);
}

const ustarMagic = utils.extractString(
array,
HeaderOffset.USTAR_NAME,
HeaderSize.USTAR_NAME,
);
if (ustarMagic !== constants.USTAR_NAME) {
throw new errors.ErrorTarParserInvalidHeader(
`Expected ustar magic to be '${constants.USTAR_NAME}', got '${ustarMagic}'`,
);
}

const ustarVersion = utils.extractString(
array,
HeaderOffset.USTAR_VERSION,
HeaderSize.USTAR_VERSION,
);
if (ustarVersion !== constants.USTAR_VERSION) {
throw new errors.ErrorTarParserInvalidHeader(
`Expected ustar version to be '${constants.USTAR_VERSION}', got '${ustarVersion}'`,
);
}

// Extract the relevant metadata from the header
const filePath = utils.parseFilePath(array);
const fileSize = utils.extractOctal(
view,
array,
HeaderOffset.FILE_SIZE,
HeaderSize.FILE_SIZE,
);
const fileMtime = new Date(
utils.extractOctal(view, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) *
utils.extractOctal(array, HeaderOffset.FILE_MTIME, HeaderSize.FILE_MTIME) *
1000,
);
const fileMode = utils.extractOctal(
view,
array,
HeaderOffset.FILE_MODE,
HeaderSize.FILE_MODE,
);
const ownerGid = utils.extractOctal(
view,
array,
HeaderOffset.OWNER_GID,
HeaderSize.OWNER_GID,
);
const ownerUid = utils.extractOctal(
view,
array,
HeaderOffset.OWNER_UID,
HeaderSize.OWNER_UID,
);
const ownerName = utils.extractString(
view,
array,
HeaderOffset.OWNER_NAME,
HeaderSize.OWNER_NAME,
);
const ownerGroupName = utils.extractString(
view,
array,
HeaderOffset.OWNER_GROUPNAME,
HeaderSize.OWNER_GROUPNAME,
);
const ownerUserName = utils.extractString(
view,
array,
HeaderOffset.OWNER_USERNAME,
HeaderSize.OWNER_USERNAME,
);
const fileType =
utils.extractString(view, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) ===
utils.extractString(array, HeaderOffset.TYPE_FLAG, HeaderSize.TYPE_FLAG) ===
EntryType.FILE
? 'file'
: 'directory';
Expand All @@ -68,11 +104,11 @@ function parseHeader(view: DataView): HeaderToken {
};
}

function parseData(view: DataView, remainingBytes: number): DataToken {
function parseData(array: Uint8Array, remainingBytes: number): DataToken {
if (remainingBytes > 512) {
return { type: 'data', data: utils.extractBytes(view) };
return { type: 'data', data: utils.extractBytes(array) };
} else {
const data = utils.extractBytes(view, 0, remainingBytes);
const data = utils.extractBytes(array, 0, remainingBytes);
return { type: 'data', data: data };
}
}
Expand All @@ -83,29 +119,27 @@ class Parser {

write(data: Uint8Array) {
if (data.byteLength !== constants.BLOCK_SIZE) {
throw new errors.ErrorVirtualTarBlockSize(
throw new errors.ErrorTarParserBlockSize(
`Expected block size to be ${constants.BLOCK_SIZE} bytes but received ${data.byteLength} bytes`,
);
}

const view = new DataView(data.buffer, 0, constants.BLOCK_SIZE);

switch (this.state) {
case ParserState.ENDED: {
throw new errors.ErrorVirtualTarEndOfArchive(
throw new errors.ErrorTarParserEndOfArchive(
'Archive has already ended',
);
}

case ParserState.READY: {
// Check if we need to parse the end-of-archive marker
if (utils.checkNullView(view)) {
if (utils.isNullBlock(data)) {
this.state = ParserState.NULL;
return;
}

// Set relevant state if the header corresponds to a file
const headerToken = parseHeader(view);
const headerToken = parseHeader(data);
if (headerToken.fileType === 'file') {
this.state = ParserState.DATA;
this.remainingBytes = headerToken.fileSize;
Expand All @@ -114,18 +148,18 @@ class Parser {
}

case ParserState.DATA: {
const parsedData = parseData(view, this.remainingBytes);
const parsedData = parseData(data, this.remainingBytes);
this.remainingBytes -= 512;
if (this.remainingBytes < 0) this.state = ParserState.READY;
return parsedData;
}

case ParserState.NULL: {
if (utils.checkNullView(view)) {
if (utils.isNullBlock(data)) {
this.state = ParserState.ENDED;
return { type: 'end' } as EndToken;
} else {
throw new errors.ErrorVirtualTarEndOfArchive(
throw new errors.ErrorTarParserEndOfArchive(
'Received garbage data after first end marker',
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const BLOCK_SIZE = 512;
export const USTAR_NAME = 'ustar\0';
export const USTAR_NAME = 'ustar';
export const USTAR_VERSION = '00';
40 changes: 25 additions & 15 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
import { AbstractError } from '@matrixai/errors';

class ErrorVirtualTar<T> extends AbstractError<T> {
class ErrorTar<T> extends AbstractError<T> {
static description = 'VirtualTar errors';
}

class ErrorVirtualTarUndefinedBehaviour<T> extends ErrorVirtualTar<T> {
class ErrorVirtualTarUndefinedBehaviour<T> extends ErrorTar<T> {
static description = 'You should never see this error';
}

class ErrorVirtualTarInvalidFileName<T> extends ErrorVirtualTar<T> {
static description = 'The provided file name is invalid';
class ErrorTarGenerator<T> extends ErrorTar<T> {
static description = 'VirtualTar genereator errors';
}

class ErrorVirtualTarInvalidHeader<T> extends ErrorVirtualTar<T> {
static description = 'The header has invalid data';
class ErrorTarGeneratorInvalidFileName<T> extends ErrorTarGenerator<T> {
static description = 'The provided file name is invalid';
}

class ErrorVirtualTarInvalidStat<T> extends ErrorVirtualTar<T> {
class ErrorTarGeneratorInvalidStat<T> extends ErrorTarGenerator<T> {
static description = 'The stat contains invalid data';
}

class ErrorVirtualTarBlockSize<T> extends ErrorVirtualTar<T> {
class ErrorTarParser<T> extends ErrorTar<T> {
static description = 'VirtualTar parsing errors';
}

class ErrorTarParserInvalidHeader<T> extends ErrorTarParser<T> {
static description = 'The checksum did not match the header';
}

class ErrorTarParserBlockSize<T> extends ErrorTarParser<T> {
static description = 'The block size is incorrect';
}

class ErrorVirtualTarEndOfArchive<T> extends ErrorVirtualTar<T> {
class ErrorTarParserEndOfArchive<T> extends ErrorTarParser<T> {
static description = 'No data can come after an end-of-archive marker';
}

export {
ErrorVirtualTar,
ErrorTar,
ErrorTarGenerator,
ErrorVirtualTarUndefinedBehaviour,
ErrorVirtualTarInvalidFileName,
ErrorVirtualTarInvalidHeader,
ErrorVirtualTarInvalidStat,
ErrorVirtualTarBlockSize,
ErrorVirtualTarEndOfArchive,
ErrorTarGeneratorInvalidFileName,
ErrorTarGeneratorInvalidStat,
ErrorTarParser,
ErrorTarParserInvalidHeader,
ErrorTarParserBlockSize,
ErrorTarParserEndOfArchive,
};
Loading

0 comments on commit 76fe731

Please sign in to comment.