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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ Grf-extractor
2
+ =========
3
+
4
+ This is a command line tool for extracting and handling .grf packages.
5
+
6
+ GRF files are a set of packed and compressed files, normally found on Ragnarok's game files.
7
+
8
+ Install
9
+ =========
10
+ ```
11
+ npm install -g grf-extractor
12
+ ```
13
+
14
+ Usage
15
+ ====
16
+ ```
17
+ Usage: grf-extractor -g data.grf -o output_dir
18
+ Options:
19
+ -g, --grf The grf file to be worked on.
20
+ -s, --search Search a single file on the .grf or a list of files separated by comma. RegExp are supported.
21
+ -l, --list List files inside the grf.
22
+ -o, --output Output directoy to write the extracted files to.
23
+ -e, --extract Extract a single file from the .grf, prints to stdout. Example: grf-extractor -e data/clientinfo.xml > clientinfo.xml
24
+ -c, --concurrency Concurrency rate, how many parallel extractions should we do, set it to higher values for a faster extraction. [default: 100]
25
+ -v, --verbose Enable verbose output, will print debug messages.
26
+ -h, --help Print help and usage information
27
+ ```
28
+
29
+ TODO
30
+ ======
31
+ * Support writting files to the grf.
32
+ * Standardize the API.
33
+ * Refactor `extractor.js`.
package/dist/index.js ADDED
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ sayHello: () => sayHello
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+ function sayHello(name = "world") {
27
+ return `Hello, ${name}!`;
28
+ }
29
+ // Annotate the CommonJS export names for ESM import in node:
30
+ 0 && (module.exports = {
31
+ sayHello
32
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ // src/index.ts
2
+ function sayHello(name = "world") {
3
+ return `Hello, ${name}!`;
4
+ }
5
+ export {
6
+ sayHello
7
+ };
package/index.js ADDED
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ // Entry point
3
+ // Parse arguments and setup the enviroment
4
+
5
+ // Dependencies
6
+ var argv = require('optimist')
7
+ var _package = require('./package.json')
8
+ var Grf = require('./lib/grf.js')
9
+ var ProgressBar = require('progress')
10
+ var Fs = require('fs')
11
+
12
+ // Setup args
13
+ argv.options('g', {
14
+ alias: 'grf',
15
+ describe: 'The grf file to be worked on.',
16
+ })
17
+ argv.options('s', {
18
+ alias: 'search',
19
+ describe: 'Search a single file on the .grf or a list of files separated by comma. RegExp are supported.',
20
+ })
21
+ argv.options('l', {
22
+ alias: 'list',
23
+ describe: 'List files inside the grf.',
24
+ })
25
+ argv.options('o', {
26
+ alias: 'output',
27
+ describe: 'Output directoy to write the extracted files to.',
28
+ })
29
+ argv.options('e', {
30
+ alias: 'extract',
31
+ describe: 'Extract a single file from the .grf, prints to stdout. Example: grf-extractor -e data/clientinfo.xml > clientinfo.xml',
32
+ })
33
+ argv.options('c', {
34
+ alias: 'concurrency',
35
+ default: 100,
36
+ describe: 'Concurrency rate, how many parallel extractions should we do, set it to higher values for a faster extraction.',
37
+ })
38
+ argv.options('v', {
39
+ alias: 'verbose',
40
+ describe: 'Enable verbose output, will print debug messages.',
41
+ })
42
+ argv.options('version', {
43
+ describe: 'Print package version.',
44
+ })
45
+ argv.options('k', {
46
+ alias: 'key',
47
+ describe: 'Path to a cps.dll for decrypting encrypted GRF files.',
48
+ })
49
+ argv.options('h', {
50
+ alias: 'help',
51
+ describe: 'Print help and usage information',
52
+ })
53
+
54
+
55
+ // Parse arguments
56
+ var arg = argv.argv
57
+
58
+ // Print version
59
+ if(arg.version) {
60
+ console.log(_package.version)
61
+ return
62
+ }
63
+
64
+ // Print help
65
+ if(!arg.g || arg.h) {
66
+ console.error("Usage: grf-extractor -g data.grf -o output_dir [-k cps.dll]")
67
+ argv.showHelp()
68
+ return
69
+ }
70
+
71
+ // Init working grf
72
+ var fd = Fs.openSync(arg.g, 'r')
73
+
74
+ var grf = new Grf({
75
+ fd: fd
76
+ })
77
+
78
+ // Set debugging mode
79
+ if(typeof arg.v !== 'undefined') {
80
+ grf.printDebug = true
81
+ }
82
+
83
+ // Search and print information about a single file in the grf
84
+ if(typeof arg.s === 'string') {
85
+ var list = arg.s.split(',')
86
+
87
+ for(var i in list) {
88
+ var pattern = list[i]
89
+
90
+ grf.searchPattern(new RegExp(pattern, 'gi'), function(matches) {
91
+ console.log("Search for %s:", pattern)
92
+
93
+ if(! matches) {
94
+ console.log("- Nothing found matching this pattern.")
95
+ return
96
+ }
97
+
98
+ for(var m in matches) {
99
+ var match = matches[m]
100
+
101
+ console.log("- File path: %s - File size: %d kb", match.filename, match.real_size/1000)
102
+ }
103
+ })
104
+ }
105
+
106
+ return
107
+ }
108
+
109
+ // List files inside the grf
110
+ if(typeof arg.l !== 'undefined') {
111
+ var list = grf.entries
112
+
113
+ for(var i in list) {
114
+ var file = list[i]
115
+
116
+ console.log("%s - size in bytes: %d", file.filename, file.real_size)
117
+ }
118
+
119
+ return
120
+ }
121
+
122
+ // Extract a single file frm the grf
123
+ if(typeof arg.e === 'string') {
124
+ var filename = arg.e
125
+
126
+ // We need to replace our normal slashes for windows forward slash
127
+ filename = filename.replace( /\//g, '\\')
128
+
129
+ var exists = grf.getFile(filename, function(data){
130
+ var buf = Buffer.from(new Uint8Array(data))
131
+ process.stdout.write(buf)
132
+ })
133
+
134
+ if(exists === false) {
135
+ console.error("File '%s' not in the .grf, don't forget to put the data/ folder in front.", filename)
136
+ console.error("Example: -e data/clientinfo.xml")
137
+ }
138
+
139
+ return
140
+ }
141
+
142
+ // Load CPS decryption key if provided
143
+ var Cluster = require('cluster')
144
+ var cpsSbox = null
145
+ if(typeof arg.k === 'string') {
146
+ var Cps = require('./lib/cps.js')
147
+ try {
148
+ cpsSbox = Cps.loadKey(arg.k)
149
+ if(Cluster.isMaster) {
150
+ console.log("Loaded CPS decryption key from %s", arg.k)
151
+ }
152
+ } catch(err) {
153
+ console.error("Failed to load CPS key:", err.message)
154
+ return
155
+ }
156
+ }
157
+
158
+ // Default action, extract the entire grf
159
+ var extraction = grf.extract({
160
+ output: (typeof arg.o !== 'string' ? '' : arg.o),
161
+ concurrency: arg.c,
162
+ cpsSbox: cpsSbox,
163
+ })
164
+
165
+ extraction.on('start', function() {
166
+ console.log("Extraction has started on %d files.", grf.entries.length)
167
+
168
+ // Pretty progress bar extraction on process
169
+ var bar = new ProgressBar('Progress [:current/:total] [:bar] :percent :elapseds - :etas', {
170
+ width: 100,
171
+ total: grf.entries.length
172
+ })
173
+ var last = 0
174
+ var timer = setInterval(function () {
175
+ var progress = extraction.progress()
176
+ var rate = progress - last
177
+
178
+ last = progress
179
+
180
+ bar.tick(rate)
181
+
182
+ if (bar.complete) {
183
+ console.log("Finished extracting GRF.")
184
+ process.nextTick(process.exit)
185
+ }
186
+ }, 100)
187
+ })
188
+
189
+ extraction.on('error', function(err) {
190
+ console.error("Extraction error", err)
191
+ })
192
+
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Loaders/GameFile.js
3
+ *
4
+ * Loaders for Gravity .grf file (Game RO File)
5
+ *
6
+ * This file is part of ROBrowser, Ragnarok Online in the Web Browser (http://www.robrowser.com/).
7
+ *
8
+ * @author Vincent Thibault
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ var GameFileDecrypt = require('./GameFileDecrypt');
14
+ var BinaryReader = require('../Utils/BinaryReader');
15
+ var Inflate = require('../Utils/Inflate');
16
+ var Struct = require('../Utils/Struct');
17
+
18
+
19
+ /**
20
+ * GRF Constructor
21
+ *
22
+ * @param {File} data
23
+ */
24
+ function GRF( data )
25
+ {
26
+ if (data) {
27
+ this.load( data );
28
+ }
29
+ }
30
+
31
+
32
+ /**
33
+ * @var {File System} Nodejs
34
+ */
35
+ var fs = require('fs');
36
+
37
+
38
+ /**
39
+ * GRF Constants
40
+ */
41
+ GRF.FILELIST_TYPE_FILE = 0x01; // entry is a file
42
+ GRF.FILELIST_TYPE_ENCRYPT_MIXED = 0x02; // encryption mode 0 (header DES + periodic DES/shuffle)
43
+ GRF.FILELIST_TYPE_ENCRYPT_HEADER = 0x04; // encryption mode 1 (header DES only)
44
+
45
+
46
+ /**
47
+ * GRF Structures
48
+ */
49
+ GRF.struct_header = new Struct(
50
+ 'unsigned char signature[15]',
51
+ 'unsigned char key[15]',
52
+ 'unsigned long file_table_offset',
53
+ 'unsigned long skip',
54
+ 'unsigned long filecount',
55
+ 'unsigned long version'
56
+ );
57
+
58
+ GRF.struct_table = new Struct(
59
+ 'unsigned long pack_size',
60
+ 'unsigned long real_size'
61
+ );
62
+
63
+ GRF.struct_entry = new Struct(
64
+ 'unsigned long pack_size',
65
+ 'unsigned long length_aligned',
66
+ 'unsigned long real_size',
67
+ 'unsigned char type',
68
+ 'unsigned long offset'
69
+ );
70
+
71
+
72
+ /**
73
+ * GRF METHODs
74
+ */
75
+ GRF.prototype.file = null;
76
+ GRF.prototype.reader = null;
77
+ GRF.prototype.header = null;
78
+ GRF.prototype.table = null;
79
+
80
+
81
+ /**
82
+ * Loading GRF
83
+ *
84
+ * @param {File} file
85
+ */
86
+ GRF.prototype.load = function Load( file )
87
+ {
88
+ // Global object
89
+ this.file = file;
90
+
91
+ // node.js
92
+ this.reader = (typeof FileReaderSync !== 'undefined' ? new FileReaderSync() : {});
93
+
94
+
95
+ // Local object
96
+ var buffer, fp;
97
+ var header, entries, table;
98
+ var reader = this.reader;
99
+ var data, out;
100
+ var i, count;
101
+
102
+
103
+ // Load grf
104
+ file.slice = file.slice || file.webkitSlice || file.mozSlice;
105
+ reader.load = function( start, len ) {
106
+ // node.js
107
+ if (fs && file.fd) {
108
+ var buffer = Buffer.alloc(len);
109
+ fs.readSync(file.fd, buffer, 0, len, start);
110
+ return buffer.buffer;
111
+ }
112
+
113
+ return reader.readAsArrayBuffer(
114
+ file.slice( start, start+len )
115
+ );
116
+ };
117
+
118
+
119
+ // Check if file has enought content.
120
+ if (file.size < GRF.struct_header.size) {
121
+ throw new Error('GRF::load() - Not enough bytes to be a valid GRF');
122
+ }
123
+
124
+ // Read the header
125
+ buffer = reader.load( 0, GRF.struct_header.size );
126
+ fp = new BinaryReader(buffer);
127
+ header = fp.readStruct( GRF.struct_header );
128
+
129
+ header.signature = String.fromCharCode.apply( null, header.signature);
130
+ header.filecount -= header.skip + 7;
131
+
132
+
133
+ // Check file header
134
+ if (header.signature !== 'Master of Magic') {
135
+ throw new Error('GRF::load() - Incorrect header "' + header.signature + '", must be "Master of Magic".');
136
+ }
137
+
138
+ if (header.version !== 0x200) {
139
+ throw new Error('GRF::load() - Incorrect version "0x' + parseInt(header.version, 10).toString(16) + '", just support version "0x200"');
140
+ }
141
+
142
+ if (header.file_table_offset + GRF.struct_header.size > file.size || header.file_table_offset < 0) {
143
+ throw new Error('GRF::load() - Can\'t jump to table list (' + header.file_table_offset + '), file length: ' + file.size);
144
+ }
145
+
146
+ // Load Table Info
147
+ buffer = reader.load( header.file_table_offset + GRF.struct_header.size, GRF.struct_table.size );
148
+ fp = new BinaryReader( buffer );
149
+ table = fp.readStruct( GRF.struct_table );
150
+
151
+ // Load Table Data
152
+ buffer = reader.load( header.file_table_offset + GRF.struct_header.size + GRF.struct_table.size, table.pack_size );
153
+ data = new Uint8Array(buffer);
154
+ out = new Uint8Array(table.real_size);
155
+
156
+ // Uncompress data
157
+ (new Inflate(data)).getBytes(out);
158
+
159
+ // Load entries
160
+ entries = loadEntries(out, header.filecount);
161
+
162
+ // Store table data (used for regex search in tablelist)
163
+ // Set filename to lowercase (case insensitive in official client)
164
+ table.data = '';
165
+ for (i = 0, count = entries.length; i < count; ++i) {
166
+ table.data += entries[i].filename + '\0';
167
+ entries[i].filename = entries[i].filename.toLowerCase();
168
+ }
169
+
170
+ // Sort entries (for binary search)
171
+ entries.sort(sortEntries);
172
+
173
+ this.header = header;
174
+ this.entries = entries;
175
+ this.table = table;
176
+ };
177
+
178
+
179
+ /**
180
+ * Load entries
181
+ * Note: this function is quiet intensive, BinaryReader is slowing down
182
+ * the process and generate too much memory to garbage (GC pause of 6sec).
183
+ *
184
+ * @param {Uint8Array} content table
185
+ * @param {number} file count
186
+ */
187
+ function loadEntries( out, count )
188
+ {
189
+ // Read all entries
190
+ var i, pos, str;
191
+ var entries = new Array(count);
192
+
193
+ for (i = 0, pos = 0; i < count; ++i) {
194
+ str = '';
195
+ while (out[pos]) {
196
+ str += String.fromCharCode(out[pos++]);
197
+ }
198
+ pos++;
199
+
200
+ entries[i] = {
201
+ filename: str,
202
+ pack_size: (out[pos++] | out[pos++] << 8 | out[pos++] << 16 | out[pos++] << 24) >>> 0,
203
+ length_aligned: (out[pos++] | out[pos++] << 8 | out[pos++] << 16 | out[pos++] << 24) >>> 0,
204
+ real_size: (out[pos++] | out[pos++] << 8 | out[pos++] << 16 | out[pos++] << 24) >>> 0,
205
+ type: out[pos++],
206
+ offset: (out[pos++] | out[pos++] << 8 | out[pos++] << 16 | out[pos++] << 24) >>> 0
207
+ };
208
+ }
209
+
210
+ return entries;
211
+ }
212
+
213
+
214
+
215
+ /**
216
+ * Sort entries (to find it faster)
217
+ *
218
+ * @param {object} entry 1
219
+ * @param {object} entry 2
220
+ */
221
+ function sortEntries(a, b)
222
+ {
223
+ if (a.filename > b.filename) {
224
+ return 1;
225
+ }
226
+
227
+ if (a.filename < b.filename) {
228
+ return -1;
229
+ }
230
+
231
+ return 0;
232
+ }
233
+
234
+
235
+ /**
236
+ * Decode entry to return its content
237
+ *
238
+ * @param {ArrayBuffer}
239
+ * @param {Entry}
240
+ * @param {function} callback
241
+ */
242
+ GRF.prototype.decodeEntry = function DecodeEntry( buffer, entry, callback )
243
+ {
244
+ var out;
245
+
246
+ var data = new Uint8Array( buffer );
247
+
248
+ // Decode the file
249
+ if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_MIXED) {
250
+ console.log(11111111)
251
+ GameFileDecrypt.decodeFull( data, entry.length_aligned, entry.pack_size);
252
+ }
253
+ else if (entry.type & GRF.FILELIST_TYPE_ENCRYPT_HEADER) {
254
+ console.log(22222222)
255
+ GameFileDecrypt.decodeHeader( data, entry.length_aligned );
256
+ }
257
+
258
+ // Uncompress
259
+ out = new Uint8Array(entry.real_size);
260
+ (new Inflate(data)).getBytes(out);
261
+
262
+ callback(out.buffer);
263
+ };
264
+
265
+
266
+ /**
267
+ * Binary Search hack to find files in GRF list
268
+ *
269
+ * @param {string} filename
270
+ */
271
+ GRF.prototype.search = function searchClosure()
272
+ {
273
+ var range = new Uint32Array(2);
274
+
275
+ return function search( filename )
276
+ {
277
+ var entries = this.entries;
278
+ var v = 0;
279
+ var middle = 0;
280
+
281
+ range[1] = 0;
282
+ range[0] = entries.length - 1;
283
+
284
+ while (range[1] < range[0]) {
285
+ middle = range[1] + ((range[0]-range[1]) >> 1);
286
+ v = (entries[middle].filename < filename) | 0;
287
+ range[v] = middle + v;
288
+ }
289
+
290
+ if (range[1] < entries.length && entries[range[1]].filename === filename) {
291
+ return range[1];
292
+ }
293
+
294
+ return -1;
295
+ };
296
+ }();
297
+
298
+
299
+
300
+ /**
301
+ * Find a file in the GRF
302
+ *
303
+ * @param {string} filename
304
+ * @param {function} callback
305
+ */
306
+ GRF.prototype.getFile = function getFile( filename, callback )
307
+ {
308
+ // Not case sensitive...
309
+ var path = filename.toLowerCase();
310
+ var entry, blob;
311
+ var reader;
312
+
313
+ var pos = this.search(path);
314
+
315
+ // If filename is find in GRF table list
316
+ if (pos !== -1) {
317
+ entry = this.entries[pos];
318
+
319
+ // Directory ?
320
+ if (!(entry.type & GRF.FILELIST_TYPE_FILE)) {
321
+ return false;
322
+ }
323
+
324
+ // node.js
325
+ if (fs && this.file.fd) {
326
+ var buffer = new Buffer(entry.length_aligned);
327
+ var grf = this;
328
+
329
+ fs.read(this.file.fd, buffer, 0, entry.length_aligned, entry.offset + GRF.struct_header.size, function(err, bytesRead, buffer){
330
+ if(err) throw err;
331
+ grf.decodeEntry( (new Uint8Array(buffer)).buffer, entry, callback);
332
+ });
333
+ return true;
334
+ }
335
+
336
+ blob = this.file.slice(
337
+ entry.offset + GRF.struct_header.size,
338
+ entry.offset + GRF.struct_header.size + entry.length_aligned
339
+ );
340
+
341
+ // Load into memory
342
+ if (self.FileReader) {
343
+ var grf = this;
344
+
345
+ reader = new FileReader();
346
+ reader.onload = function(){
347
+ grf.decodeEntry( reader.result, entry, callback);
348
+ };
349
+ reader.readAsArrayBuffer(blob);
350
+ }
351
+
352
+ // Firefox doesn't seems to support FileReader in web worker
353
+ else {
354
+ reader = new FileReaderSync();
355
+ this.decodeEntry( reader.readAsArrayBuffer(blob), entry, callback );
356
+ }
357
+
358
+ return true;
359
+ }
360
+
361
+ return false;
362
+ };
363
+
364
+
365
+ /**
366
+ * Export
367
+ */
368
+ module.exports = GRF;