Advertisement
Aussiemon

read-bundles.js

Jan 19th, 2018
150
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 12.58 KB | None | 0 0
  1. const assert = require('assert');
  2. const fs = require('fs');
  3. const globule = require('globule');
  4. const mkdirp = require('mkdirp');
  5. const murmurhash = require('murmurhash-native');
  6. const path = require('path');
  7. const process = require('process');
  8. const util = require("util");
  9. const zlib = require('zlib');
  10.  
  11. // ==================================================================
  12.  
  13. // Change this value to choose extension
  14. const WANTED_EXT = 'package'; // set to 'none' to use all known extensions
  15. const KNOWN_EXTENSIONS = [ 'package', 'unit', 'lua', 'material', 'particles', 'shading_environment', 'vector_field', 'wwise_dep', 'font', 'mouse_cursor', 'texture', 'physics_properties', 'animation', 'bones', 'level', 'state_machine', 'bsi', 'animation_set', 'editor', 'entity', 'mod', 'physx_metadata', 'scene_cache', 'shader_import', 'shader_node', 'shader_source', 'statemachine_editor', 'texture_category', 'wwise_bank', 'wwise_bank_metadata', 'script_flow_nodes'];
  16.  
  17. // Change this value to choose a specific bundle name
  18. const DESIRED_BUNDLE = 'none'; // (names are 16 characters or 'none')
  19.  
  20. // Change this value to choose a specific patch file number
  21. const DESIRED_PATCH = 'none'; // (in format 'patch_xxx', 'patch_x' (VR only), 'base', or 'none')
  22.  
  23. // Change this value to true to ignore or focus on extracted files that do not have a dictionary match
  24. const IGNORE_UNDEFINED = false; // (accepted values are 'all', 'only', and false)
  25.  
  26. // Change this value to true to sort extracted files by their bundle of origin
  27. const SORT_BY_BUNDLE = false;
  28.  
  29. // Change this value to true to sort extracted files by their patch file number of origin
  30. const SORT_BY_PATCH = false;
  31.  
  32. // Change this value to choose between extracting from Vermintide 1, Vermintide 2, and Hero Trials VR
  33. const EXTRACTION_MODE = 'vt1'; // (options are 'vt1', 'vt2', and 'vr')
  34.  
  35. // ==================================================================
  36.  
  37. // Tracks successful matches and appends them to an output file
  38. var successfulDictionary = [];
  39. var undefinedCounter = 0
  40.  
  41. function FileReader(filename) {
  42. this.fd = fs.openSync(filename, 'r');
  43. this.path = filename;
  44. }
  45. FileReader.prototype = {
  46.  
  47. readBytes: function readBytes(count) {
  48. var result = Buffer.allocUnsafe(count);
  49. var bytesRead = fs.readSync(this.fd, result, 0, count, null);
  50. assert.strictEqual(bytesRead, count, 'bytesRead');
  51. return result;
  52. },
  53.  
  54. readUint: function readUint() {
  55. return this.readBytes(4).readUInt32LE(0);
  56. },
  57.  
  58. close: function close() {
  59. fs.closeSync(this.fd);
  60. this.fd = null;
  61. }
  62. };
  63. FileReader.prototype.constructor = FileReader;
  64.  
  65. function InflatingReader(fileReader) {
  66. this.fileReader = fileReader;
  67. this.pos = 0;
  68. this.buf = Buffer.allocUnsafe(0);
  69. }
  70. InflatingReader.prototype = {
  71.  
  72. readBytes: function readBytes(count) {
  73. var result = Buffer.allocUnsafe(count);
  74. var self = this;
  75. while (count !== 0) {
  76. if (self.pos == self.buf.length) {
  77. var chunkSize = this.fileReader.readUint();
  78. var chunkZipped = this.fileReader.readBytes(chunkSize);
  79. while (chunkSize > 0xFFFF) {
  80. // This is bizarre.
  81. count = Math.max(0, count - chunkSize);
  82.  
  83. chunkSize = this.fileReader.readUint();
  84. chunkZipped = Buffer.concat([ this.fileReader.readBytes(chunkSize), chunkZipped ]);
  85. }
  86. self.buf = zlib.inflateSync(chunkZipped);
  87. self.pos = 0;
  88. }
  89. var copySize = Math.min(self.buf.length - self.pos, count);
  90. self.buf.copy(result, result.length - count, self.pos, self.pos + copySize);
  91. self.pos += copySize;
  92. count -= copySize;
  93. }
  94. return result;
  95. },
  96.  
  97. readUint: function readUint() {
  98. return this.readBytes(4).readUInt32LE(0);
  99. }
  100. };
  101. InflatingReader.prototype.constructor = InflatingReader;
  102.  
  103. function Dictionary() {
  104. var lines = fs.readFileSync(path.join(__dirname, 'dictionary.txt')).toString().split("\n");
  105. var hasher = murmurhash.LE.murmurHash64;
  106. for (var i in lines) {
  107. var filename = lines[i];
  108. var hash = '' + hasher(filename);
  109. //console.log('adding hash '+ hash);
  110. this[hash] = filename;
  111. }
  112. }
  113.  
  114. const dictionary = new Dictionary();
  115. const fileVersions = {};
  116.  
  117. function processBundlefile(bundlefile, bundlefilePath, chosenExtensionIn, extensionHashIn) {
  118. var magic = bundlefile.readUint();
  119. //console.log('magic: ' + magic);
  120.  
  121. // A new bundle file format was added in 1.6 beta.
  122. var isNewFormat = (magic === 0xF0000005);
  123. assert(isNewFormat || (magic === 0xF0000004), 'magic');
  124.  
  125. var unzippedSize = bundlefile.readUint();
  126. //console.log('unzippedSize: ' + unzippedSize);
  127.  
  128. var padding = bundlefile.readUint();
  129. //console.log('padding: ' + padding);
  130. assert.strictEqual(padding, 0x0, 'padding');
  131.  
  132. var inflater = new InflatingReader(bundlefile);
  133. if (EXTRACTION_MODE === 'vr') {
  134. inflater.readBytes(260); // skip the initial header early, as well as an extra 4 bytes
  135. }
  136.  
  137. var entryCount = inflater.readUint();
  138. //console.log('entryCount: ' + entryCount);
  139.  
  140. // skip header
  141. if (EXTRACTION_MODE === 'vr') {
  142. inflater.readBytes(4); // skip the header remainder
  143. } else {
  144. inflater.readBytes(256);
  145. }
  146.  
  147. var containsScripts = false;
  148. for (var i = 0; i < entryCount; ++i) {
  149. var extensionHash = inflater.readBytes(8);
  150. var nameHash = inflater.readBytes(8);
  151. // New format has an extra 4-byte field here, I've seen values of 1 (seems to mean
  152. // luajit v1 bytecode) and 2 (not sure, might mean file no longer in use).
  153. // VR bundles do not seem to use this field.
  154. var flags = 0
  155. if (EXTRACTION_MODE != 'vr') {
  156. flags = isNewFormat ? inflater.readUint() : 0;
  157. }
  158. //console.log('entry hashes ' + i + ': ' + extensionHash.toString('hex') + ' ' + nameHash.toString('hex'));
  159.  
  160. // Print filename to console
  161. if (false) {
  162. var nameHashString = nameHash.toString('hex');
  163. var name = dictionary[nameHashString];
  164. var extensionHashString = extensionHash.toString('hex');
  165. var extension = dictionary[extensionHashString];
  166. var filePath = (name || nameHashString) + '.' + (extension || extensionHashString);
  167. console.log(' ' + filePath);
  168. }
  169.  
  170. containsScripts = containsScripts || (extensionHashIn === extensionHash.toString('hex'));
  171. }
  172.  
  173. if (containsScripts) {
  174. for (var i = 0; i < entryCount; ++i) {
  175. var extensionHash = inflater.readBytes(8);
  176. var nameHash = inflater.readBytes(8);
  177. //console.log(' entry hashes ' + i + ': ' + extensionHash.toString('hex') + ' ' + nameHash.toString('hex'));
  178.  
  179. var headerCount = inflater.readUint();
  180. var headerUnknown = inflater.readUint();
  181. var headers = [];
  182. for (var j = 0; j < headerCount; ++j) {
  183. var languageId = inflater.readUint();
  184. var size = inflater.readUint();
  185. var unknown = inflater.readUint();
  186. headers.push({ languageId: languageId, size: size, unknown: unknown });
  187. }
  188.  
  189. for (var j = 0; j < headerCount; ++j) {
  190. var entryData = inflater.readBytes(headers[j].size);
  191.  
  192. if ((extensionHashIn === extensionHash.toString('hex')) && (headers[j].languageId === 0)) {
  193. var nameHashString = nameHash.toString('hex');
  194. var name = dictionary[nameHashString];
  195. if (name != null) {
  196. successfulDictionary.push(name);
  197. } else {
  198. undefinedCounter += 1
  199. }
  200.  
  201. if ((name && (IGNORE_UNDEFINED != 'only')) || (!name && (IGNORE_UNDEFINED != 'all'))) {
  202. var filePath = (name || nameHashString) + '.' + chosenExtensionIn;
  203.  
  204. if (SORT_BY_PATCH) {
  205. if (EXTRACTION_MODE != 'vr') {
  206. if (bundlefilePath.includes('patch_')) {
  207. filePath = bundlefilePath.substring(bundlefilePath.length-9) + '/' + filePath
  208. } else {
  209. filePath = 'base_bundle/' + filePath
  210. }
  211. } else {
  212. if (bundlefilePath.includes('patch_')) {
  213. filePath = bundlefilePath.substring(bundlefilePath.length-7) + '/' + filePath
  214. } else {
  215. filePath = 'base_bundle/' + filePath
  216. }
  217. }
  218. }
  219.  
  220. if (SORT_BY_BUNDLE) {
  221. if (EXTRACTION_MODE != 'vr') {
  222. if (bundlefilePath.includes('patch_')) {
  223. filePath = bundlefilePath.substring(bundlefilePath.length-26, bundlefilePath.length-10) + '/' + filePath
  224. } else {
  225. filePath = bundlefilePath.substring(bundlefilePath.length-16) + '/' + filePath
  226. }
  227. } else { //
  228. if (bundlefilePath.includes('patch_')) {
  229. filePath = bundlefilePath.substring(bundlefilePath.length-24, bundlefilePath.length-8) + '/' + filePath
  230. } else {
  231. filePath = bundlefilePath.substring(bundlefilePath.length-16) + '/' + filePath
  232. }
  233. }
  234. }
  235.  
  236. console.log('extracting ' + filePath);
  237. //console.log(' size=' + headers[j].size + ' unknown=' + headers[j].unknown + ' @0=' +
  238. // entryData.readUInt32LE(0) + ' @4=' + entryData.readUInt32LE(4) + ' @8=' + entryData.readUInt32LE(8));
  239.  
  240. var localpath = path.join(process.argv[3] || '', filePath);
  241. mkdirp.sync(path.dirname(localpath));
  242.  
  243. // New format adds another 4-byte field, seems to always be 0.
  244. var headerByteCount = isNewFormat ? 12 : 8;
  245. var scriptEnd = headerByteCount + entryData.readUInt32LE(0);
  246. var scriptData = entryData.slice(headerByteCount, scriptEnd);
  247. fs.writeFileSync(localpath, scriptData);
  248. }
  249. }
  250. }
  251. }
  252. }
  253. }
  254.  
  255. function processDirectory(dirpath) {
  256. var bundleFiles = null;
  257.  
  258. // No desired bundle or patch to focus on
  259. if ((DESIRED_BUNDLE === 'none') && (DESIRED_PATCH === 'none')) {
  260. bundlefiles = globule.find(dirpath + '/*', '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data');
  261. // Desired bundle but no patch to focus on
  262. } else if ((DESIRED_BUNDLE != 'none') && (DESIRED_PATCH === 'none')) {
  263. bundlefiles = globule.find(dirpath + '/' + DESIRED_BUNDLE + '*', '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data');
  264. // No desired bundle but desired patch to focus on
  265. } else if ((DESIRED_BUNDLE === 'none') && (DESIRED_PATCH != 'none')) {
  266. if (DESIRED_PATCH != 'base') {
  267. bundlefiles = globule.find(dirpath + '/*.' + DESIRED_PATCH, '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data');
  268. } else {
  269. bundlefiles = globule.find(dirpath + '/*', '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data', '!' + dirpath + '/*.patch_*');
  270. }
  271. // Desired bundle and patch to focus on
  272. } else {
  273. if (DESIRED_PATCH != 'base') {
  274. bundlefiles = globule.find(dirpath + '/' + DESIRED_BUNDLE + '.' + DESIRED_PATCH, '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data');
  275. } else {
  276. bundlefiles = globule.find(dirpath + '/' + DESIRED_BUNDLE + '*', '!' + dirpath + '/*.stream*', '!' + dirpath + '/*.ini', '!' + dirpath + '/*.data', '!' + dirpath + '/*.patch_*');
  277. }
  278. }
  279. bundlefiles.sort(function(a, b) { return path.extname(a).localeCompare(path.extname(b)) });
  280.  
  281. for (var i in bundlefiles) {
  282. var bundlefilePath = bundlefiles[i];
  283. if (!fs.lstatSync(bundlefilePath).isDirectory()) {
  284. console.log('___ processing file: ' + bundlefilePath);
  285. try {
  286.  
  287. if (WANTED_EXT === 'none') {
  288. for (var j in KNOWN_EXTENSIONS) {
  289. var bundlefile = new FileReader(bundlefilePath);
  290. var chosenExtension = KNOWN_EXTENSIONS[j]
  291. var extensionHash = murmurhash.LE.murmurHash64(chosenExtension);
  292. processBundlefile(bundlefile, bundlefilePath, chosenExtension, extensionHash);
  293. if (bundlefile) {
  294. bundlefile.close();
  295. }
  296. }
  297. } else {
  298. var bundlefile = new FileReader(bundlefilePath);
  299. var chosenExtension = WANTED_EXT
  300. var extensionHash = murmurhash.LE.murmurHash64(chosenExtension);
  301. processBundlefile(bundlefile, bundlefilePath, chosenExtension, extensionHash);
  302. }
  303. }
  304. catch (excn) {
  305. console.log('error processing file: ' + bundlefilePath + ": " + excn);
  306. }
  307. finally {
  308. if (bundlefile && WANTED_EXT != 'none') {
  309. bundlefile.close();
  310. }
  311. }
  312. }
  313. }
  314. }
  315.  
  316. function printHash(str) {
  317. console.log(str + '=' + murmurhash.LE.murmurHash64(str));
  318. }
  319.  
  320. // Output successful match dictionary
  321. processDirectory(process.argv[2]);
  322. var outputPath = process.cwd();
  323. for (var s = 0, len = successfulDictionary.length; s < len; s++) {
  324. fs.appendFileSync(outputPath+"\\dictionary_matches.txt", successfulDictionary[s]+"\n");
  325. }
  326. if ((IGNORE_UNDEFINED != 'all') && (IGNORE_UNDEFINED != 'only')) {
  327. console.log("Matched " + successfulDictionary.length + " files, found " + undefinedCounter + " undefined files successfully")
  328. } else if (IGNORE_UNDEFINED === 'all') {
  329. console.log("Matched " + successfulDictionary.length + " files successfully")
  330. } else {
  331. console.log("Found " + undefinedCounter + " undefined files")
  332. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement