@chicowall/grf-loader 1.0.12 → 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/README.md CHANGED
@@ -1,85 +1,296 @@
1
1
  # GRF Loader
2
2
 
3
- **GRF** is an archive file format that support lossless data compression used on **Ragnarok Online** to store game assets. A GRF file may contain one or more files or directories that may have been compressed (deflate) and encrypted (variant of DES).
3
+ **GRF** is an archive file format that supports lossless data compression used on **Ragnarok Online** to store game assets. A GRF file may contain one or more files or directories that may have been compressed (deflate) and encrypted (variant of DES).
4
4
 
5
5
  [![roBrowser project](https://img.shields.io/badge/project-roBrowser-informational.svg)](https://github.com/vthibault/roBrowser) [![license: MIT](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
6
6
  ![node](https://github.com/vthibault/grf-loader/workflows/node/badge.svg?branch=master) ![browser](https://github.com/vthibault/grf-loader/workflows/browser/badge.svg?branch=master) ![lint](https://github.com/vthibault/grf-loader/workflows/lint/badge.svg?branch=master)
7
7
 
8
- ## About
8
+ ## Features
9
9
 
10
- - Only supports GRF version 0x200.
11
- - It's working both on node and browser environments
12
- - Supports DES description.
13
- - Avoid bloating client/server memory _(by not loading the whole file into the RAM)_
14
- - Does not supports custom encryption
10
+ - GRF version 0x200 support
11
+ - Works in both Node.js and browser environments
12
+ - DES decryption support
13
+ - **Korean filename encoding (CP949/EUC-KR)** with auto-detection
14
+ - **Mojibake detection and fixing**
15
+ - ✅ **Case-insensitive path resolution**
16
+ - ✅ **Collision-safe indexing** (no lost files)
17
+ - ✅ Memory efficient (streams data without loading entire file)
18
+ - ❌ Custom encryption not supported
15
19
 
16
20
  ## Installation
17
21
 
18
- ```
22
+ ```bash
19
23
  npm install @chicowall/grf-loader
20
24
  ```
21
25
 
22
- ## Basic usage
23
-
24
- - Load a grf file on node.js
25
- - Load a grf from the browser
26
- - List all files content
27
- - Extract a file from the GRF
26
+ ## Quick Start
28
27
 
29
- ### Load a grf file on node.js
28
+ ### Node.js
30
29
 
31
30
  ```ts
32
- import {GrfNode} from 'grf-loader';
33
- import {openSync} from 'fs';
31
+ import { GrfNode } from '@chicowall/grf-loader';
32
+ import { openSync } from 'fs';
34
33
 
35
34
  const fd = openSync('path/to/data.grf', 'r');
36
35
  const grf = new GrfNode(fd);
37
36
 
38
- // Start parsing the grf.
39
37
  await grf.load();
38
+
39
+ // Get file
40
+ const { data, error } = await grf.getFile('data\\sprite\\monster.spr');
40
41
  ```
41
42
 
42
- ### Load a grf from the browser
43
+ ### Browser
43
44
 
44
45
  ```ts
45
- import {GrfBrowser} from 'grf-loader';
46
+ import { GrfBrowser } from '@chicowall/grf-loader';
46
47
 
47
- const blob = document.querySelector('input[type="file"]').files[0];
48
- const grf = new GrfBrowser(blob);
48
+ const file = document.querySelector('input[type="file"]').files[0];
49
+ const grf = new GrfBrowser(file);
49
50
 
50
- // Start parsing the grf
51
51
  await grf.load();
52
52
  ```
53
53
 
54
- ### List all files content
54
+ ## Configuration Options
55
+
56
+ ```ts
57
+ const grf = new GrfNode(fd, {
58
+ // Filename encoding: 'auto' | 'cp949' | 'euc-kr' | 'utf-8' | 'latin1'
59
+ filenameEncoding: 'auto',
60
+
61
+ // Auto-detection threshold for bad characters (default: 1%)
62
+ autoDetectThreshold: 0.01,
63
+
64
+ // Maximum uncompressed file size (default: 256MB)
65
+ maxFileUncompressedBytes: 256 * 1024 * 1024,
66
+
67
+ // Maximum entries allowed (default: 500,000)
68
+ maxEntries: 500000
69
+ });
70
+ ```
71
+
72
+ ## API Reference
73
+
74
+ ### File Operations
75
+
76
+ ```ts
77
+ // Get file data
78
+ const { data, error } = await grf.getFile('data\\clientinfo.xml');
79
+
80
+ // Check if file exists (case-insensitive)
81
+ grf.hasFile('DATA\\CLIENTINFO.XML'); // true
82
+
83
+ // Get file entry metadata
84
+ const entry = grf.getEntry('data\\clientinfo.xml');
85
+ // { type, offset, realSize, compressedSize, lengthAligned, rawNameBytes }
86
+
87
+ // Resolve path (handles case-insensitivity and collisions)
88
+ const result = grf.resolvePath('DATA\\Sprite\\Test.spr');
89
+ // { status: 'found' | 'not_found' | 'ambiguous', matchedPath?, candidates? }
90
+ ```
55
91
 
56
- Once the GRF is loaded, it's possible to list all files included inside it
92
+ ### Search API
57
93
 
58
94
  ```ts
59
- grf.files.forEach((entry, path) => {
60
- console.log(path);
95
+ // Find files with multiple filters
96
+ const files = grf.find({
97
+ ext: 'spr', // Filter by extension
98
+ contains: 'monster', // Filter by substring (case-insensitive)
99
+ endsWith: 'poring.spr', // Filter by path ending
100
+ regex: /^data\\sprite/, // Filter by regex
101
+ limit: 100 // Max results
61
102
  });
103
+
104
+ // Get all files by extension (fast, uses index)
105
+ const sprites = grf.getFilesByExtension('spr');
106
+ const textures = grf.getFilesByExtension('bmp');
107
+
108
+ // List all unique extensions
109
+ const extensions = grf.listExtensions();
110
+ // ['spr', 'act', 'bmp', 'wav', ...]
111
+
112
+ // List all files
113
+ const allFiles = grf.listFiles();
114
+ ```
115
+
116
+ ### Statistics
117
+
118
+ ```ts
119
+ const stats = grf.getStats();
120
+ // {
121
+ // fileCount: 203092,
122
+ // badNameCount: 4, // Files with encoding issues
123
+ // collisionCount: 0, // Normalized path collisions
124
+ // extensionStats: Map, // Extension -> count
125
+ // detectedEncoding: 'cp949'
126
+ // }
127
+
128
+ // Get detected encoding
129
+ const encoding = grf.getDetectedEncoding(); // 'cp949' | 'utf-8' | ...
130
+ ```
131
+
132
+ ### Encoding Utilities
133
+
134
+ ```ts
135
+ import {
136
+ isMojibake,
137
+ fixMojibake,
138
+ normalizeFilename,
139
+ normalizeEncodingPath,
140
+ countBadChars,
141
+ hasIconvLite
142
+ } from '@chicowall/grf-loader';
143
+
144
+ // Detect mojibake (CP949 misread as Windows-1252)
145
+ isMojibake('À¯ÀúÀÎÅÍÆäÀ̽º'); // true
146
+ isMojibake('유저인터페이스'); // false
147
+
148
+ // Fix mojibake
149
+ fixMojibake('À¯ÀúÀÎÅÍÆäÀ̽º'); // '유저인터페이스'
150
+
151
+ // Normalize entire path
152
+ normalizeEncodingPath('data\\texture\\À¯ÀúÀÎÅÍÆäÀ̽º\\test.bmp');
153
+ // 'data\\texture\\유저인터페이스\\test.bmp'
154
+
155
+ // Count problematic characters
156
+ countBadChars('test�file.txt'); // 1 (U+FFFD replacement char)
157
+
158
+ // Check if iconv-lite is available (Node.js only)
159
+ hasIconvLite(); // true in Node.js, false in browser
160
+ ```
161
+
162
+ ## Korean Encoding Support
163
+
164
+ GRF files from Korean Ragnarok Online clients use CP949 encoding for filenames. This library automatically detects and handles Korean encoding:
165
+
166
+ ```ts
167
+ // Auto-detection (default)
168
+ const grf = new GrfNode(fd, { filenameEncoding: 'auto' });
169
+
170
+ // Force CP949
171
+ const grf = new GrfNode(fd, { filenameEncoding: 'cp949' });
172
+
173
+ // Reload with different encoding
174
+ await grf.reloadWithEncoding('euc-kr');
62
175
  ```
63
176
 
64
- ### Extract a file from the GRF
177
+ ### Encoding Detection Results
178
+
179
+ | Scenario | Detection | Result |
180
+ |----------|-----------|--------|
181
+ | Korean GRF | `cp949` | ✅ Proper Korean display |
182
+ | English GRF | `utf-8` | ✅ ASCII preserved |
183
+ | Mixed content | `cp949` | ✅ Both work |
65
184
 
66
- Once the GRF is loaded, it's possible to extract all files you need
185
+ ## Error Handling
67
186
 
68
187
  ```ts
69
- const {data, error} = await grf.getFile('data\\clientinfo.xml');
188
+ import { GrfError, GRF_ERROR_CODES } from '@chicowall/grf-loader';
70
189
 
71
- // data is a Uint8Array data, so we transform it into text
72
- const content = String.fromCharCode.apply(null, data);
73
- console.log(content);
190
+ try {
191
+ await grf.load();
192
+ } catch (e) {
193
+ if (e instanceof GrfError) {
194
+ switch (e.code) {
195
+ case 'INVALID_MAGIC':
196
+ console.log('Not a GRF file');
197
+ break;
198
+ case 'UNSUPPORTED_VERSION':
199
+ console.log('Only version 0x200 supported');
200
+ break;
201
+ case 'CORRUPT_TABLE':
202
+ console.log('File table is corrupted');
203
+ break;
204
+ case 'LIMIT_EXCEEDED':
205
+ console.log('File exceeds size limit');
206
+ break;
207
+ }
208
+ }
209
+ }
74
210
  ```
75
211
 
76
- ### Extract all files
212
+ ### Error Codes
213
+
214
+ | Code | Description |
215
+ |------|-------------|
216
+ | `INVALID_MAGIC` | File is not a GRF (invalid signature) |
217
+ | `UNSUPPORTED_VERSION` | GRF version not 0x200 |
218
+ | `NOT_LOADED` | GRF not loaded yet |
219
+ | `FILE_NOT_FOUND` | Requested file not in archive |
220
+ | `AMBIGUOUS_PATH` | Multiple files match (collision) |
221
+ | `DECOMPRESS_FAIL` | Decompression failed |
222
+ | `CORRUPT_TABLE` | File table is corrupted |
223
+ | `LIMIT_EXCEEDED` | Size/count limit exceeded |
77
224
 
78
- A sample script is provided in `examples/extract-all.ts` to dump every file from a GRF.
79
- Run it with [ts-node](https://typestrong.org/ts-node/) passing the GRF path and an optional output directory:
225
+ ## Validation Tools
226
+
227
+ ### Validate a Single GRF
228
+
229
+ ```bash
230
+ npm run validate:grf -- path/to/data.grf auto 100
231
+ ```
232
+
233
+ ### Validate All GRFs in a Folder
234
+
235
+ ```bash
236
+ npm run validate:all -- path/to/grf/folder auto
237
+ ```
238
+
239
+ Output example:
240
+ ```
241
+ ================================================================================
242
+ SUMMARY
243
+ ================================================================================
244
+ GRFs loaded: 3/3
245
+ Total files: 655,144
246
+ Bad U+FFFD: 12
247
+ Bad C1 Control: 40
248
+ Read tests passed: 300
249
+ Read tests failed: 0
250
+
251
+ Encoding Health: 99.99% (655,092/655,144 clean)
252
+ ```
253
+
254
+ ## Examples
255
+
256
+ ### Extract All Files
80
257
 
81
258
  ```bash
82
259
  npx ts-node examples/extract-all.ts path/to/data.grf output-directory
83
260
  ```
84
261
 
85
- All files will be written under `output-directory` (defaults to `output`).
262
+ ### List All Files by Extension
263
+
264
+ ```ts
265
+ const grf = new GrfNode(fd);
266
+ await grf.load();
267
+
268
+ // Get all sprite files
269
+ const sprites = grf.getFilesByExtension('spr');
270
+ console.log(`Found ${sprites.length} sprite files`);
271
+
272
+ // Get extension statistics
273
+ const stats = grf.getStats();
274
+ for (const [ext, count] of stats.extensionStats) {
275
+ console.log(`${ext}: ${count} files`);
276
+ }
277
+ ```
278
+
279
+ ### Handle Case-Insensitive Lookups
280
+
281
+ ```ts
282
+ // All of these resolve to the same file:
283
+ await grf.getFile('data\\sprite\\monster.spr');
284
+ await grf.getFile('DATA\\SPRITE\\MONSTER.SPR');
285
+ await grf.getFile('data/sprite/monster.spr');
286
+ ```
287
+
288
+ ## Browser Limitations
289
+
290
+ - **iconv-lite** is not available in browsers
291
+ - CP949 extended characters may show as C1 control characters
292
+ - Use `hasIconvLite()` to check availability
293
+
294
+ ## License
295
+
296
+ MIT
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var O=Object.create;var h=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var D=Object.getPrototypeOf,M=Object.prototype.hasOwnProperty;var N=(r,e)=>{for(var t in e)h(r,t,{get:e[t],enumerable:!0})},S=(r,e,t,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of C(e))!M.call(r,a)&&a!==t&&h(r,a,{get:()=>e[a],enumerable:!(o=I(e,a))||o.enumerable});return r};var P=(r,e,t)=>(t=r!=null?O(D(r)):{},S(e||!r||!r.__esModule?h(t,"default",{value:r,enumerable:!0}):t,r)),j=r=>S(h({},"__esModule",{value:!0}),r);var oe={};N(oe,{GrfBrowser:()=>p,GrfNode:()=>y,bufferPool:()=>m});module.exports=j(oe);var A=P(require("pako"),1),R=P(require("jdataview"),1);var c=new Uint8Array([128,64,32,16,8,4,2,1]),n=new Uint8Array(8),f=new Uint8Array(8),u=new Uint8Array(8),G=new Uint8Array([58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7]),H=new Uint8Array([40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25]),k=new Uint8Array([16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25]),T=[new Uint8Array([239,3,65,253,216,116,30,71,38,239,251,34,179,216,132,30,57,172,167,96,98,193,205,186,92,150,144,89,5,59,122,133,64,253,30,200,231,138,139,33,218,67,100,159,45,20,177,114,245,91,200,182,156,55,118,236,57,160,163,5,82,110,15,217]),new Uint8Array([167,221,13,120,158,11,227,149,96,54,54,79,249,96,90,163,17,36,210,135,200,82,117,236,187,193,76,186,36,254,143,25,218,19,102,175,73,208,144,6,140,106,251,145,55,141,13,120,191,73,17,244,35,229,206,59,85,188,162,87,232,34,116,206]),new Uint8Array([44,234,193,191,74,36,31,194,121,71,162,124,182,217,104,21,128,86,93,1,51,253,244,174,222,48,7,155,229,131,155,104,73,180,46,131,31,194,181,124,162,25,216,229,124,47,131,218,247,107,144,254,196,1,90,151,97,166,61,64,11,88,230,61]),new Uint8Array([77,209,178,15,40,189,228,120,246,74,15,147,139,23,209,164,58,236,201,53,147,86,126,203,85,32,160,254,108,137,23,98,23,98,75,177,180,222,209,135,201,20,60,74,126,168,226,125,160,159,246,92,106,9,141,240,15,227,83,37,149,54,40,203])];function Y(r,e){for(let t=0;t<64;++t){let o=G[t]-1;r[e+(o>>3&7)]&c[o&7]&&(n[t>>3&7]|=c[t&7])}r.set(n,e),n.set(u)}function $(r,e){for(let t=0;t<64;++t){let o=H[t]-1;r[e+(o>>3&7)]&c[o&7]&&(n[t>>3&7]|=c[t&7])}r.set(n,e),n.set(u)}function q(r,e){for(let t=0;t<32;++t){let o=k[t]-1;r[e+(o>>3)]&c[o&7]&&(n[(t>>3)+4]|=c[t&7])}r.set(n,e),n.set(u)}function Z(r,e){n[0]=(r[e+7]<<5|r[e+4]>>3)&63,n[1]=(r[e+4]<<1|r[e+5]>>7)&63,n[2]=(r[e+4]<<5|r[e+5]>>3)&63,n[3]=(r[e+5]<<1|r[e+6]>>7)&63,n[4]=(r[e+5]<<5|r[e+6]>>3)&63,n[5]=(r[e+6]<<1|r[e+7]>>7)&63,n[6]=(r[e+6]<<5|r[e+7]>>3)&63,n[7]=(r[e+7]<<1|r[e+4]>>7)&63,r.set(n,e),n.set(u)}function X(r,e){for(let t=0;t<4;++t)n[t]=T[t][r[t*2+0+e]]&240|T[t][r[t*2+1+e]]&15;r.set(n,e),n.set(u)}function J(r,e){for(let t=0;t<8;t++)f[t]=r[e+t];Z(f,0),X(f,0),q(f,0),r[e+0]^=f[4],r[e+1]^=f[5],r[e+2]^=f[6],r[e+3]^=f[7]}function U(r,e){Y(r,e),J(r,e),$(r,e)}function F(r,e,t){let o=t.toString().length,a=o<3?1:o<5?o+1:o<7?o+9:o+15,i=e>>3;for(let x=0;x<20&&x<i;++x)U(r,x*8);for(let x=20,l=-1;x<i;++x){if(x%a===0){U(r,x*8);continue}++l&&l%7===0&&K(r,x*8)}}function B(r,e){let t=e>>3;for(let o=0;o<20&&o<t;++o)U(r,o*8)}function K(r,e){n[0]=r[e+3],n[1]=r[e+4],n[2]=r[e+6],n[3]=r[e+0],n[4]=r[e+1],n[5]=r[e+2],n[6]=r[e+5],n[7]=Q[r[e+7]],r.set(n,e),n.set(u)}var Q=(()=>{let r=new Uint8Array([0,43,108,128,1,104,72,119,96,255,185,192,254,235]),e=new Uint8Array(Array.from({length:256},(o,a)=>a)),t=r.length;for(let o=0;o<t;o+=2)e[r[o+0]]=r[o+1],e[r[o+1]]=r[o+0];return e})();var V=1,W=2,ee=4,te="Master of Magic",v=46,z=Uint32Array.BYTES_PER_ELEMENT*2,d=class{constructor(e){this.fd=e;this.version=512;this.fileCount=0;this.loaded=!1;this.files=new Map;this.fileTableOffset=0;this.cache=new Map;this.cacheMaxSize=50;this.cacheOrder=[]}async getStreamReader(e,t){let o=await this.getStreamBuffer(this.fd,e,t);return new R.default(o,void 0,void 0,!0)}async load(){this.loaded||(await this.parseHeader(),await this.parseFileList(),this.loaded=!0)}async parseHeader(){let e=await this.getStreamReader(0,v);if(e.getString(15)!==te)throw new Error("Not a GRF file (invalid signature)");e.skip(15),this.fileTableOffset=e.getUint32()+v;let o=e.getUint32();if(this.fileCount=e.getUint32()-o-7,this.version=e.getUint32(),this.version!==512)throw new Error(`Unsupported version "0x${this.version.toString(16)}"`)}async parseFileList(){let e=await this.getStreamReader(this.fileTableOffset,z),t=e.getUint32(),o=e.getUint32(),a=await this.getStreamBuffer(this.fd,this.fileTableOffset+z,t),i=A.default.inflate(a),x=new TextDecoder("utf-8");for(let l=0,s=0;l<this.fileCount;++l){let b=s;for(;i[b]!==0&&b<i.length;)b++;let L=x.decode(i.subarray(s,b));s=b+1;let E={compressedSize:i[s++]|i[s++]<<8|i[s++]<<16|i[s++]<<24,lengthAligned:i[s++]|i[s++]<<8|i[s++]<<16|i[s++]<<24,realSize:i[s++]|i[s++]<<8|i[s++]<<16|i[s++]<<24,type:i[s++],offset:(i[s++]|i[s++]<<8|i[s++]<<16|i[s++]<<24)>>>0};E.type&V&&this.files.set(L,E)}}decodeEntry(e,t){return t.type&W?F(e,t.lengthAligned,t.compressedSize):t.type&ee&&B(e,t.lengthAligned),t.realSize===t.compressedSize?e:A.default.inflate(e)}addToCache(e,t){if(this.cacheOrder.length>=this.cacheMaxSize){let o=this.cacheOrder.shift();o&&this.cache.delete(o)}this.cache.set(e,t),this.cacheOrder.push(e)}getFromCache(e){let t=this.cache.get(e);if(t){let o=this.cacheOrder.indexOf(e);o>-1&&(this.cacheOrder.splice(o,1),this.cacheOrder.push(e))}return t}clearCache(){this.cache.clear(),this.cacheOrder=[]}async getFile(e){if(!this.loaded)return Promise.resolve({data:null,error:"GRF not loaded yet"});let t=e;if(!this.files.has(t))return Promise.resolve({data:null,error:`File "${t}" not found`});let o=this.getFromCache(t);if(o)return Promise.resolve({data:o,error:null});let a=this.files.get(t);if(!a)return{data:null,error:`File "${t}" not found`};let i=await this.getStreamBuffer(this.fd,a.offset+v,a.lengthAligned);try{let x=this.decodeEntry(i,a);return this.addToCache(t,x),Promise.resolve({data:x,error:null})}catch(x){return{data:null,error:x instanceof Error?x.message:String(x)}}}};var p=class extends d{async getStreamBuffer(e,t,o){return new Promise((a,i)=>{let x=new FileReader;x.onerror=i,x.onload=()=>a(new Uint8Array(x.result)),x.readAsArrayBuffer(e.slice(t,t+o))})}};var g=require("fs"),_=require("util");var w=class{constructor(){this.pools=new Map;this.maxPoolSize=10;this.poolSizes=[1024,4096,8192,16384,32768,65536,131072,262144];for(let e of this.poolSizes)this.pools.set(e,[])}getPoolSize(e){for(let t of this.poolSizes)if(e<=t)return t;return null}acquire(e){let t=this.getPoolSize(e);if(t===null)return Buffer.allocUnsafe(e);let o=this.pools.get(t);if(o){let a=o.find(i=>!i.inUse);if(a)return a.inUse=!0,a.buffer.subarray(0,e);if(o.length<this.maxPoolSize){let i=Buffer.allocUnsafe(t);return o.push({buffer:i,inUse:!0}),i.subarray(0,e)}}return Buffer.allocUnsafe(e)}release(e){let t=e.buffer.byteLength,o=this.pools.get(t);if(o){let a=o.find(i=>i.buffer===e||i.buffer.buffer===e.buffer);a&&(a.inUse=!1)}}clear(){for(let e of this.pools.values())e.length=0}stats(){let e=[];for(let[t,o]of this.pools.entries())e.push({size:t,total:o.length,inUse:o.filter(a=>a.inUse).length});return e}},m=new w;var re=(0,_.promisify)(g.read),y=class extends d{constructor(e,t){super(e),this.useBufferPool=t?.useBufferPool??!0;try{if(!(0,g.fstatSync)(e).isFile())throw new Error("GRFNode: file descriptor must point to a regular file")}catch{throw new Error("GRFNode: invalid file descriptor")}}async getStreamBuffer(e,t,o){let a=this.useBufferPool?m.acquire(o):Buffer.allocUnsafe(o),{bytesRead:i}=await re(e,a,0,o,t);if(i!==o)throw this.useBufferPool&&m.release(a),new Error("Not a GRF file (invalid signature)");return a}};0&&(module.exports={GrfBrowser,GrfNode,bufferPool});
1
+ "use strict";var ne=Object.create;var C=Object.defineProperty;var re=Object.getOwnPropertyDescriptor;var oe=Object.getOwnPropertyNames;var ie=Object.getPrototypeOf,se=Object.prototype.hasOwnProperty;var ae=(n,e)=>{for(var t in e)C(n,t,{get:e[t],enumerable:!0})},k=(n,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of oe(e))!se.call(n,i)&&i!==t&&C(n,i,{get:()=>e[i],enumerable:!(r=re(e,i))||r.enumerable});return n};var j=(n,e,t)=>(t=n!=null?ne(ie(n)):{},k(e||!n||!n.__esModule?C(t,"default",{value:n,enumerable:!0}):t,n)),ce=n=>k(C({},"__esModule",{value:!0}),n);var Pe={};ae(Pe,{GRF_ERROR_CODES:()=>ee,GrfBrowser:()=>S,GrfError:()=>h,GrfNode:()=>T,bufferPool:()=>U,countBadChars:()=>E,countC1ControlChars:()=>_,countReplacementChars:()=>R,fixMojibake:()=>I,hasIconvLite:()=>W,isMojibake:()=>B,normalizeEncodingPath:()=>K,normalizeFilename:()=>O,toMojibake:()=>X});module.exports=ce(Pe);var L=j(require("pako"),1),J=j(require("jdataview"),1);var g=new Uint8Array([128,64,32,16,8,4,2,1]),c=new Uint8Array(8),m=new Uint8Array(8),b=new Uint8Array(8),le=new Uint8Array([58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7]),fe=new Uint8Array([40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25]),ue=new Uint8Array([16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25]),H=[new Uint8Array([239,3,65,253,216,116,30,71,38,239,251,34,179,216,132,30,57,172,167,96,98,193,205,186,92,150,144,89,5,59,122,133,64,253,30,200,231,138,139,33,218,67,100,159,45,20,177,114,245,91,200,182,156,55,118,236,57,160,163,5,82,110,15,217]),new Uint8Array([167,221,13,120,158,11,227,149,96,54,54,79,249,96,90,163,17,36,210,135,200,82,117,236,187,193,76,186,36,254,143,25,218,19,102,175,73,208,144,6,140,106,251,145,55,141,13,120,191,73,17,244,35,229,206,59,85,188,162,87,232,34,116,206]),new Uint8Array([44,234,193,191,74,36,31,194,121,71,162,124,182,217,104,21,128,86,93,1,51,253,244,174,222,48,7,155,229,131,155,104,73,180,46,131,31,194,181,124,162,25,216,229,124,47,131,218,247,107,144,254,196,1,90,151,97,166,61,64,11,88,230,61]),new Uint8Array([77,209,178,15,40,189,228,120,246,74,15,147,139,23,209,164,58,236,201,53,147,86,126,203,85,32,160,254,108,137,23,98,23,98,75,177,180,222,209,135,201,20,60,74,126,168,226,125,160,159,246,92,106,9,141,240,15,227,83,37,149,54,40,203])];function xe(n,e){for(let t=0;t<64;++t){let r=le[t]-1;n[e+(r>>3&7)]&g[r&7]&&(c[t>>3&7]|=g[t&7])}n.set(c,e),c.set(b)}function de(n,e){for(let t=0;t<64;++t){let r=fe[t]-1;n[e+(r>>3&7)]&g[r&7]&&(c[t>>3&7]|=g[t&7])}n.set(c,e),c.set(b)}function he(n,e){for(let t=0;t<32;++t){let r=ue[t]-1;n[e+(r>>3)]&g[r&7]&&(c[(t>>3)+4]|=g[t&7])}n.set(c,e),c.set(b)}function me(n,e){c[0]=(n[e+7]<<5|n[e+4]>>3)&63,c[1]=(n[e+4]<<1|n[e+5]>>7)&63,c[2]=(n[e+4]<<5|n[e+5]>>3)&63,c[3]=(n[e+5]<<1|n[e+6]>>7)&63,c[4]=(n[e+5]<<5|n[e+6]>>3)&63,c[5]=(n[e+6]<<1|n[e+7]>>7)&63,c[6]=(n[e+6]<<5|n[e+7]>>3)&63,c[7]=(n[e+7]<<1|n[e+4]>>7)&63,n.set(c,e),c.set(b)}function pe(n,e){for(let t=0;t<4;++t)c[t]=H[t][n[t*2+0+e]]&240|H[t][n[t*2+1+e]]&15;n.set(c,e),c.set(b)}function ge(n,e){for(let t=0;t<8;t++)m[t]=n[e+t];me(m,0),pe(m,0),he(m,0),n[e+0]^=m[4],n[e+1]^=m[5],n[e+2]^=m[6],n[e+3]^=m[7]}function P(n,e){xe(n,e),ge(n,e),de(n,e)}function $(n,e,t){let r=t.toString().length,i=r<3?1:r<5?r+1:r<7?r+9:r+15,o=e>>3;for(let l=0;l<20&&l<o;++l)P(n,l*8);for(let l=20,a=-1;l<o;++l){if(l%i===0){P(n,l*8);continue}++a&&a%7===0&&be(n,l*8)}}function Y(n,e){let t=e>>3;for(let r=0;r<20&&r<t;++r)P(n,r*8)}function be(n,e){c[0]=n[e+3],c[1]=n[e+4],c[2]=n[e+6],c[3]=n[e+0],c[4]=n[e+1],c[5]=n[e+2],c[6]=n[e+5],c[7]=Ee[n[e+7]],n.set(c,e),c.set(b)}var Ee=(()=>{let n=new Uint8Array([0,43,108,128,1,104,72,119,96,255,185,192,254,235]),e=new Uint8Array(Array.from({length:256},(r,i)=>i)),t=n.length;for(let r=0;r<t;r+=2)e[n[r+0]]=n[r+1],e[n[r+1]]=n[r+0];return e})();var d=null;try{typeof process<"u"&&process.versions?.node&&(d=require("iconv-lite"))}catch{d=null}function W(){return d!==null}function _(n){let e=0;for(let t of n){let r=t.charCodeAt(0);r>=128&&r<=159&&e++}return e}function R(n){let e=0;for(let t of n)t==="\uFFFD"&&e++;return e}function E(n){return R(n)+_(n)}function v(n,e){let t=e.toLowerCase();if((t==="cp949"||t==="euc-kr")&&d)try{let r=Buffer.from(n);return d.decode(r,"cp949")}catch{}try{let r=t==="cp949"?"euc-kr":t;return new TextDecoder(r,{fatal:!1}).decode(n)}catch{return Array.from(n).map(r=>String.fromCharCode(r)).join("")}}function V(n,e){let t=v(n,e),r=_(t),i=R(t),o=r+i;return{text:t,badChars:o,c1Chars:r,replacementChars:i}}var ye=[/[ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞß][¡-þ]/,/À¯/,/Àú/,/ÀÎ/,/Ÿ/,/Æä/,/ÀÌ/,/½º/,/¾Æ/,/¸ð/,/¸®/,/¿¡/,/Áö/,/µ¥/,/ÅØ/,/½ºÆ®/,/¸ÁÅä/];function B(n){if(!n||n.length===0||/[\uAC00-\uD7AF]/.test(n))return!1;for(let r of ye)if(r.test(n))return!0;let e=0;for(let r of n){let i=r.charCodeAt(0);i>=128&&i<=255&&e++}return e/n.length>.3}function I(n){if(!d)return n;try{let e=d.encode(n,"windows-1252"),t=d.decode(e,"cp949"),r=/[\uAC00-\uD7AF]/.test(t),i=E(t),o=E(n);return r&&i<=o?t:n}catch{return n}}function X(n){if(!d)return n;try{let e=d.encode(n,"cp949");return d.decode(e,"windows-1252")}catch{return n}}function O(n){return B(n)?I(n):n}function K(n){let t=n.split(/[\\/]/).map(i=>O(i)),r=n.includes("\\")?"\\":"/";return t.join(r)}function Q(n,e=.01){if(n.length===0)return"utf-8";let t=0,r=0,i=0,o=0;for(let s of n){if(!s.some(p=>p>127))continue;o++,i+=s.length;let x=V(s,"utf-8"),f=V(s,"cp949");t+=x.badChars,r+=f.badChars}if(o===0)return"utf-8";let l=i>0?t/i:0,a=i>0?r/i:0;return l<e?"utf-8":a<l?"cp949":"utf-8"}var ee={INVALID_MAGIC:"GRF_INVALID_MAGIC",UNSUPPORTED_VERSION:"GRF_UNSUPPORTED_VERSION",NOT_LOADED:"GRF_NOT_LOADED",FILE_NOT_FOUND:"GRF_FILE_NOT_FOUND",AMBIGUOUS_PATH:"GRF_AMBIGUOUS_PATH",DECOMPRESS_FAIL:"GRF_DECOMPRESS_FAIL",CORRUPT_TABLE:"GRF_CORRUPT_TABLE",LIMIT_EXCEEDED:"GRF_LIMIT_EXCEEDED",INVALID_OFFSET:"GRF_INVALID_OFFSET",DECRYPT_REQUIRED:"GRF_DECRYPT_REQUIRED"},h=class extends Error{constructor(t,r,i){super(r);this.code=t;this.context=i;this.name="GrfError"}},Ae=1,Fe=2,Ue=4,Ce="Master of Magic",D=46,q=Uint32Array.BYTES_PER_ELEMENT*2,_e=256*1024*1024,Re=5e5,Se=.01;function y(n){return n.toLowerCase().replace(/\\/g,"/")}function Z(n){let e=n.lastIndexOf(".");return e===-1||e===n.length-1?"":n.substring(e+1).toLowerCase()}function Te(n,e){return v(n,e==="euc-kr"||e==="cp949"?"cp949":e)}var A=class{constructor(e,t){this.fd=e;this.version=512;this.fileCount=0;this.loaded=!1;this.files=new Map;this.normalizedIndex=new Map;this.extensionIndex=new Map;this.fileTableOffset=0;this.cache=new Map;this.cacheMaxSize=50;this.cacheOrder=[];this._stats={fileCount:0,badNameCount:0,collisionCount:0,extensionStats:new Map,detectedEncoding:"utf-8"};this.options={filenameEncoding:t?.filenameEncoding??"auto",autoDetectThreshold:t?.autoDetectThreshold??Se,maxFileUncompressedBytes:t?.maxFileUncompressedBytes??_e,maxEntries:t?.maxEntries??Re}}async getStreamReader(e,t){let r=await this.getStreamBuffer(this.fd,e,t);return new J.default(r,void 0,void 0,!0)}async load(){this.loaded||(await this.parseHeader(),await this.parseFileList(),this.loaded=!0)}async parseHeader(){let e=await this.getStreamReader(0,D),t=e.getString(15);if(t!==Ce)throw new h("INVALID_MAGIC","Not a GRF file (invalid signature)",{signature:t});e.skip(15),this.fileTableOffset=e.getUint32()+D;let r=e.getUint32();if(this.fileCount=e.getUint32()-r-7,this.version=e.getUint32(),this.version!==512)throw new h("UNSUPPORTED_VERSION",`Unsupported version "0x${this.version.toString(16)}"`,{version:this.version});if(this.fileCount>this.options.maxEntries)throw new h("LIMIT_EXCEEDED",`File count ${this.fileCount} exceeds limit ${this.options.maxEntries}`,{fileCount:this.fileCount,maxEntries:this.options.maxEntries})}async parseFileList(){let e=await this.getStreamReader(this.fileTableOffset,q),t=e.getUint32(),r=e.getUint32(),i=await this.getStreamBuffer(this.fd,this.fileTableOffset+q,t),o;try{o=L.default.inflate(i)}catch(a){throw new h("CORRUPT_TABLE","Failed to decompress file table",{compressedSize:t,realSize:r,error:a instanceof Error?a.message:String(a)})}if(o.length!==r)throw new h("CORRUPT_TABLE",`File table size mismatch: expected ${r}, got ${o.length}`,{expected:r,actual:o.length});let l=this.options.filenameEncoding;if(this.options.filenameEncoding==="auto"){let a=[],s=0,u=Math.min(200,this.fileCount);for(let x=0;x<u&&s<o.length;x++){let f=s;for(;o[f]!==0&&f<o.length;)f++;let p=o.subarray(s,f);a.push(p),s=f+1+17}l=Q(a,this.options.autoDetectThreshold)}this._stats.detectedEncoding=l,this._stats.badNameCount=0,this._stats.collisionCount=0,this._stats.extensionStats.clear();for(let a=0,s=0;a<this.fileCount;++a){if(s>=o.length)throw new h("CORRUPT_TABLE",`Unexpected end of file table at entry ${a}`,{position:s,dataLength:o.length,entryIndex:a});let u=s;for(;o[u]!==0&&u<o.length;)u++;let x=o.slice(s,u),f=Te(x,l);if(E(f)>0&&this._stats.badNameCount++,s=u+1,s+17>o.length)throw new h("CORRUPT_TABLE",`Incomplete entry data at entry ${a}`,{position:s,dataLength:o.length,entryIndex:a});let p={compressedSize:o[s++]|o[s++]<<8|o[s++]<<16|o[s++]<<24,lengthAligned:o[s++]|o[s++]<<8|o[s++]<<16|o[s++]<<24,realSize:o[s++]|o[s++]<<8|o[s++]<<16|o[s++]<<24,type:o[s++],offset:(o[s++]|o[s++]<<8|o[s++]<<16|o[s++]<<24)>>>0,rawNameBytes:x};if(!(p.realSize>this.options.maxFileUncompressedBytes)&&p.type&Ae){this.files.set(f,p);let G=y(f),N=this.normalizedIndex.get(G);N?(N.push(f),this._stats.collisionCount++):this.normalizedIndex.set(G,[f]);let F=Z(f);if(F){let M=this.extensionIndex.get(F);M?M.push(f):this.extensionIndex.set(F,[f]),this._stats.extensionStats.set(F,(this._stats.extensionStats.get(F)||0)+1)}}}this._stats.fileCount=this.files.size}decodeEntry(e,t){return t.type&Fe?$(e,t.lengthAligned,t.compressedSize):t.type&Ue&&Y(e,t.lengthAligned),t.realSize===t.compressedSize?e:L.default.inflate(e)}addToCache(e,t){if(this.cacheOrder.length>=this.cacheMaxSize){let r=this.cacheOrder.shift();r&&this.cache.delete(r)}this.cache.set(e,t),this.cacheOrder.push(e)}getFromCache(e){let t=this.cache.get(e);if(t){let r=this.cacheOrder.indexOf(e);r>-1&&(this.cacheOrder.splice(r,1),this.cacheOrder.push(e))}return t}clearCache(){this.cache.clear(),this.cacheOrder=[]}async getFile(e){if(!this.loaded)return Promise.resolve({data:null,error:"GRF not loaded yet"});let t=this.resolvePath(e);if(t.status==="not_found")return Promise.resolve({data:null,error:`File "${e}" not found`});if(t.status==="ambiguous")return Promise.resolve({data:null,error:`Ambiguous path "${e}": ${t.candidates?.length} matches found. Use exact path: ${t.candidates?.slice(0,5).join(", ")}${(t.candidates?.length||0)>5?"...":""}`});let r=t.matchedPath,i=this.getFromCache(r);if(i)return Promise.resolve({data:i,error:null});let o=this.files.get(r);if(!o)return{data:null,error:`File "${r}" not found`};let l=await this.getStreamBuffer(this.fd,o.offset+D,o.lengthAligned);try{let a=this.decodeEntry(l,o);return this.addToCache(r,a),Promise.resolve({data:a,error:null})}catch(a){return{data:null,error:a instanceof Error?a.message:String(a)}}}resolvePath(e){if(this.files.has(e))return{status:"found",matchedPath:e};let t=y(e),r=this.normalizedIndex.get(t);return!r||r.length===0?{status:"not_found"}:r.length===1?{status:"found",matchedPath:r[0]}:{status:"ambiguous",candidates:r}}hasFile(e){return this.resolvePath(e).status==="found"}getEntry(e){let t=this.resolvePath(e);return t.status!=="found"||!t.matchedPath?null:this.files.get(t.matchedPath)||null}find(e={}){let{ext:t,contains:r,endsWith:i,regex:o,limit:l}=e,a=[];if(t&&!r&&!i&&!o){let s=t.toLowerCase().replace(/^\./,"");a=this.extensionIndex.get(s)||[]}else for(let s of this.files.keys()){if(t){let u=t.toLowerCase().replace(/^\./,"");if(Z(s)!==u)continue}if(r){let u=y(s),x=y(r);if(!u.includes(x))continue}if(i){let u=y(s),x=y(i);if(!u.endsWith(x))continue}if(!(o&&!o.test(s))&&(a.push(s),l&&a.length>=l))break}return l&&a.length>l&&(a=a.slice(0,l)),a}getFilesByExtension(e){let t=e.toLowerCase().replace(/^\./,"");return this.extensionIndex.get(t)||[]}listExtensions(){return Array.from(this.extensionIndex.keys()).sort()}listFiles(){return Array.from(this.files.keys())}getStats(){return{...this._stats,extensionStats:new Map(this._stats.extensionStats)}}getDetectedEncoding(){return this._stats.detectedEncoding}async reloadWithEncoding(e){this.options.filenameEncoding=e,this.files.clear(),this.normalizedIndex.clear(),this.extensionIndex.clear(),this.clearCache(),this.loaded=!1,await this.load()}};var S=class extends A{constructor(e,t){super(e,t)}async getStreamBuffer(e,t,r){return new Promise((i,o)=>{let l=new FileReader;l.onerror=o,l.onload=()=>i(new Uint8Array(l.result)),l.readAsArrayBuffer(e.slice(t,t+r))})}};var w=require("fs"),te=require("util");var z=class{constructor(){this.pools=new Map;this.maxPoolSize=10;this.poolSizes=[1024,4096,8192,16384,32768,65536,131072,262144];for(let e of this.poolSizes)this.pools.set(e,[])}getPoolSize(e){for(let t of this.poolSizes)if(e<=t)return t;return null}acquire(e){let t=this.getPoolSize(e);if(t===null)return Buffer.allocUnsafe(e);let r=this.pools.get(t);if(r){let i=r.find(o=>!o.inUse);if(i)return i.inUse=!0,i.buffer.subarray(0,e);if(r.length<this.maxPoolSize){let o=Buffer.allocUnsafe(t);return r.push({buffer:o,inUse:!0}),o.subarray(0,e)}}return Buffer.allocUnsafe(e)}release(e){let t=e.buffer.byteLength,r=this.pools.get(t);if(r){let i=r.find(o=>o.buffer===e||o.buffer.buffer===e.buffer);i&&(i.inUse=!1)}}clear(){for(let e of this.pools.values())e.length=0}stats(){let e=[];for(let[t,r]of this.pools.entries())e.push({size:t,total:r.length,inUse:r.filter(i=>i.inUse).length});return e}},U=new z;var we=(0,te.promisify)(w.read),T=class extends A{constructor(e,t){super(e,t),this.useBufferPool=t?.useBufferPool??!0;try{if(!(0,w.fstatSync)(e).isFile())throw new Error("GRFNode: file descriptor must point to a regular file")}catch{throw new Error("GRFNode: invalid file descriptor")}}async getStreamBuffer(e,t,r){let i=this.useBufferPool?U.acquire(r):Buffer.allocUnsafe(r),{bytesRead:o}=await we(e,i,0,r,t);if(o!==r)throw this.useBufferPool&&U.release(i),new Error("Not a GRF file (invalid signature)");return i}};0&&(module.exports={GRF_ERROR_CODES,GrfBrowser,GrfError,GrfNode,bufferPool,countBadChars,countC1ControlChars,countReplacementChars,fixMojibake,hasIconvLite,isMojibake,normalizeEncodingPath,normalizeFilename,toMojibake});
2
2
  //# sourceMappingURL=index.cjs.map