@chicowall/grf-loader 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -6,25 +6,146 @@ interface TFileEntry {
6
6
  realSize: number;
7
7
  compressedSize: number;
8
8
  lengthAligned: number;
9
+ /** Raw filename bytes for re-decoding if needed */
10
+ rawNameBytes?: Uint8Array;
11
+ }
12
+ /** Supported filename encodings */
13
+ type FilenameEncoding = 'utf-8' | 'euc-kr' | 'cp949' | 'latin1' | 'auto';
14
+ /** GRF loader options */
15
+ interface GrfOptions {
16
+ /** Encoding for filenames (default: 'auto') */
17
+ filenameEncoding?: FilenameEncoding;
18
+ /** Threshold for auto-detection: if % of U+FFFD exceeds this, try Korean encodings (default: 0.01 = 1%) */
19
+ autoDetectThreshold?: number;
20
+ /** Maximum uncompressed size per file in bytes (default: 256MB) */
21
+ maxFileUncompressedBytes?: number;
22
+ /** Maximum total entries allowed (default: 500000) */
23
+ maxEntries?: number;
24
+ }
25
+ /** Search/find options */
26
+ interface FindOptions {
27
+ /** Filter by file extension (without dot, e.g., 'spr', 'act') */
28
+ ext?: string;
29
+ /** Filter by substring in path */
30
+ contains?: string;
31
+ /** Filter by path ending */
32
+ endsWith?: string;
33
+ /** Filter by regex pattern */
34
+ regex?: RegExp;
35
+ /** Maximum results to return (default: unlimited) */
36
+ limit?: number;
37
+ }
38
+ /** Result of path resolution */
39
+ interface ResolveResult {
40
+ status: 'found' | 'not_found' | 'ambiguous';
41
+ /** The exact matched path (if found) */
42
+ matchedPath?: string;
43
+ /** All candidate paths (if ambiguous) */
44
+ candidates?: string[];
45
+ }
46
+ /** GRF statistics */
47
+ interface GrfStats {
48
+ /** Total file count */
49
+ fileCount: number;
50
+ /** Number of filenames with replacement character (U+FFFD) */
51
+ badNameCount: number;
52
+ /** Number of normalized key collisions */
53
+ collisionCount: number;
54
+ /** Extension statistics: ext -> count */
55
+ extensionStats: Map<string, number>;
56
+ /** Detected encoding used */
57
+ detectedEncoding: FilenameEncoding;
58
+ }
59
+ declare const GRF_ERROR_CODES: {
60
+ readonly INVALID_MAGIC: "GRF_INVALID_MAGIC";
61
+ readonly UNSUPPORTED_VERSION: "GRF_UNSUPPORTED_VERSION";
62
+ readonly NOT_LOADED: "GRF_NOT_LOADED";
63
+ readonly FILE_NOT_FOUND: "GRF_FILE_NOT_FOUND";
64
+ readonly AMBIGUOUS_PATH: "GRF_AMBIGUOUS_PATH";
65
+ readonly DECOMPRESS_FAIL: "GRF_DECOMPRESS_FAIL";
66
+ readonly CORRUPT_TABLE: "GRF_CORRUPT_TABLE";
67
+ readonly LIMIT_EXCEEDED: "GRF_LIMIT_EXCEEDED";
68
+ readonly INVALID_OFFSET: "GRF_INVALID_OFFSET";
69
+ readonly DECRYPT_REQUIRED: "GRF_DECRYPT_REQUIRED";
70
+ };
71
+ declare class GrfError extends Error {
72
+ code: keyof typeof GRF_ERROR_CODES;
73
+ context?: Record<string, unknown> | undefined;
74
+ constructor(code: keyof typeof GRF_ERROR_CODES, message: string, context?: Record<string, unknown> | undefined);
9
75
  }
10
76
  declare abstract class GrfBase<T> {
11
77
  private fd;
12
78
  version: number;
13
79
  fileCount: number;
14
80
  loaded: boolean;
81
+ /** Map of exact filename -> entry */
15
82
  files: Map<string, TFileEntry>;
83
+ /** Map of normalized path -> array of exact filenames (supports collisions) */
84
+ private normalizedIndex;
85
+ /** Map of extension -> array of exact filenames (for fast extension lookup) */
86
+ private extensionIndex;
16
87
  private fileTableOffset;
17
- constructor(fd: T);
88
+ private cache;
89
+ private cacheMaxSize;
90
+ private cacheOrder;
91
+ protected options: Required<GrfOptions>;
92
+ private _stats;
93
+ constructor(fd: T, options?: GrfOptions);
18
94
  abstract getStreamBuffer(fd: T, offset: number, length: number): Promise<Uint8Array>;
19
95
  getStreamReader(offset: number, length: number): Promise<jDataview>;
20
96
  load(): Promise<void>;
21
97
  private parseHeader;
22
98
  private parseFileList;
23
99
  private decodeEntry;
100
+ private addToCache;
101
+ private getFromCache;
102
+ clearCache(): void;
24
103
  getFile(filename: string): Promise<{
25
104
  data: null | Uint8Array;
26
105
  error: null | string;
27
106
  }>;
107
+ /**
108
+ * Resolve a path to its exact filename in the GRF.
109
+ * Tries exact match first, then normalized (case-insensitive, slash-agnostic).
110
+ */
111
+ resolvePath(query: string): ResolveResult;
112
+ /**
113
+ * Check if a file exists in the GRF.
114
+ */
115
+ hasFile(filename: string): boolean;
116
+ /**
117
+ * Get file entry metadata without extracting the file.
118
+ */
119
+ getEntry(filename: string): TFileEntry | null;
120
+ /**
121
+ * Find files matching the given criteria.
122
+ */
123
+ find(options?: FindOptions): string[];
124
+ /**
125
+ * Get all files with a specific extension.
126
+ */
127
+ getFilesByExtension(ext: string): string[];
128
+ /**
129
+ * List all unique extensions in the GRF.
130
+ */
131
+ listExtensions(): string[];
132
+ /**
133
+ * List all files in the GRF.
134
+ */
135
+ listFiles(): string[];
136
+ /**
137
+ * Get GRF statistics.
138
+ */
139
+ getStats(): GrfStats;
140
+ /**
141
+ * Get the detected/configured encoding used for filenames.
142
+ */
143
+ getDetectedEncoding(): FilenameEncoding;
144
+ /**
145
+ * Re-decode all filenames with a different encoding.
146
+ * Useful if auto-detection chose wrong or you want to try a specific encoding.
147
+ */
148
+ reloadWithEncoding(encoding: FilenameEncoding): Promise<void>;
28
149
  }
29
150
 
30
151
  /**
@@ -33,12 +154,136 @@ declare abstract class GrfBase<T> {
33
154
  * loading 2 gigas into memory
34
155
  */
35
156
  declare class GrfBrowser extends GrfBase<File | Blob> {
157
+ constructor(file: File | Blob, options?: GrfOptions);
36
158
  getStreamBuffer(buffer: File | Blob, offset: number, length: number): Promise<Uint8Array>;
37
159
  }
38
160
 
161
+ /** Options for GrfNode */
162
+ interface GrfNodeOptions extends GrfOptions {
163
+ /** Use buffer pool for better performance (default: true) */
164
+ useBufferPool?: boolean;
165
+ }
39
166
  declare class GrfNode extends GrfBase<number> {
40
- constructor(fd: number);
167
+ private useBufferPool;
168
+ constructor(fd: number, options?: GrfNodeOptions);
41
169
  getStreamBuffer(fd: number, offset: number, length: number): Promise<Uint8Array>;
42
170
  }
43
171
 
44
- export { GrfBrowser, GrfNode, type TFileEntry };
172
+ /**
173
+ * Simple buffer pool for reducing GC pressure
174
+ * Pools buffers of common sizes for reuse
175
+ */
176
+ declare class BufferPool {
177
+ private pools;
178
+ private maxPoolSize;
179
+ private readonly poolSizes;
180
+ constructor();
181
+ /**
182
+ * Get appropriate pool size for requested length
183
+ */
184
+ private getPoolSize;
185
+ /**
186
+ * Acquire a buffer from the pool or create new one
187
+ */
188
+ acquire(length: number): Buffer;
189
+ /**
190
+ * Release a buffer back to the pool
191
+ */
192
+ release(buffer: Buffer): void;
193
+ /**
194
+ * Clear all pools
195
+ */
196
+ clear(): void;
197
+ /**
198
+ * Get pool statistics
199
+ */
200
+ stats(): {
201
+ size: number;
202
+ total: number;
203
+ inUse: number;
204
+ }[];
205
+ }
206
+ declare const bufferPool: BufferPool;
207
+
208
+ /**
209
+ * Korean encoding decoder module
210
+ *
211
+ * Uses iconv-lite in Node.js for proper CP949 support.
212
+ * Falls back to TextDecoder in browser (with limitations for CP949 extended chars).
213
+ */
214
+ /**
215
+ * Check if we're in a Node.js environment with iconv-lite available
216
+ */
217
+ declare function hasIconvLite(): boolean;
218
+ /**
219
+ * Count C1 control characters (U+0080-U+009F) in a string.
220
+ * These usually indicate incorrectly decoded Korean bytes.
221
+ * When EUC-KR decoder encounters CP949-extended bytes (0x80-0x9F range),
222
+ * they get decoded as C1 control characters instead of Korean characters.
223
+ */
224
+ declare function countC1ControlChars(str: string): number;
225
+ /**
226
+ * Count replacement characters (U+FFFD) in a string
227
+ */
228
+ declare function countReplacementChars(str: string): number;
229
+ /**
230
+ * Count total "bad" characters (replacement + C1 control)
231
+ */
232
+ declare function countBadChars(str: string): number;
233
+ /**
234
+ * Check if a string looks like mojibake (CP949 bytes misread as Windows-1252).
235
+ *
236
+ * Mojibake occurs when:
237
+ * 1. Korean text is encoded as CP949 bytes
238
+ * 2. Those bytes are incorrectly decoded as Windows-1252/Latin-1
239
+ *
240
+ * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º"
241
+ *
242
+ * @param str - The string to check
243
+ * @returns true if the string appears to be mojibake
244
+ */
245
+ declare function isMojibake(str: string): boolean;
246
+ /**
247
+ * Fix mojibake by re-encoding as Windows-1252 and decoding as CP949.
248
+ *
249
+ * This reverses the common encoding error where CP949 bytes were
250
+ * incorrectly interpreted as Windows-1252.
251
+ *
252
+ * Example: "À¯ÀúÀÎÅÍÆäÀ̽º" → "유저인터페이스"
253
+ *
254
+ * @param garbled - The mojibake string to fix
255
+ * @returns The corrected Korean string, or the original if unfixable
256
+ */
257
+ declare function fixMojibake(garbled: string): string;
258
+ /**
259
+ * Convert Korean text to mojibake (for testing purposes).
260
+ *
261
+ * This simulates the encoding error where Korean text is encoded as CP949
262
+ * but decoded as Windows-1252.
263
+ *
264
+ * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º"
265
+ *
266
+ * @param korean - The Korean string to garble
267
+ * @returns The mojibake string
268
+ */
269
+ declare function toMojibake(korean: string): string;
270
+ /**
271
+ * Normalize a filename by detecting and fixing encoding issues.
272
+ *
273
+ * This function:
274
+ * 1. Checks if the filename is mojibake and fixes it
275
+ * 2. Returns the normalized filename
276
+ *
277
+ * @param filename - The filename to normalize
278
+ * @returns The normalized filename
279
+ */
280
+ declare function normalizeFilename(filename: string): string;
281
+ /**
282
+ * Normalize a path by fixing mojibake in each segment.
283
+ *
284
+ * @param filepath - The full path to normalize
285
+ * @returns The normalized path
286
+ */
287
+ declare function normalizePath(filepath: string): string;
288
+
289
+ 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 };
package/dist/index.d.ts CHANGED
@@ -6,25 +6,146 @@ interface TFileEntry {
6
6
  realSize: number;
7
7
  compressedSize: number;
8
8
  lengthAligned: number;
9
+ /** Raw filename bytes for re-decoding if needed */
10
+ rawNameBytes?: Uint8Array;
11
+ }
12
+ /** Supported filename encodings */
13
+ type FilenameEncoding = 'utf-8' | 'euc-kr' | 'cp949' | 'latin1' | 'auto';
14
+ /** GRF loader options */
15
+ interface GrfOptions {
16
+ /** Encoding for filenames (default: 'auto') */
17
+ filenameEncoding?: FilenameEncoding;
18
+ /** Threshold for auto-detection: if % of U+FFFD exceeds this, try Korean encodings (default: 0.01 = 1%) */
19
+ autoDetectThreshold?: number;
20
+ /** Maximum uncompressed size per file in bytes (default: 256MB) */
21
+ maxFileUncompressedBytes?: number;
22
+ /** Maximum total entries allowed (default: 500000) */
23
+ maxEntries?: number;
24
+ }
25
+ /** Search/find options */
26
+ interface FindOptions {
27
+ /** Filter by file extension (without dot, e.g., 'spr', 'act') */
28
+ ext?: string;
29
+ /** Filter by substring in path */
30
+ contains?: string;
31
+ /** Filter by path ending */
32
+ endsWith?: string;
33
+ /** Filter by regex pattern */
34
+ regex?: RegExp;
35
+ /** Maximum results to return (default: unlimited) */
36
+ limit?: number;
37
+ }
38
+ /** Result of path resolution */
39
+ interface ResolveResult {
40
+ status: 'found' | 'not_found' | 'ambiguous';
41
+ /** The exact matched path (if found) */
42
+ matchedPath?: string;
43
+ /** All candidate paths (if ambiguous) */
44
+ candidates?: string[];
45
+ }
46
+ /** GRF statistics */
47
+ interface GrfStats {
48
+ /** Total file count */
49
+ fileCount: number;
50
+ /** Number of filenames with replacement character (U+FFFD) */
51
+ badNameCount: number;
52
+ /** Number of normalized key collisions */
53
+ collisionCount: number;
54
+ /** Extension statistics: ext -> count */
55
+ extensionStats: Map<string, number>;
56
+ /** Detected encoding used */
57
+ detectedEncoding: FilenameEncoding;
58
+ }
59
+ declare const GRF_ERROR_CODES: {
60
+ readonly INVALID_MAGIC: "GRF_INVALID_MAGIC";
61
+ readonly UNSUPPORTED_VERSION: "GRF_UNSUPPORTED_VERSION";
62
+ readonly NOT_LOADED: "GRF_NOT_LOADED";
63
+ readonly FILE_NOT_FOUND: "GRF_FILE_NOT_FOUND";
64
+ readonly AMBIGUOUS_PATH: "GRF_AMBIGUOUS_PATH";
65
+ readonly DECOMPRESS_FAIL: "GRF_DECOMPRESS_FAIL";
66
+ readonly CORRUPT_TABLE: "GRF_CORRUPT_TABLE";
67
+ readonly LIMIT_EXCEEDED: "GRF_LIMIT_EXCEEDED";
68
+ readonly INVALID_OFFSET: "GRF_INVALID_OFFSET";
69
+ readonly DECRYPT_REQUIRED: "GRF_DECRYPT_REQUIRED";
70
+ };
71
+ declare class GrfError extends Error {
72
+ code: keyof typeof GRF_ERROR_CODES;
73
+ context?: Record<string, unknown> | undefined;
74
+ constructor(code: keyof typeof GRF_ERROR_CODES, message: string, context?: Record<string, unknown> | undefined);
9
75
  }
10
76
  declare abstract class GrfBase<T> {
11
77
  private fd;
12
78
  version: number;
13
79
  fileCount: number;
14
80
  loaded: boolean;
81
+ /** Map of exact filename -> entry */
15
82
  files: Map<string, TFileEntry>;
83
+ /** Map of normalized path -> array of exact filenames (supports collisions) */
84
+ private normalizedIndex;
85
+ /** Map of extension -> array of exact filenames (for fast extension lookup) */
86
+ private extensionIndex;
16
87
  private fileTableOffset;
17
- constructor(fd: T);
88
+ private cache;
89
+ private cacheMaxSize;
90
+ private cacheOrder;
91
+ protected options: Required<GrfOptions>;
92
+ private _stats;
93
+ constructor(fd: T, options?: GrfOptions);
18
94
  abstract getStreamBuffer(fd: T, offset: number, length: number): Promise<Uint8Array>;
19
95
  getStreamReader(offset: number, length: number): Promise<jDataview>;
20
96
  load(): Promise<void>;
21
97
  private parseHeader;
22
98
  private parseFileList;
23
99
  private decodeEntry;
100
+ private addToCache;
101
+ private getFromCache;
102
+ clearCache(): void;
24
103
  getFile(filename: string): Promise<{
25
104
  data: null | Uint8Array;
26
105
  error: null | string;
27
106
  }>;
107
+ /**
108
+ * Resolve a path to its exact filename in the GRF.
109
+ * Tries exact match first, then normalized (case-insensitive, slash-agnostic).
110
+ */
111
+ resolvePath(query: string): ResolveResult;
112
+ /**
113
+ * Check if a file exists in the GRF.
114
+ */
115
+ hasFile(filename: string): boolean;
116
+ /**
117
+ * Get file entry metadata without extracting the file.
118
+ */
119
+ getEntry(filename: string): TFileEntry | null;
120
+ /**
121
+ * Find files matching the given criteria.
122
+ */
123
+ find(options?: FindOptions): string[];
124
+ /**
125
+ * Get all files with a specific extension.
126
+ */
127
+ getFilesByExtension(ext: string): string[];
128
+ /**
129
+ * List all unique extensions in the GRF.
130
+ */
131
+ listExtensions(): string[];
132
+ /**
133
+ * List all files in the GRF.
134
+ */
135
+ listFiles(): string[];
136
+ /**
137
+ * Get GRF statistics.
138
+ */
139
+ getStats(): GrfStats;
140
+ /**
141
+ * Get the detected/configured encoding used for filenames.
142
+ */
143
+ getDetectedEncoding(): FilenameEncoding;
144
+ /**
145
+ * Re-decode all filenames with a different encoding.
146
+ * Useful if auto-detection chose wrong or you want to try a specific encoding.
147
+ */
148
+ reloadWithEncoding(encoding: FilenameEncoding): Promise<void>;
28
149
  }
29
150
 
30
151
  /**
@@ -33,12 +154,136 @@ declare abstract class GrfBase<T> {
33
154
  * loading 2 gigas into memory
34
155
  */
35
156
  declare class GrfBrowser extends GrfBase<File | Blob> {
157
+ constructor(file: File | Blob, options?: GrfOptions);
36
158
  getStreamBuffer(buffer: File | Blob, offset: number, length: number): Promise<Uint8Array>;
37
159
  }
38
160
 
161
+ /** Options for GrfNode */
162
+ interface GrfNodeOptions extends GrfOptions {
163
+ /** Use buffer pool for better performance (default: true) */
164
+ useBufferPool?: boolean;
165
+ }
39
166
  declare class GrfNode extends GrfBase<number> {
40
- constructor(fd: number);
167
+ private useBufferPool;
168
+ constructor(fd: number, options?: GrfNodeOptions);
41
169
  getStreamBuffer(fd: number, offset: number, length: number): Promise<Uint8Array>;
42
170
  }
43
171
 
44
- export { GrfBrowser, GrfNode, type TFileEntry };
172
+ /**
173
+ * Simple buffer pool for reducing GC pressure
174
+ * Pools buffers of common sizes for reuse
175
+ */
176
+ declare class BufferPool {
177
+ private pools;
178
+ private maxPoolSize;
179
+ private readonly poolSizes;
180
+ constructor();
181
+ /**
182
+ * Get appropriate pool size for requested length
183
+ */
184
+ private getPoolSize;
185
+ /**
186
+ * Acquire a buffer from the pool or create new one
187
+ */
188
+ acquire(length: number): Buffer;
189
+ /**
190
+ * Release a buffer back to the pool
191
+ */
192
+ release(buffer: Buffer): void;
193
+ /**
194
+ * Clear all pools
195
+ */
196
+ clear(): void;
197
+ /**
198
+ * Get pool statistics
199
+ */
200
+ stats(): {
201
+ size: number;
202
+ total: number;
203
+ inUse: number;
204
+ }[];
205
+ }
206
+ declare const bufferPool: BufferPool;
207
+
208
+ /**
209
+ * Korean encoding decoder module
210
+ *
211
+ * Uses iconv-lite in Node.js for proper CP949 support.
212
+ * Falls back to TextDecoder in browser (with limitations for CP949 extended chars).
213
+ */
214
+ /**
215
+ * Check if we're in a Node.js environment with iconv-lite available
216
+ */
217
+ declare function hasIconvLite(): boolean;
218
+ /**
219
+ * Count C1 control characters (U+0080-U+009F) in a string.
220
+ * These usually indicate incorrectly decoded Korean bytes.
221
+ * When EUC-KR decoder encounters CP949-extended bytes (0x80-0x9F range),
222
+ * they get decoded as C1 control characters instead of Korean characters.
223
+ */
224
+ declare function countC1ControlChars(str: string): number;
225
+ /**
226
+ * Count replacement characters (U+FFFD) in a string
227
+ */
228
+ declare function countReplacementChars(str: string): number;
229
+ /**
230
+ * Count total "bad" characters (replacement + C1 control)
231
+ */
232
+ declare function countBadChars(str: string): number;
233
+ /**
234
+ * Check if a string looks like mojibake (CP949 bytes misread as Windows-1252).
235
+ *
236
+ * Mojibake occurs when:
237
+ * 1. Korean text is encoded as CP949 bytes
238
+ * 2. Those bytes are incorrectly decoded as Windows-1252/Latin-1
239
+ *
240
+ * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º"
241
+ *
242
+ * @param str - The string to check
243
+ * @returns true if the string appears to be mojibake
244
+ */
245
+ declare function isMojibake(str: string): boolean;
246
+ /**
247
+ * Fix mojibake by re-encoding as Windows-1252 and decoding as CP949.
248
+ *
249
+ * This reverses the common encoding error where CP949 bytes were
250
+ * incorrectly interpreted as Windows-1252.
251
+ *
252
+ * Example: "À¯ÀúÀÎÅÍÆäÀ̽º" → "유저인터페이스"
253
+ *
254
+ * @param garbled - The mojibake string to fix
255
+ * @returns The corrected Korean string, or the original if unfixable
256
+ */
257
+ declare function fixMojibake(garbled: string): string;
258
+ /**
259
+ * Convert Korean text to mojibake (for testing purposes).
260
+ *
261
+ * This simulates the encoding error where Korean text is encoded as CP949
262
+ * but decoded as Windows-1252.
263
+ *
264
+ * Example: "유저인터페이스" → "À¯ÀúÀÎÅÍÆäÀ̽º"
265
+ *
266
+ * @param korean - The Korean string to garble
267
+ * @returns The mojibake string
268
+ */
269
+ declare function toMojibake(korean: string): string;
270
+ /**
271
+ * Normalize a filename by detecting and fixing encoding issues.
272
+ *
273
+ * This function:
274
+ * 1. Checks if the filename is mojibake and fixes it
275
+ * 2. Returns the normalized filename
276
+ *
277
+ * @param filename - The filename to normalize
278
+ * @returns The normalized filename
279
+ */
280
+ declare function normalizeFilename(filename: string): string;
281
+ /**
282
+ * Normalize a path by fixing mojibake in each segment.
283
+ *
284
+ * @param filepath - The full path to normalize
285
+ * @returns The normalized path
286
+ */
287
+ declare function normalizePath(filepath: string): string;
288
+
289
+ 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 };