UNPKG

@chicowall/grf-loader

Version:

A loader for GRF files (Ragnarok Online game file)

290 lines (283 loc) 9.64 kB
import jDataview from 'jdataview'; interface TFileEntry { type: number; offset: number; realSize: number; compressedSize: number; lengthAligned: number; /** Raw filename bytes for re-decoding if needed */ rawNameBytes?: Uint8Array; } /** Supported filename encodings */ type FilenameEncoding = 'utf-8' | 'euc-kr' | 'cp949' | 'latin1' | 'auto'; /** GRF loader options */ interface GrfOptions { /** Encoding for filenames (default: 'auto') */ filenameEncoding?: FilenameEncoding; /** Threshold for auto-detection: if % of U+FFFD exceeds this, try Korean encodings (default: 0.01 = 1%) */ autoDetectThreshold?: number; /** Maximum uncompressed size per file in bytes (default: 256MB) */ maxFileUncompressedBytes?: number; /** Maximum total entries allowed (default: 500000) */ maxEntries?: number; } /** Search/find options */ interface FindOptions { /** Filter by file extension (without dot, e.g., 'spr', 'act') */ ext?: string; /** Filter by substring in path */ contains?: string; /** Filter by path ending */ endsWith?: string; /** Filter by regex pattern */ regex?: RegExp; /** Maximum results to return (default: unlimited) */ limit?: number; } /** Result of path resolution */ interface ResolveResult { status: 'found' | 'not_found' | 'ambiguous'; /** The exact matched path (if found) */ matchedPath?: string; /** All candidate paths (if ambiguous) */ candidates?: string[]; } /** GRF statistics */ interface GrfStats { /** Total file count */ fileCount: number; /** Number of filenames with replacement character (U+FFFD) */ badNameCount: number; /** Number of normalized key collisions */ collisionCount: number; /** Extension statistics: ext -> count */ extensionStats: Map<string, number>; /** Detected encoding used */ detectedEncoding: FilenameEncoding; } declare const GRF_ERROR_CODES: { readonly INVALID_MAGIC: "GRF_INVALID_MAGIC"; readonly UNSUPPORTED_VERSION: "GRF_UNSUPPORTED_VERSION"; readonly NOT_LOADED: "GRF_NOT_LOADED"; readonly FILE_NOT_FOUND: "GRF_FILE_NOT_FOUND"; readonly AMBIGUOUS_PATH: "GRF_AMBIGUOUS_PATH"; readonly DECOMPRESS_FAIL: "GRF_DECOMPRESS_FAIL"; readonly CORRUPT_TABLE: "GRF_CORRUPT_TABLE"; readonly LIMIT_EXCEEDED: "GRF_LIMIT_EXCEEDED"; readonly INVALID_OFFSET: "GRF_INVALID_OFFSET"; readonly DECRYPT_REQUIRED: "GRF_DECRYPT_REQUIRED"; }; declare class GrfError extends Error { code: keyof typeof GRF_ERROR_CODES; context?: Record<string, unknown> | undefined; constructor(code: keyof typeof GRF_ERROR_CODES, message: string, context?: Record<string, unknown> | undefined); } declare abstract class GrfBase<T> { private fd; version: number; fileCount: number; loaded: boolean; /** Map of exact filename -> entry */ files: Map<string, TFileEntry>; /** Map of normalized path -> array of exact filenames (supports collisions) */ private normalizedIndex; /** Map of extension -> array of exact filenames (for fast extension lookup) */ private extensionIndex; private fileTableOffset; private cache; private cacheMaxSize; private cacheOrder; protected options: Required<GrfOptions>; private _stats; constructor(fd: T, options?: GrfOptions); abstract getStreamBuffer(fd: T, offset: number, length: number): Promise<Uint8Array>; getStreamReader(offset: number, length: number): Promise<jDataview>; load(): Promise<void>; private parseHeader; private parseFileList; private decodeEntry; private addToCache; private getFromCache; clearCache(): void; getFile(filename: string): Promise<{ data: null | Uint8Array; error: null | string; }>; /** * Resolve a path to its exact filename in the GRF. * Tries exact match first, then normalized (case-insensitive, slash-agnostic). */ resolvePath(query: string): ResolveResult; /** * Check if a file exists in the GRF. */ hasFile(filename: string): boolean; /** * Get file entry metadata without extracting the file. */ getEntry(filename: string): TFileEntry | null; /** * Find files matching the given criteria. */ find(options?: FindOptions): string[]; /** * Get all files with a specific extension. */ getFilesByExtension(ext: string): string[]; /** * List all unique extensions in the GRF. */ listExtensions(): string[]; /** * List all files in the GRF. */ listFiles(): string[]; /** * Get GRF statistics. */ getStats(): GrfStats; /** * Get the detected/configured encoding used for filenames. */ getDetectedEncoding(): FilenameEncoding; /** * Re-decode all filenames with a different encoding. * Useful if auto-detection chose wrong or you want to try a specific encoding. */ reloadWithEncoding(encoding: FilenameEncoding): Promise<void>; } /** * Using this Browser, we work from a File or Blob object. * We are use the FileReader API to read only some part of the file to avoid * loading 2 gigas into memory */ declare class GrfBrowser extends GrfBase<File | Blob> { constructor(file: File | Blob, options?: GrfOptions); getStreamBuffer(buffer: File | Blob, offset: number, length: number): Promise<Uint8Array>; } /** Options for GrfNode */ interface GrfNodeOptions extends GrfOptions { /** Use buffer pool for better performance (default: true) */ useBufferPool?: boolean; } declare class GrfNode extends GrfBase<number> { private useBufferPool; constructor(fd: number, options?: GrfNodeOptions); getStreamBuffer(fd: number, offset: number, length: number): Promise<Uint8Array>; } /** * Simple buffer pool for reducing GC pressure * Pools buffers of common sizes for reuse */ declare class BufferPool { private pools; private maxPoolSize; private readonly poolSizes; constructor(); /** * Get appropriate pool size for requested length */ private getPoolSize; /** * Acquire a buffer from the pool or create new one */ acquire(length: number): Buffer; /** * Release a buffer back to the pool */ release(buffer: Buffer): void; /** * Clear all pools */ clear(): void; /** * Get pool statistics */ stats(): { size: number; total: number; inUse: number; }[]; } declare const bufferPool: BufferPool; /** * Korean encoding decoder module * * Uses iconv-lite in Node.js for proper CP949 support. * Falls back to TextDecoder in browser (with limitations for CP949 extended chars). */ /** * Check if we're in a Node.js environment with iconv-lite available */ declare function hasIconvLite(): boolean; /** * Count C1 control characters (U+0080-U+009F) in a string. * These usually indicate incorrectly decoded Korean bytes. * When EUC-KR decoder encounters CP949-extended bytes (0x80-0x9F range), * they get decoded as C1 control characters instead of Korean characters. */ declare function countC1ControlChars(str: string): number; /** * Count replacement characters (U+FFFD) in a string */ declare function countReplacementChars(str: string): number; /** * Count total "bad" characters (replacement + C1 control) */ declare function countBadChars(str: string): number; /** * Check if a string looks like mojibake (CP949 bytes misread as Windows-1252). * * Mojibake occurs when: * 1. Korean text is encoded as CP949 bytes * 2. Those bytes are incorrectly decoded as Windows-1252/Latin-1 * * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º" * * @param str - The string to check * @returns true if the string appears to be mojibake */ declare function isMojibake(str: string): boolean; /** * Fix mojibake by re-encoding as Windows-1252 and decoding as CP949. * * This reverses the common encoding error where CP949 bytes were * incorrectly interpreted as Windows-1252. * * Example: "À¯ÀúÀÎÅÍÆäÀ̽º" → "유저인터페이스" * * @param garbled - The mojibake string to fix * @returns The corrected Korean string, or the original if unfixable */ declare function fixMojibake(garbled: string): string; /** * Convert Korean text to mojibake (for testing purposes). * * This simulates the encoding error where Korean text is encoded as CP949 * but decoded as Windows-1252. * * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º" * * @param korean - The Korean string to garble * @returns The mojibake string */ declare function toMojibake(korean: string): string; /** * Normalize a filename by detecting and fixing encoding issues. * * This function: * 1. Checks if the filename is mojibake and fixes it * 2. Returns the normalized filename * * @param filename - The filename to normalize * @returns The normalized filename */ declare function normalizeFilename(filename: string): string; /** * Normalize a path by fixing mojibake in each segment. * * @param filepath - The full path to normalize * @returns The normalized path */ declare function normalizePath(filepath: string): string; export { type FilenameEncoding, type FindOptions, GRF_ERROR_CODES, GrfBrowser, GrfError, GrfNode, type GrfNodeOptions, type GrfOptions, type GrfStats, type ResolveResult, type TFileEntry, bufferPool, countBadChars, countC1ControlChars, countReplacementChars, fixMojibake, hasIconvLite, isMojibake, normalizePath as normalizeEncodingPath, normalizeFilename, toMojibake };