grf-file-extractor 1.0.35

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/lib/cps.js ADDED
@@ -0,0 +1,72 @@
1
+ // CPS (cps.dll / Fury.dll) decryption support
2
+ // Extracts the RC4 S-box from a cps.dll and provides decryption for GRF entries
3
+
4
+ var fs = require('fs')
5
+
6
+ // Load encryption key from a CPS DLL or a raw S-box hex file
7
+ function loadKey(path) {
8
+ var data = fs.readFileSync(path)
9
+
10
+ // If the file is small and looks like a hex dump (text file), parse as raw S-box
11
+ var text = data.toString('utf8').trim()
12
+ var hexMatch = text.match(/^(?:SBOX:)?([0-9a-fA-F]{512})$/)
13
+ if (hexMatch) {
14
+ return new Uint8Array(Buffer.from(hexMatch[1], 'hex'))
15
+ }
16
+
17
+ // Otherwise treat as a DLL: search for UTF-16LE L"NUMBER" marker
18
+ var pattern = [0x4E, 0x00, 0x55, 0x00, 0x4D, 0x00, 0x42, 0x00, 0x45, 0x00, 0x52, 0x00]
19
+ var offset = -1
20
+
21
+ for (var i = 0; i < data.length - pattern.length; i++) {
22
+ var match = true
23
+ for (var j = 0; j < pattern.length; j++) {
24
+ if (data[i + j] !== pattern[j]) { match = false; break }
25
+ }
26
+ if (match) { offset = i; break }
27
+ }
28
+
29
+ if (offset === -1) {
30
+ throw new Error('CPS: Could not find encryption key in ' + path)
31
+ }
32
+
33
+ // Key is after L"NUMBER\0" (14 bytes) + 4 byte header
34
+ var keyStart = offset + 14 + 4
35
+ var key = data.slice(keyStart, keyStart + 260)
36
+
37
+ if (key.length < 260) {
38
+ throw new Error('CPS: Not enough key data in ' + path)
39
+ }
40
+
41
+ // Build S-box using the DLL's key scheduling algorithm
42
+ var a = (key[3] << 24) | (key[2] << 16) | (key[1] << 8) | key[0]
43
+ var sbox = new Uint8Array(256)
44
+ for (var i = 0; i < 256; i++) {
45
+ sbox[i] = ((a & 0xFF) ^ key[i + 4]) & 0xFF
46
+ a = Math.imul(a, 0x2F)
47
+ }
48
+
49
+ return sbox
50
+ }
51
+
52
+ // RC4 decrypt a buffer in-place using the S-box
53
+ // initialI is typically the entry's real_size (decompressed size)
54
+ function decrypt(buffer, sbox, initialI) {
55
+ var S = new Uint8Array(sbox) // working copy
56
+ var i = initialI & 0xFF
57
+ var j = 0
58
+
59
+ for (var k = 0; k < buffer.length; k++) {
60
+ i = (i + 1) & 0xFF
61
+ j = (j + S[i]) & 0xFF
62
+ var tmp = S[i]; S[i] = S[j]; S[j] = tmp
63
+ buffer[k] ^= S[(S[i] + S[j]) & 0xFF]
64
+ }
65
+
66
+ return buffer
67
+ }
68
+
69
+ module.exports = {
70
+ loadKey: loadKey,
71
+ decrypt: decrypt
72
+ }
@@ -0,0 +1,258 @@
1
+ // Grf full extraction
2
+ var GRF = require('./Loaders/GameFile.js')
3
+ var Inflate = require('./Utils/Inflate.js')
4
+ var GameFileDecrypt = require('./Loaders/GameFileDecrypt.js')
5
+ var EventEmitter = require('events').EventEmitter
6
+ var Cluster = require('cluster')
7
+ var Os = require('os')
8
+ var Fs = require('fs')
9
+ var Util = require('util')
10
+ var Zlib = require('zlib')
11
+ var Cps = require('./cps.js')
12
+ var fs = require('fs')
13
+
14
+ // Constructor
15
+ var Extractor = function Constructor(grf, options) {
16
+ this._progress = 0
17
+ this.grf = grf
18
+ this.concurrency = options.concurrency || 100
19
+ this.output_dir = options.output || ''
20
+ this.cpsSbox = options.cpsSbox || null
21
+ this.mkdirPaths = [] // List of paths already created
22
+
23
+ // Start at next tick, give time for event listener to bind
24
+ process.nextTick(extractStart.bind(this))
25
+
26
+ return this
27
+ }
28
+
29
+ // Event emmiter
30
+ Util.inherits(Extractor, EventEmitter);
31
+
32
+ // Start the extraction
33
+ function extractStart() {
34
+ var numCpus = Os.cpus().length
35
+ var cluster = Cluster
36
+ var grf = this.grf
37
+ var concurrency = this.concurrency
38
+ var extract_folder = (this.output_dir ? this.output_dir.replace(/\/?$/, '/') : '') // Add trailling slash to folder, if specified
39
+ var self = this
40
+
41
+ // The master only spawn workers to work on the GRF
42
+ // And receives progress updates
43
+ if(cluster.isMaster) {
44
+ // Fork some workers
45
+ for(var i = 0; i < numCpus; i++) {
46
+ cluster.fork()
47
+ }
48
+
49
+ // Workers online
50
+ var workers = []
51
+
52
+ // Separate the entries table into the number of workers
53
+ var entries_slices = [{
54
+ start: 0,
55
+ end: 0,
56
+ }]
57
+ var last = 0
58
+ var chunk_size = Math.floor(grf.entries.length / numCpus)
59
+
60
+ for(var i = 0; i < numCpus; i++) {
61
+ next = last + chunk_size
62
+
63
+ // If last iteration, set this slice to the rest of the entries array
64
+ if(i == numCpus - 1) {
65
+ next = grf.entries.length
66
+ }
67
+
68
+ entries_slices[i] = {
69
+ start: last,
70
+ end: next,
71
+ }
72
+
73
+ last = next
74
+ }
75
+
76
+ // A worker is online, send it work :)
77
+ cluster.on('online', function(worker) {
78
+ var worker_id = workers.length
79
+
80
+ // Send a slice of the entries table to this worker to process
81
+ var slice = entries_slices[ worker_id ]
82
+ worker.send(slice)
83
+
84
+ grf.debug("Sending to worker #%d slice %d to %d.", worker_id, slice.start, slice.end)
85
+
86
+ // Worker is reporting its progress
87
+ worker.on('message', function(msg) {
88
+ if(msg.cmd == 'progress') {
89
+ self._progress++
90
+ }
91
+ })
92
+
93
+ // Register worker
94
+ workers.push(worker)
95
+ })
96
+
97
+ self.emit('start')
98
+
99
+ return
100
+ }
101
+
102
+ // Else is, worker
103
+ // Wait for the master to send us the go signal
104
+ process.on('message', function(msg) {
105
+ // Receive a slice to work on
106
+ var entries = grf.entries.slice(msg.start, msg.end)
107
+ var next = 0
108
+
109
+ grf.debug("Worker received slice from %d to %d to work on.", msg.start, msg.end)
110
+ grf.debug('Worker extracting', entries.length, 'files.')
111
+
112
+ // Launch # concurrent extractors
113
+ for(var i = 0; i < concurrency; i++){
114
+ extract_next()
115
+ }
116
+
117
+ function extract_next() {
118
+ if(next >= entries.length) {
119
+ return
120
+ }
121
+
122
+ var i = next++
123
+ var entry = entries[i]
124
+ var buffer = Buffer.alloc(entry.length_aligned);
125
+ var file = entry.filename.replace( /\\/g, '/') // Replace windows back slash \ to unix forward slash
126
+ var fullpath = extract_folder + file
127
+
128
+ if(entry.type & GRF.FILELIST_TYPE_FILE) {
129
+ // Read entry from .grf file
130
+ Fs.read(grf.file.fd, buffer, 0, entry.length_aligned, entry.offset + GRF.struct_header.size, function(err, bytesRead, buffer){
131
+ if(err) {
132
+ console.error("Error reading %s: %s", file, err.message)
133
+ finish()
134
+ return
135
+ }
136
+
137
+ // Write entry to file
138
+ self.writeEntry(entry, fullpath, buffer, finish)
139
+ })
140
+ }
141
+ // Directory?
142
+ else {
143
+ self.makeDir(fullpath, finish)
144
+ }
145
+
146
+ // Extract next
147
+ function finish() {
148
+ // Notify master of progress
149
+ process.send({cmd: 'progress'})
150
+ extract_next()
151
+ }
152
+ }
153
+ })
154
+
155
+ // Avoid workers crashing because of excpections
156
+ process.on('uncaughtException', function(err) {
157
+ console.error(err)
158
+ })
159
+ }
160
+
161
+ // Progress
162
+ Extractor.prototype.progress = function progress() {
163
+ return this._progress
164
+ }
165
+
166
+ // Make a directory
167
+ Extractor.prototype.makeDir = function mkDir(path, callback) {
168
+ // Create directoy
169
+ if(typeof this.mkdirPaths[path] === 'undefined') {
170
+ var self = this
171
+
172
+ fs.mkdir(path, { recursive: true }, function (err) {
173
+ if(err) {
174
+ console.error("Error creating directory %s: %s", path, err.message)
175
+ }
176
+
177
+ // Add to created paths hashtable
178
+ self.mkdirPaths[path] = true
179
+
180
+ callback()
181
+ })
182
+
183
+ return
184
+ }
185
+
186
+ // Directoy already exixsts
187
+ callback()
188
+ }
189
+
190
+ // Write entry buffer to a file
191
+ Extractor.prototype.writeEntry = function writeEntry(entry, path, buffer, callback) {
192
+ // Decode buffer if needed
193
+ if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_MIXED) {
194
+ var data = new Uint8Array(buffer)
195
+ GameFileDecrypt.decodeFull( data, entry.length_aligned, entry.pack_size)
196
+ buffer = Buffer.from(data)
197
+ }
198
+ else if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_HEADER) {
199
+ var data = new Uint8Array(buffer)
200
+ GameFileDecrypt.decodeHeader( data, entry.length_aligned )
201
+ buffer = Buffer.from(data)
202
+ }
203
+
204
+ var self = this
205
+
206
+ function writeInflated(buf) {
207
+ var folder = path.substring(0, path.lastIndexOf("/"))
208
+ self.makeDir(folder, function() {
209
+ self.writeFile(path, buf, callback)
210
+ })
211
+ }
212
+
213
+ if (self.cpsSbox) {
214
+ // CPS encrypted GRF: RC4 decrypt a copy, then inflate
215
+ var decrypted = Buffer.from(buffer)
216
+ Cps.decrypt(decrypted, self.cpsSbox, entry.real_size)
217
+
218
+ Zlib.inflate(decrypted, function(err, buf) {
219
+ if(!err) {
220
+ writeInflated(buf)
221
+ return
222
+ }
223
+
224
+ // Fallback: try without CPS decryption (some files may be unencrypted)
225
+ Zlib.inflate(buffer, function(err2, buf2) {
226
+ if(!err2) {
227
+ writeInflated(buf2)
228
+ return
229
+ }
230
+ console.error("Error decompressing %s: %s", path, err.message2)
231
+ callback()
232
+ })
233
+ })
234
+ } else {
235
+ Zlib.inflate(buffer, function(err, buf) {
236
+ if(err) {
237
+ console.error("Error decompressing %s: %s", path, err.message)
238
+ callback()
239
+ return
240
+ }
241
+ writeInflated(buf)
242
+ })
243
+ }
244
+ }
245
+
246
+ // Write buffer to file
247
+ Extractor.prototype.writeFile = function writeFile(path, buffer, callback) {
248
+ Fs.writeFile(path, buffer, function (err) {
249
+ if (err) {
250
+ console.error("Error writing %s: %s", path, err.message)
251
+ }
252
+
253
+ callback()
254
+ })
255
+ }
256
+
257
+ // Exports
258
+ module.exports = Extractor
package/lib/grf.js ADDED
@@ -0,0 +1,41 @@
1
+ // This librarie extends default roBrowser software
2
+ // It extends roBrowser default GRF loader
3
+ // Giving it a set a new set of API's and replacing some performance critical functions
4
+ var Grf = module.exports = require('./Loaders/GameFile.js')
5
+ var Extractor = require('./extractor')
6
+
7
+ // Should we print debug messages?
8
+ Grf.prototype.printDebug = false
9
+
10
+ // Search for a pattern on the .grf file
11
+ // @param regexp: A regular expression instance
12
+ Grf.prototype.searchPattern = function search(regexp, callback) {
13
+ var matches = []
14
+
15
+ for(var i in this.entries) {
16
+ var entry = this.entries[i]
17
+ var filename = entry.filename
18
+
19
+ if(regexp.test(filename)) {
20
+ matches.push(entry)
21
+ }
22
+ }
23
+
24
+ callback(matches)
25
+ }
26
+
27
+ // Extract all files from the .grf to a folder
28
+ // @param options {
29
+ // output: output directory
30
+ // concurrency: concurrency rate
31
+ // }
32
+ Grf.prototype.extract = function(options) {
33
+ return new Extractor(this, options)
34
+ }
35
+
36
+ // Print a debug message if verbosity is set
37
+ Grf.prototype.debug = function debug() {
38
+ if(this.printDebug) {
39
+ console.log.apply(this, arguments)
40
+ }
41
+ }
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "grf-file-extractor",
3
+ "version": "1.0.35",
4
+ "description": "A command line GRF extractor, used to manage Ragnarok's .grf files.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "grf-file-extractor": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "npm test",
11
+ "build": "tsup src/index.ts --format cjs,esm --dts"
12
+ },
13
+ "keywords": [
14
+ "ragnarok",
15
+ "grf",
16
+ "robrowser",
17
+ "extractor"
18
+ ],
19
+ "author": "ayenpanda",
20
+ "license": "MIT",
21
+ "engines": {
22
+ "node": ">=10.12.0"
23
+ }
24
+ }
package/profile.js ADDED
@@ -0,0 +1,17 @@
1
+ // Profiler
2
+ var cluster = require('cluster')
3
+ var agent = require('webkit-devtools-agent');
4
+
5
+ if(cluster.isMaster) {
6
+ agent.start()
7
+ }
8
+ else {
9
+ var id = cluster.worker.id
10
+ agent.start({
11
+ port: 9999 + id,
12
+ ipc_port: 3333 + id,
13
+ })
14
+ console.log("Worker #%d inspector on port %d.", id, 9999 + id)
15
+ }
16
+
17
+ require('./index.js')
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export function sayHello(name: string = 'world'): string {
2
+ return `Hello, ${name}!`;
3
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "noImplicitAny": true,
5
+ "module": "commonjs",
6
+ "target": "ES2022",
7
+ "outDir": "dist",
8
+ "declaration": true,
9
+ "esModuleInterop": true,
10
+ "strictNullChecks": true,
11
+ "moduleResolution": "node",
12
+ "isolatedModules": true,
13
+ "noEmit": true,
14
+ "ignoreDeprecations": "6.0"
15
+ },
16
+ "include": ["**/*.ts"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ format: ['cjs', 'esm'],
5
+ entry: ['./src/index.ts'],
6
+ dts: true,
7
+ shims: true,
8
+ skipNodeModulesBundle: true,
9
+ clean: true,
10
+ });