From c783adece3dd99712b08075aa67e358146d3d3da Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Thu, 28 Jun 2018 20:50:10 +0200 Subject: [PATCH 1/6] Changed some of the package.json dependencies to make it easier to install with npm. Updated both pngquant and canvas, pngquant now includes a binary with their more recent versions which means less requirements to compile. --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 652ac4f..274275d 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,13 @@ "dependencies": { "async": "0.2.10", "buffer-crc32": "^0.2.5", - "canvas": "1.1.6", + "canvas": "1.6.11", "commander": "2.2.0", "fs-extra": "0.8.1", "glob": "4.3.1", "image-optim": "0.3.0", "js-beautify": "1.4.2", - "node-pngquant-native": "0.0.12" + "node-pngquant-native": "2.0.1" }, "repository": { "type": "git", @@ -32,4 +32,4 @@ "url": "https://github.com/Wizcorp/jeff/blob/master/LICENSE" } ] -} \ No newline at end of file +} From c677e6f7d4f877d84128dd327c1911c7717dd384 Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Fri, 29 Jun 2018 19:46:28 +0200 Subject: [PATCH 2/6] Getting to know the code. - Making it possible to specify a version you want to try to export as - Making it possible to turn on/off different log levels - Preparing for the possibility of LZMA file compression, a rare format - Passing Versioning info deeper into the parsing mechanism, and streaming, so that versions can be used to circumvent problems in different decoding methods. --- bin/jeff | 6 +++++- src/Gordon/base.js | 3 ++- src/Gordon/parser.js | 32 ++++++++++++++++++++++++-------- src/Gordon/stream.js | 17 +++++++++++------ src/index.js | 31 +++++++++++++++++++++++++------ 5 files changed, 67 insertions(+), 22 deletions(-) diff --git a/bin/jeff b/bin/jeff index 231f6f0..f2d0857 100755 --- a/bin/jeff +++ b/bin/jeff @@ -36,6 +36,8 @@ program .option('-I, --ignoreImages ', 'Not to export images', 'false') .option('-F, --filtering ', 'Filtering that should be used when rendering the animation', 'linear') .option('-e, --outlineEmphasis ', 'Emphasis of outlines when rendering Flash vectorial drawings', '1') +.option('-V, --version ', 'If specified, force decompiler to attempt to decompile as if the swf was specified version', '-1') +.option('-L, --verbosity ', '(L)og Level. 1-10, 1 showing least, 10 showing the most. Default is 3', '3') .parse(process.argv); @@ -67,7 +69,9 @@ var exportParams = { ignoreData: JSON.parse(program.ignoreData), ignoreImages: JSON.parse(program.ignoreImages), filtering: program.filtering, - outlineEmphasis: JSON.parse(program.outlineEmphasis) + outlineEmphasis: JSON.parse(program.outlineEmphasis), + forceVersion: JSON.parse(program.version), + verbosity: JSON.parse(program.verbosity) }; // Creating a new Jeff diff --git a/src/Gordon/base.js b/src/Gordon/base.js index dc608ae..f770b1a 100644 --- a/src/Gordon/base.js +++ b/src/Gordon/base.js @@ -17,7 +17,8 @@ var Base = { }, validSignatures: { SWF: 'FWS', - COMPRESSED_SWF: 'CWS' + COMPRESSED_ZLIB_SWF: 'CWS', + COMPRESSED_LZMA_SWF: 'ZWS' }, readyStates: { LOADING: 0, diff --git a/src/Gordon/parser.js b/src/Gordon/parser.js index 4aa7805..c24a86a 100644 --- a/src/Gordon/parser.js +++ b/src/Gordon/parser.js @@ -2,6 +2,7 @@ 'use strict'; +var util = require('util'); var Base = require('./base.js'); var Stream = require('./stream.js'); @@ -42,25 +43,40 @@ SwfParser.prototype = { // swfData is a buffer containing the SWF data // onData(object) will be called several times with the result of the parsing of 1 object // cb(error) will be called at the end or upon error - parse: function (swfName, swfData, onData, cb) { + parse: function (swfName, swfData, options, onData, cb) { this.swfName = swfName; + this._options = options; this.onData = onData; - var stream = new Stream(swfData); + + var stream = new Stream(swfData, options); this._readSwlTable(stream); // Symbol table not used var sign = stream.readString(3); - this.version = stream.readUI8(); + var version = stream.readUI8(); this.fileLen = stream.readUI32(); + if(this._options.forceVersion == null || this._options.forceVersion == -1){ + this._options.version = version; + } + else{ + this._options.version = forceVersion; + } + + if(this._options.verbosity > 3){ + console.log('Attempting to decompile for SWF Version:', this._options.version); + } + var signatures = Base.validSignatures; - if (sign === signatures.COMPRESSED_SWF) { + if (sign === signatures.COMPRESSED_ZLIB_SWF) { var self = this; stream.decompressAsync(function () { self._parseSwf(stream, cb); }); } else if (sign === signatures.SWF) { this._parseSwf(stream, cb); + } else if (sign === signatures.COMPRESSED_LZMA_SWF) { + cb('No Support for LZMA Compressed SWF file: ' + swfName); } else { cb('Invalid SWF file: ' + swfName); } @@ -104,7 +120,7 @@ SwfParser.prototype = { //create a "header" object with file level info this.onData({ type: 'header', - version: this.version, + version: this._options.version, fileLength: this.fileLen, frameSize: stream.readRect(), frameRate: stream.readUI16() / 256 @@ -977,7 +993,7 @@ SwfParser.prototype = { isUTF8: stream.readBool(), isItalic: stream.readBool(), isBold: stream.readBool(), - languageCode: (this.version > 5) ? stream.readLanguageCode() : 0, + languageCode: (this._options.version > 5) ? stream.readLanguageCode() : 0, name: stream.readUI8() && stream.readString() }; @@ -1022,7 +1038,7 @@ SwfParser.prototype = { _handleDefineFont3: function (stream, offset, len) { var startingOffset = stream.offset; -// console.error('version', this.version) +// console.error('version', this._options.version) // console.error('offset -1', stream.offset) var id = stream.readUI16(); // console.error('offset 0', stream.offset) @@ -1037,7 +1053,7 @@ SwfParser.prototype = { isUTF8: stream.readBool(), isItalic: stream.readBool(), isBold: stream.readBool(), - languageCode: (this.version > 5) ? stream.readLanguageCode() : 0, + languageCode: (this._options.version > 5) ? stream.readLanguageCode() : 0, name: stream.readUI8() && stream.readString() }; diff --git a/src/Gordon/stream.js b/src/Gordon/stream.js index 184ecb2..482efe8 100644 --- a/src/Gordon/stream.js +++ b/src/Gordon/stream.js @@ -3,11 +3,13 @@ var zlib = require('zlib'); -function Stream(data){ +function Stream(data, options){ this._buffer = new Buffer(data); this._nBytes = data.length; this.offset = 0; + this._options = options; + this._bitBuffer = null; this._bitOffset = 8; this._lastOffset = 0; @@ -217,14 +219,17 @@ module.exports = Stream; // We simply skip this useless zeros here, but verify it was actually zeros we skipped. // If one day we find something else than an all zero rect (e.g. a 0,0,1,1) we will // have to adapt some more here... - + // Edit: it looks like this hack is making the export fail on some swf // (may be those from the latest flash versions?) // So the bytes need to be tested in order to know whether the hack is necessary - if (this._buffer.readUInt16LE(this.offset) === 0) { - var empty = this.readBytes(16); //console.warn('numBits=1, rect:',rect) - for(var i = 0; i < 16; i++) { - if(empty[i] !== 0) { throw new Error('Unexpected rectangle data'); } + if(this._options.version < 30)//Not exactly sure which version makes this work/break + { + if (this._buffer.readUInt16LE(this.offset) === 0) { + var empty = this.readBytes(16); //console.warn('numBits=1, rect:',rect) + for(var i = 0; i < 16; i++) { + if(empty[i] !== 0) { throw new Error('Unexpected rectangle data'); } + } } } } diff --git a/src/index.js b/src/index.js index ed9c5ac..686dfd7 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ /* jshint camelcase: false */ //for js_beautify and indent_size 'use strict'; +var util = require('util'); var fs = require('fs-extra'); var glob = require('glob'); @@ -20,6 +21,11 @@ var JSON_WRITE_OPTIONS = { encoding:'binary' }; // Jeff's only API method function extractSwf(exportParams, cb) { + + if(exportParams.verbosity > 8){ + console.log('Options: '+util.inspect(exportParams)); + } + var jeff = new Jeff(); // Setting up Jeff ... @@ -104,6 +110,8 @@ function JeffOptions(params) { this.customWriteFile = params.customWriteFile; this.customReadFile = params.customReadFile; this.fixedSize = params.fixedSize; + this.forceVersion = params.forceVersion; + this.version = params.version; this.fallbackFrameRate = params.fallbackFrameRate || 25; // 1: minimal log level, 10: maximum log level. TODO: needs to be implemented on every console.warn/log/error this.verbosity = params.verbosity || 3; @@ -227,15 +235,26 @@ Jeff.prototype._parseFile = function (swfName, nextSwfCb) { var swfObjects = []; function onFileRead(error, swfData) { if (error) return nextSwfCb(error); - self._parser.parse(swfName, swfData, + self._parser.parse(swfName, swfData, self._options, function (swfObject) { var id = swfObject.id; swfObject._swfName = swfName; - // console.log('object', JSON.stringify(swfObject)); - // console.log('properties!!', swfObject.id); - // for (var p in swfObject) { - // console.log(p); - // } + + if(self._options.verbosity > 5) + { + + console.log('parsing: ' + swfName); + + //console.log('object', JSON.stringify(swfObject)); + console.log(''); + console.log('--Object-- '); + console.log('Id', swfObject.id); + console.log('Type', swfObject.type); + for (var p in swfObject) { + console.log(p); + } + } + if (id === undefined) { if (swfObject.type === 'scalingGrid') { From 5be137b38624dc07bcc781cca72656747e960357 Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Sun, 1 Jul 2018 20:53:32 +0200 Subject: [PATCH 3/6] Added a way to parse stored mp3 files. Started working on a way to parse streamed mp3 files but I couldn't find any swf examples of this, leaving the code here in hopes it will help in future commits. Temporarily storing Audio bytes in parser.js and saving them to disk in index.js's _extractClassGroup(). I assume there is a better place for this, I need to understand the index.js _processFileGroup() code a bit more. --- src/Gordon/base.js | 16 ++++++++ src/Gordon/parser.js | 93 ++++++++++++++++++++++++++++++++++++++++++-- src/index.js | 5 +++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/Gordon/base.js b/src/Gordon/base.js index f770b1a..1b483e7 100644 --- a/src/Gordon/base.js +++ b/src/Gordon/base.js @@ -304,6 +304,22 @@ var Base = { 13: 'overlay', 14: 'hardlight' }, + soundFormats: { + UNCOMPRESSED_NATIVE_ENDIAN: 0, //swf 1 + ADPCM: 1, //swf 1 + MP3: 2, //swf 4 + UNCOMPRESSED_LITTLE_ENDIAN: 3, //swf 4 + NELLYMOSER16KHZ: 4, //swf 10 + NELLYMOSER8KHZ: 5, //swf 10 + NELLYMOSER: 6, //swf 6 + SPEEX: 11 //swf 10 + }, + soundRates: { + 0: 5500, + 1: 11000, + 2: 22000, + 3: 44000 + } }; (function () { diff --git a/src/Gordon/parser.js b/src/Gordon/parser.js index c24a86a..19713c6 100644 --- a/src/Gordon/parser.js +++ b/src/Gordon/parser.js @@ -44,6 +44,7 @@ SwfParser.prototype = { // onData(object) will be called several times with the result of the parsing of 1 object // cb(error) will be called at the end or upon error parse: function (swfName, swfData, options, onData, cb) { + this.dataStorage = []; this.swfName = swfName; this._options = options; this.onData = onData; @@ -918,11 +919,19 @@ SwfParser.prototype = { while (true) { var flags = stream.readUI8(); if (!flags) break; - var objId = stream.readUI16(), - depth = stream.readUI16(), + var objId = stream.readUI16(); + var pointingTo; + if(d[objId] != null) { + pointingTo = d[objId].id; + } + else { + console.warn('Button '+id+' is pointing to a non existing resource'); + } + + var depth = stream.readUI16(), state = 0x01, character = { - id: d[objId].id, + id: pointingTo, depth: depth, matrix: stream.readMatrix() }; @@ -959,6 +968,84 @@ SwfParser.prototype = { frm.bgcolor = stream.readRGB(); }, + _handleSoundStreamHead2: function (stream, offset, len) { + var f = Base.soundFormats; + var headerSize = 4;//7 bytes as defined next + if(len == 4) + { + //just the header... absolutly no data? how/why is this possible? + this._skipEndOfTag('SoundStreamHead2', true, stream, offset, len); + return; + } + + var info = { + reserved: stream.readUB(4), //not used, always 0 + playbackSoundRate: stream.readUB(2), //enum: soundRates + playbackSoundSize: stream.readUB(1), //0: '8Bit', 1: '16Bit' + playbackSoundType: stream.readUB(1), //0: 'Mono', 1: 'Stereo' + streamSoundFormat: stream.readUB(4), //enum: soundFormats + streamSoundRate: stream.readUB(2), //enum: soundRates + streamSoundSize: stream.readUB(1), //0: '8Bit', 1: '16Bit' + streamSoundType: stream.readUB(1), //0: 'Mono', 1: 'Stereo' + streamSoundSampleCount: stream.readUI16() //--- in a single frame, I'm assuming there can't be more than 16 bits worth of sample + }; + + if(info.streamSoundRate == f.MP3) + { + info.latencySeek = stream.readSI16(); //only used for mp3 + } + + var soundData = stream.readBytes(len - headerSize);//is only a tiny bit of the information, todo: needs to be stored and joined together with the other audios of this block + + console.warn('SoundStreamHead2 has '+ (len - headerSize) +' bytes of audio data which have not been handled.'); + }, + + _handleDefineSound: function (stream, offset, len) { + var f = Base.soundFormats; + var headerSize = 7;//7 bytes as defined next + + var info = { + soundId: stream.readUI16(), + soundFormat: stream.readUB(4), + soundRate: stream.readUB(2), //ignored for nellymoser and speex -- enums found in "soundRates" + soundSize: stream.readUB(1), //only matters for uncompressed, compressed always becomes 16bit. 0: '8Bit', 1: '16Bit' + soundType: stream.readUB(1), //Mono or stereo, ignored for Nellymoser and Speex, 0: 'Mono', 1: 'Stereo' + soundSampleCount: stream.readUI32() + }; + + var fileToSave; + + switch(info.soundFormat){ + case f.UNCOMPRESSED_NATIVE_ENDIAN: + case f.ADPCM: + case f.UNCOMPRESSED_LITTLE_ENDIAN: + case f.NELLYMOSER16KHZ: + case f.NELLYMOSER8KHZ: + case f.NELLYMOSER: + case f.SPEEX: + console.warn('Sound format: '+info.soundFormat+' not supported'); + break; + case f.MP3: + var soundData = stream.readBytes(len - headerSize); + fileToSave = { + fileName: info.soundId+'.mp3', + bytes: soundData + }; + break; + default: + throw new Error('Sound format not known: '+info.soundFormat); + } + + if(fileToSave != null) + { + this.dataStorage.push(fileToSave); + } + + if(this._options.verbosity > 5){ + console.log('Sound Found:\n'+util.inspect(info)+'\n'); + } + }, + _handleDefineFont: function (stream) { var id = stream.readUI16(), numGlyphs = stream.readUI16() / 2, diff --git a/src/index.js b/src/index.js index 686dfd7..d5b742e 100644 --- a/src/index.js +++ b/src/index.js @@ -356,6 +356,11 @@ Jeff.prototype._extractClassGroup = function (imageList, graphicProperties, next if (!this._options.ignoreData) { this._writeDataToDisk(data); } + for (var i = 0; i < this._parser.dataStorage.length; i += 1) { + + writeFile(this._fileGroupName + this._parser.dataStorage[i].fileName, this._parser.dataStorage[i].bytes); + } + } if (this._options.returnData) { From b0d41c81ed06cd24add413ef962b769f3533e5ba Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Sun, 1 Jul 2018 21:36:17 +0200 Subject: [PATCH 4/6] Some cleanup of redundant logs and some organization to them. --- src/index.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index d5b742e..83b980d 100644 --- a/src/index.js +++ b/src/index.js @@ -229,29 +229,30 @@ Jeff.prototype._parseFileGroup = function (fileGroup, nextGroupCb) { Jeff.prototype._parseFile = function (swfName, nextSwfCb) { if (this._options.verbosity >= 5) { - console.log('parsing: ' + swfName); + console.log('=================================');//to quickly find the start in a wall of text + console.log('Parsing: ' + swfName); + console.log('================================='); } var self = this; var swfObjects = []; function onFileRead(error, swfData) { if (error) return nextSwfCb(error); self._parser.parse(swfName, swfData, self._options, - function (swfObject) { + function (swfObject) {//include a .info on the object if you want to display more information var id = swfObject.id; swfObject._swfName = swfName; if(self._options.verbosity > 5) { - - console.log('parsing: ' + swfName); - - //console.log('object', JSON.stringify(swfObject)); - console.log(''); - console.log('--Object-- '); + console.log('\n--Object-- '); console.log('Id', swfObject.id); console.log('Type', swfObject.type); - for (var p in swfObject) { - console.log(p); + //for (var p in swfObject) { + //console.log(p); + //} + //console.log('object', JSON.stringify(swfObject)); + if(swfObject.info != null) { + console.log(util.inspect(swfObject.info)); } } From ae5d9e3bd850445430acde591f78a57752198f67 Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Sun, 1 Jul 2018 21:42:34 +0200 Subject: [PATCH 5/6] Moving DefineBinaryData parsing out of the ignored tags area, and moved below the dashes. Also gave it a purpose. While we may not know what the binary is, it could still be relevant for the parsing. In my test cases, I had a newer project which the company had stored the mp3 as a binary file, this is a common way to store things in a swf. I changed it to output all binaries with a .binary ending, and let them try to guess what they are. Also fixing up some of my code to more properly adhere to the standards, returning onData and centralizing the data logging. --- src/Gordon/parser.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/Gordon/parser.js b/src/Gordon/parser.js index 19713c6..d72a5a1 100644 --- a/src/Gordon/parser.js +++ b/src/Gordon/parser.js @@ -269,9 +269,8 @@ SwfParser.prototype = { _handleCsmTextSettings: function (stream, offset, len) { this._skipEndOfTag('CsmTextSettings', true, stream, offset, len); }, - _handleDefineBinaryData: function (stream, offset, len) { - this._skipEndOfTag('DefineBinaryData', true, stream, offset, len); - }, + + _handleDefineEditText: function (stream, offset, len) { var id = stream.readUI16(); var edit = { type: 'fakeEditText', id: id }; @@ -368,7 +367,26 @@ SwfParser.prototype = { if (code !== 0 || len !== 1) throw new Error('Unexpected value in undocumented1 tag: ' + code + ',' + len); }, //--------------------------------------------------------- + _handleDefineBinaryData: function (stream, offset, len) { + var headerSize = 6; + var info = { + binaryId: stream.readUI16(), //16bit id + reserved: stream.readUB(32) //reserved must be 0 + } + var data = stream.readBytes(len - headerSize); + + var fileToSave = { + id: info.binaryId, + type: 'binary', + fileName: info.binaryId+'.unknown', + info: info, + bytes: data + }; + + this.dataStorage.push(fileToSave); + this.onData(fileToSave); + }, _handleSymbolClass: function (stream, offset, len, frm) { var count = stream.readUI16(); while (count--) { @@ -1028,7 +1046,10 @@ SwfParser.prototype = { case f.MP3: var soundData = stream.readBytes(len - headerSize); fileToSave = { + id: info.soundId, + type: 'sound', fileName: info.soundId+'.mp3', + info: info, bytes: soundData }; break; @@ -1041,9 +1062,7 @@ SwfParser.prototype = { this.dataStorage.push(fileToSave); } - if(this._options.verbosity > 5){ - console.log('Sound Found:\n'+util.inspect(info)+'\n'); - } + this.onData(fileToSave); }, _handleDefineFont: function (stream) { From 757fcc6e8b6a11eae39383386b948436171cfe87 Mon Sep 17 00:00:00 2001 From: Jonathan Callis Date: Sun, 1 Jul 2018 21:55:29 +0200 Subject: [PATCH 6/6] House cleaning, not sure which versioning system is being used here, but I guessed an appropriate number change based on previous commits. Updated readme to show how to get more documentation as user, and swf format link. --- README.md | 8 +++++++- package.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7714e39..f863277 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,12 @@ jeff(options, function (error, stats, extractedData) { * Will sequentially parse and process the SWF files. * Will export JSON meta-data and images corresponding to the SWF files. +For a list of command line options: + +```shell +jeff -h +``` + ### Roadmap for unsupported features * Texts (Static/Dynamic) @@ -98,4 +104,4 @@ jeff(options, function (error, stats, extractedData) { * Option to extract shapes under vectorial format * Option to extract meta-data under keyframe based format (as opposed to per frame transformation matrix) -For contributors, see [SWF File Format Specifications](http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/swf/pdf/swf-file-format-spec.pdf) +For contributors, see [SWF File Format Specifications](https://wwwimages2.adobe.com/content/dam/acom/en/devnet/pdf/swf-file-format-spec.pdf) \ No newline at end of file diff --git a/package.json b/package.json index 274275d..a4ffb49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "jeff", "description": "Extracts Json meta-data and images from a SWF Flash file.", - "version": "0.2.10", + "version": "0.3.0", "private": false, "author": { "name": "Brice Chevalier",