unpak.js
Version:
Modern TypeScript library for reading Unreal Engine pak files and assets, inspired by CUE4Parse
256 lines (201 loc) • 9.16 kB
text/typescript
import { IoStoreArchive } from '../src/containers/iostore/IoStoreArchive';
import { IoStoreParser, IoChunkId } from '../src/containers/iostore/IoStoreParser';
import { IoStoreTocVersion, IoContainerFlags, IOSTORE_CONSTANTS } from '../src/containers/iostore/IoStoreStructures';
import { KeyManager } from '../src/crypto/KeyManager';
import { BufferReader } from '../src/core/io/BufferReader';
import { logger, LogLevel } from '../src/core/logging/Logger';
import * as fs from 'fs/promises';
import * as path from 'path';
// Set log level to ERROR to reduce test noise
logger.setLevel(LogLevel.ERROR);
describe('IoStore', () => {
let tempDir: string;
let keyManager: KeyManager;
beforeEach(async () => {
// Create temp directory for test files
tempDir = path.join(__dirname, '../tmp/iostore-tests');
await fs.mkdir(tempDir, { recursive: true });
// Create key manager with test key
keyManager = new KeyManager();
await keyManager.submitKey(
'12345678-1234-1234-1234-123456789abc',
'0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF'
);
});
afterEach(async () => {
// Clean up temp files
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe('IoChunkId', () => {
it('should create and manipulate chunk IDs', () => {
const chunkId = new IoChunkId();
expect(chunkId.id).toBeInstanceOf(Buffer);
expect(chunkId.id.length).toBe(12);
// Test string representation
expect(typeof chunkId.toString()).toBe('string');
expect(chunkId.toString().length).toBe(24); // 12 bytes * 2 hex chars
});
it('should handle chunk type and index', () => {
const reader = new BufferReader(Buffer.alloc(12));
const chunkId = new IoChunkId(reader);
expect(typeof chunkId.chunkType).toBe('number');
expect(typeof chunkId.chunkIndex).toBe('number');
});
it('should compare chunk IDs', () => {
const chunkId1 = new IoChunkId();
const chunkId2 = new IoChunkId();
// Fill with different data
chunkId1.id.fill(0xAA);
chunkId2.id.fill(0xBB);
// Should be different
expect(chunkId1.equals(chunkId2)).toBe(false);
// Make them equal
chunkId2.id = Buffer.from(chunkId1.id);
expect(chunkId1.equals(chunkId2)).toBe(true);
});
it('should compute hash with seed', () => {
const chunkId = new IoChunkId();
const hash1 = chunkId.hashWithSeed(0);
const hash2 = chunkId.hashWithSeed(1234);
expect(typeof hash1).toBe('bigint');
expect(typeof hash2).toBe('bigint');
expect(hash1).not.toBe(hash2); // Different seeds should produce different hashes
});
});
describe('IoStoreParser', () => {
it('should handle invalid TOC magic', () => {
const invalidBuffer = Buffer.alloc(200);
invalidBuffer.write('invalid magic', 0);
expect(() => {
IoStoreParser.parseToc(invalidBuffer);
}).toThrow('Invalid IoStore TOC magic');
});
it('should create minimal valid TOC structure', () => {
// Create a minimal mock TOC buffer
const buffer = Buffer.alloc(200);
let offset = 0;
// Write TOC magic
buffer.write(IOSTORE_CONSTANTS.TOC_MAGIC, offset);
offset += 16;
// Write version
buffer.writeUInt8(IoStoreTocVersion.Latest, offset++);
// Write reserved fields
buffer.writeUInt8(0, offset++); // reserved0
buffer.writeUInt16LE(0, offset); offset += 2; // reserved1
// Write header size
buffer.writeUInt32LE(IOSTORE_CONSTANTS.TOC_HEADER_SIZE, offset); offset += 4;
// Write entry counts (all zeros for minimal test)
buffer.writeUInt32LE(0, offset); offset += 4; // tocEntryCount
buffer.writeUInt32LE(0, offset); offset += 4; // tocCompressedBlockEntryCount
// Write compressed block entry size
buffer.writeUInt32LE(IOSTORE_CONSTANTS.COMPRESSED_BLOCK_ENTRY_SIZE, offset); offset += 4;
// Fill rest with zeros (this is a minimal test)
// This should not throw but will parse successfully
expect(() => {
IoStoreParser.parseToc(buffer);
}).not.toThrow();
});
});
describe('IoStoreArchive', () => {
it('should create archive instance', () => {
const archive = new IoStoreArchive('/fake/path/test');
expect(archive).toBeInstanceOf(IoStoreArchive);
expect(archive.name).toBe('/fake/path/test');
});
it('should report initial state correctly', () => {
const archive = new IoStoreArchive('/fake/path/test', keyManager);
// Before initialization
expect(archive.isEncrypted).toBe(false);
expect(archive.fileCount).toBe(0);
expect(archive.getVersion()).toBe(0);
expect(archive.getMountPoint()).toBe('');
});
it('should handle UE version configuration', () => {
const ue4Archive = new IoStoreArchive('/fake/path/test', keyManager, 4);
const ue5Archive = new IoStoreArchive('/fake/path/test', keyManager, 5);
expect(ue4Archive).toBeInstanceOf(IoStoreArchive);
expect(ue5Archive).toBeInstanceOf(IoStoreArchive);
});
it('should handle file operations on uninitialized archive', async () => {
const archive = new IoStoreArchive('/fake/path/test');
expect(archive.hasFile('test.uasset')).toBe(false);
const file = await archive.getFile('test.uasset');
expect(file).toBeNull();
const info = archive.getFileInfo('test.uasset');
expect(info).toBeNull();
const files = archive.listFiles();
expect(Array.isArray(files)).toBe(true);
expect(files.length).toBe(0);
});
it('should handle pattern matching', () => {
const archive = new IoStoreArchive('/fake/path/test');
// Test pattern matching methods exist
const files = archive.listFiles('*.uasset');
expect(Array.isArray(files)).toBe(true);
const files2 = archive.listFiles('Content/**/*.umap');
expect(Array.isArray(files2)).toBe(true);
});
it('should handle close operation safely', async () => {
const archive = new IoStoreArchive('/fake/path/test');
// Should not throw even if not initialized
await expect(archive.close()).resolves.not.toThrow();
// Should be safe to call multiple times
await expect(archive.close()).resolves.not.toThrow();
});
});
describe('IoStore Constants', () => {
it('should have valid constants', () => {
expect(IOSTORE_CONSTANTS.TOC_MAGIC).toBe('-==--==--==--==-');
expect(IOSTORE_CONSTANTS.TOC_HEADER_SIZE).toBe(144);
expect(IOSTORE_CONSTANTS.COMPRESSED_BLOCK_ENTRY_SIZE).toBe(12);
expect(IOSTORE_CONSTANTS.AES_BLOCK_SIZE).toBe(16);
});
});
describe('IoStore Enums', () => {
it('should have valid TOC versions', () => {
expect(IoStoreTocVersion.Invalid).toBe(0);
expect(IoStoreTocVersion.Initial).toBe(1);
expect(IoStoreTocVersion.Latest).toBeGreaterThan(0);
});
it('should have valid container flags', () => {
expect(IoContainerFlags.None).toBe(0);
expect(IoContainerFlags.Compressed).toBe(1);
expect(IoContainerFlags.Encrypted).toBe(2);
expect(IoContainerFlags.Signed).toBe(4);
expect(IoContainerFlags.Indexed).toBe(8);
});
});
describe('Error Handling', () => {
it('should handle missing key for encrypted containers', async () => {
const archiveWithoutKeys = new IoStoreArchive('/fake/path/encrypted');
// Should create without throwing
expect(archiveWithoutKeys.isEncrypted).toBe(false);
});
it('should handle invalid paths gracefully', async () => {
const archive = new IoStoreArchive('/fake/path/test');
const result = await archive.getFile('');
expect(result).toBeNull();
const info = archive.getFileInfo('');
expect(info).toBeNull();
expect(archive.hasFile('')).toBe(false);
});
});
describe('Integration', () => {
it('should work with key manager integration', async () => {
const archive = new IoStoreArchive('/fake/path/test', keyManager);
// Should not throw when created with valid key manager
expect(archive).toBeInstanceOf(IoStoreArchive);
});
it('should handle both UE4 and UE5 chunk types', () => {
// This is more of a smoke test since we don't have real data
const ue4Archive = new IoStoreArchive('/fake/path/test', keyManager, 4);
const ue5Archive = new IoStoreArchive('/fake/path/test', keyManager, 5);
expect(ue4Archive).toBeInstanceOf(IoStoreArchive);
expect(ue5Archive).toBeInstanceOf(IoStoreArchive);
});
});
});