Skip to content

Commit

Permalink
chore: cleaned up tests
Browse files Browse the repository at this point in the history
  • Loading branch information
aryanjassal committed Feb 27, 2025
1 parent 4836b2a commit 00233d1
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 118 deletions.
18 changes: 0 additions & 18 deletions src/Parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,6 @@ function parseHeader(view: DataView): HeaderToken {
? 'file'
: 'directory';

// Const checksum = utils.extractBytes(view, HeaderOffset.CHECKSUM, HeaderSize.CHECKSUM);
// if (checksum.reduce((sum, byte) => sum + byte, 0) == 0) {
// console.log('Checksum null!')
// console.log({
// type: 'header',
// filePath,
// fileType,
// fileMode,
// fileMtime,
// fileSize,
// ownerGid,
// ownerUid,
// ownerName,
// ownerUserName,
// ownerGroupName,
// });
// }

return {
type: 'header',
filePath,
Expand Down
11 changes: 5 additions & 6 deletions tests/Parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { tarHeaderArb, tarDataArb } from './utils';
describe('archive parsing', () => {
test.prop([tarHeaderArb])(
'should parse headers with correct state',
({ header, type, details }) => {
const { fullPath, mode, uid, gid } = details;
({ header, stat }) => {
const { type, path, uid, gid } = stat;
const parser = new Parser();
const token = parser.write(header);

Expand All @@ -33,10 +33,9 @@ describe('archive parsing', () => {
tarUtils.never('Invalid state');
}

expect(token.filePath).toEqual(fullPath);
expect(token.fileMode).toEqual(parseInt(mode, 8));
expect(token.ownerUid).toEqual(parseInt(uid));
expect(token.ownerGid).toEqual(parseInt(gid));
expect(token.filePath).toEqual(path);
expect(token.ownerUid).toEqual(uid);
expect(token.ownerGid).toEqual(gid);
},
);

Expand Down
70 changes: 19 additions & 51 deletions tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,22 @@ describe('integration testing', () => {

const parser = new Parser();
const decoder = new TextDecoder();
const reconstructedVfs: any[] = [];
const reconstructedVfs: Array<FileType | DirectoryType> = [];
const pathStack: Map<string, any> = new Map();
let currentEntry: any = undefined;
let currentEntry: FileType;

for (const chunk of blocks) {
const token = parser.write(chunk);
if (token == null) continue;

switch (token.type) {
case 'header': {
let parsedEntry;
let parsedEntry: FileType | DirectoryType;

if (token.fileType === 'file') {
parsedEntry = {
type: EntryType.FILE,
path: token.filePath, // Path is already an array
path: token.filePath,
content: '',
stat: {
mode: token.fileMode,
Expand All @@ -105,73 +105,41 @@ describe('integration testing', () => {

const parentPath = path.dirname(token.filePath);

// If this entry is a directory, then it is pushed to the root of
// the reconstructed virtual file system and into a map at the same
// time. This allows us to add new children to the directory by
// looking up the path in a map rather than modifying the value in
// the reconstructed file system.

if (parentPath === '/' || parentPath === '.') {
// Root-level entry
reconstructedVfs.push(parsedEntry);
} else {
// Find or create the parent directory in reconstructedVfs
let parent = pathStack.get(parentPath);
if (!parent) {
parent = {
type: EntryType.DIRECTORY,
path: parentPath,
children: [],
stat: {
mode: 16877,
uid: 0,
gid: 0,
size: 0,
mtime: new Date(0),
},
};
reconstructedVfs.push(parent);
pathStack.set(parentPath, parent);
}

// It is guaranteed that in a valid tar file, the parent will
// always exist.
const parent: DirectoryType = pathStack.get(parentPath);
parent.children.push(parsedEntry);
}

// Track directories
if (token.fileType === 'directory') {
if (parsedEntry.type === EntryType.DIRECTORY) {
pathStack.set(token.filePath, parsedEntry);
} else {
currentEntry = parsedEntry;
// Type narrowing doesn't work well with manually specified types
currentEntry = parsedEntry as FileType;
}

break;
}

case 'data': {
if (currentEntry) {
currentEntry['content'] += decoder.decode(token.data);
}
// It is guaranteed that in a valid tar file, a data block will
// always come after a header block for a file.
currentEntry!['content'] += decoder.decode(token.data);
break;
}
}
}

// Console.log(printVfs(vfs));
// console.log(printVfs(reconstructedVfs));
expect(reconstructedVfs).toContainAllValues(vfs);
},
);
});

// Function printVfs(vfs: any[], prefix: string = '') {
// let output: string[] = [];
//
// const entriesCount = vfs.length;
// vfs.forEach((entry, index) => {
// const isLast = index === entriesCount - 1;
// const connector = isLast ? '└── ' : '├── ';
//
// output.push(`${prefix}${connector}${path.basename(entry.path)} ("${entry.content}")`);
//
// if (entry.type === EntryType.DIRECTORY && entry.children.length > 0) {
// const newPrefix = prefix + (isLast ? ' ' : '│ ');
// output.push(printVfs(entry.children, newPrefix));
// }
// });
//
// return output.join('\n'); // Join accumulated lines for clean logging
// }
96 changes: 53 additions & 43 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FileStat } from '@/types';
import { FileStat, HeaderOffset } from '@/types';
import fc from 'fast-check';
import { EntryType } from '@/types';
import * as tarUtils from '@/utils';
Expand Down Expand Up @@ -34,7 +34,7 @@ function splitHeaderData(data: Uint8Array) {
}

const filenameArb = fc
.string({ minLength: 1, maxLength: 255 })
.string({ minLength: 1, maxLength: 32 })
.filter((name) => !name.includes('/') && name !== '.' && name !== '..')
.noShrink();

Expand Down Expand Up @@ -116,74 +116,84 @@ const virtualFsArb = fc
.noShrink();

const tarHeaderArb = fc
.tuple(
fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined }), // Optional directory
fc.string({ minLength: 1, maxLength: 50 }), // Filename
fc.constant('0000777\0'), // Mode (octal)
fc.constant('0000000\0'), // UID (octal)
fc.constant('0000000\0'), // GID (octal)
fc.nat(65536), // File size (up to 1MB for files, 0 for directories)
fc.constant('0000000\0'), // Mtime (octal)
fc.constant(' '), // Placeholder checksum (8 spaces)
fc.constantFrom('0', '5'), // Typeflag ('0' for file, '5' for directory)
fc.constant(''.padEnd(100, '\0')), // USTAR format padding
)
.map(([dir, name, mode, uid, gid, size, , , typeflag]) => {
.record({
path: filenameArb,
uid: fc.nat(65535),
gid: fc.nat(65535),
size: fc.nat(65536),
typeflag: fc.constantFrom('0', '5'),
})
.map(({ path, uid, gid, size, typeflag }) => {
const header = new Uint8Array(tarConstants.BLOCK_SIZE);
const type = typeflag as '0' | '5';
const encoder = new TextEncoder();

if (type === '5') size = 0;

// If nested, prepend directory name
const fullPath = dir ? `${dir}/${name}` : name;

// Fill header fields
header.set(encoder.encode(fullPath), 0);
header.set(encoder.encode(mode), 100);
header.set(encoder.encode(uid), 108);
header.set(encoder.encode(gid), 116);
header.set(encoder.encode(size.toString(8).padStart(11, '0') + '\0'), 124);
header.set(encoder.encode('0000000\0'), 136); // Mtime
header.set(encoder.encode(' '), 148); // Checksum placeholder
header.set(encoder.encode(type), 156);
header.set(encoder.encode('ustar '), 257);
header.set(encoder.encode(path), HeaderOffset.FILE_NAME);
header.set(encoder.encode('0000777'), HeaderOffset.FILE_MODE);
header.set(
encoder.encode(uid.toString(8).padStart(7, '0')),
HeaderOffset.OWNER_UID,
);
header.set(
encoder.encode(gid.toString(8).padStart(7, '0')),
HeaderOffset.OWNER_GID,
);
header.set(
encoder.encode(size.toString(8).padStart(11, '0') + '\0'),
HeaderOffset.FILE_SIZE,
);
header.set(encoder.encode(' '), HeaderOffset.CHECKSUM);
header.set(encoder.encode(type), HeaderOffset.TYPE_FLAG);
header.set(encoder.encode('ustar '), HeaderOffset.USTAR_NAME);

// Compute and set checksum
const checksum = header.reduce((sum, byte) => sum + byte, 0);
header.set(
encoder.encode(checksum.toString(8).padStart(6, '0') + '\0 '),
148,
HeaderOffset.CHECKSUM,
);

return { header, size, type, details: { fullPath, mode, uid, gid } };
return { header, stat: { type, size, path, uid, gid } };
})
.noShrink();

const tarDataArb = tarHeaderArb
.chain((header) =>
fc
.tuple(
fc.constant(header),
fc.string({ minLength: header.size, maxLength: header.size }),
)
.map(([header, data]) => {
const { header: headerBlock, size, type } = header;
.record({
header: fc.constant(header),
data: fc.string({
minLength: header.stat.size,
maxLength: header.stat.size,
}),
})
.map(({ header, data }) => {
const { header: headerBlock, stat } = header;
const encoder = new TextEncoder();
const encodedData = encoder.encode(data);

// Directories don't have data
// Directories don't have any data, so set their size to zero.
let dataBlock: Uint8Array;
if (type === '0') {
const paddedSize =
Math.ceil(size / tarConstants.BLOCK_SIZE) * tarConstants.BLOCK_SIZE;
dataBlock = new Uint8Array(paddedSize);
dataBlock.set(encodedData.subarray(0, size)); // Subarray avoids unnecessary copy
if (stat.type === '0') {
// Make sure the data is aligned to 512-byte chunks
dataBlock = new Uint8Array(
Math.ceil(stat.size / tarConstants.BLOCK_SIZE) *
tarConstants.BLOCK_SIZE,
);
dataBlock.set(encodedData);
} else {
dataBlock = new Uint8Array(0); // Ensures consistent type
dataBlock = new Uint8Array(0);
}

return { header: headerBlock, data, encodedData: dataBlock, type };
return {
header: headerBlock,
data: data,
encodedData: dataBlock,
type: stat.type,
};
}),
)
.noShrink();
Expand Down

0 comments on commit 00233d1

Please sign in to comment.