diff --git a/src/helpers/traceHelper.js b/src/helpers/traceHelper.js index 38c3bd9f4b..73424f64dd 100644 --- a/src/helpers/traceHelper.js +++ b/src/helpers/traceHelper.js @@ -1,31 +1,6 @@ 'use strict' var ui = require('../helpers/ui') module.exports = { - // util section - findLowerBound: function (target, changes) { - if (changes.length === 0) { - return undefined - } - - if (changes.length === 1) { - if (changes[0] > target) { - // we only a closest maximum, returning O - return 0 - } else { - return changes[0] - } - } - - var middle = Math.floor(changes.length / 2) - if (changes[middle] > target) { - return this.findLowerBound(target, changes.slice(0, middle)) - } else if (changes[middle] < target) { - return this.findLowerBound(target, changes.slice(middle, changes.length)) - } else { - return changes[middle] - } - }, - // vmTraceIndex has to point to a CALL, CODECALL, ... resolveCalledAddress: function (vmTraceIndex, trace) { var step = trace[vmTraceIndex] diff --git a/src/helpers/util.js b/src/helpers/util.js index bd0e69bfe9..143dbace90 100644 --- a/src/helpers/util.js +++ b/src/helpers/util.js @@ -39,5 +39,40 @@ module.exports = { ret.push(row) } return ret + }, + + /* + Binary Search: + Assumes that @arg array is sorted increasingly + returns the smallest i such that target >= changes[i] + returns arary.length - 1 (if all elements in array are smaller than target) + returns 0 (if target is smaller than the first element of array || if array is empty) + */ + findLowerBound: function (target, array) { + if (array.length === 0) { + return 0 + } + return findLowerBoundInternal(target, array, 0, array.length - 1) + } +} + +function findLowerBoundInternal (target, array, lowerbound, higherbound) { + if (array[higherbound] < target) { + return higherbound + } else if (array[lowerbound] > target) { + return lowerbound + } + var middle + while (lowerbound + 1 !== higherbound) { + middle = Math.floor((higherbound + lowerbound) / 2) + if (array[middle] > target) { + higherbound = middle + } else if (array[middle] < target) { + lowerbound = middle + higherbound = array.length - 1 + } else if (array[middle] === target) { + return middle + } } + return lowerbound } diff --git a/src/index.js b/src/index.js index d15b366946..6a17480e35 100644 --- a/src/index.js +++ b/src/index.js @@ -4,11 +4,12 @@ var Debugger = require('./ui/Ethdebugger') var BasicPanel = require('./ui/BasicPanel') var TraceManager = require('./trace/traceManager') var CodeManager = require('./code/codeManager') +var SourceMappingDecoder = require('./util/sourceMappingDecoder') if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { module.exports = modules() } -if (window) { +if (typeof (window) !== 'undefined') { window.remix = modules() } @@ -24,6 +25,9 @@ function modules () { Debugger: Debugger, VMdebugger: VMDebugger, BasicPanel: BasicPanel + }, + util: { + SourceMappingDecoder: SourceMappingDecoder } } } diff --git a/src/trace/traceManager.js b/src/trace/traceManager.js index 8d9215fd77..4347ef3c27 100644 --- a/src/trace/traceManager.js +++ b/src/trace/traceManager.js @@ -4,7 +4,8 @@ var TraceRetriever = require('./traceRetriever') var TraceCache = require('./traceCache') var TraceStepManager = require('./traceStepManager') var traceHelper = require('../helpers/traceHelper') -var util = require('../helpers/global') +var util = require('../helpers/util') +var globalUtil = require('../helpers/global') function TraceManager () { this.isLoading = false @@ -20,7 +21,7 @@ function TraceManager () { TraceManager.prototype.resolveTrace = function (tx, callback) { this.tx = tx this.init() - if (!util.web3) callback('web3 not loaded', false) + if (!globalUtil.web3) callback('web3 not loaded', false) this.isLoading = true var self = this this.traceRetriever.getTrace(tx.hash, function (error, result) { @@ -79,7 +80,8 @@ TraceManager.prototype.getStorageAt = function (stepIndex, tx, callback, address return callback(check, null) } if (!address) { - var stoChange = traceHelper.findLowerBound(stepIndex, this.traceCache.storageChanges) + var stoChangeIndex = util.findLowerBound(stepIndex, this.traceCache.storageChanges) + var stoChange = this.traceCache.storageChanges[stoChangeIndex] if (stoChange === undefined) return callback('no storage found', null) address = this.traceCache.sstore[stoChange].address } @@ -122,7 +124,8 @@ TraceManager.prototype.getCallDataAt = function (stepIndex, callback) { if (check) { return callback(check, null) } - var callDataChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callDataChanges) + var callDataChangeIndex = util.findLowerBound(stepIndex, this.traceCache.callDataChanges) + var callDataChange = this.traceCache.callDataChanges[callDataChangeIndex] if (callDataChange === undefined) return callback('no calldata found', null) callback(null, [this.traceCache.callsData[callDataChange]]) } @@ -132,7 +135,8 @@ TraceManager.prototype.getCallStackAt = function (stepIndex, callback) { if (check) { return callback(check, null) } - var callStackChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callChanges) + var callStackChangeIndex = util.findLowerBound(stepIndex, this.traceCache.callChanges) + var callStackChange = this.traceCache.callChanges[callStackChangeIndex] if (callStackChange === undefined) return callback('no callstack found', null) callback(null, this.traceCache.callStack[callStackChange].callStack) } @@ -157,7 +161,8 @@ TraceManager.prototype.getLastCallChangeSince = function (stepIndex, callback) { if (check) { return callback(check, null) } - var callChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callChanges) + var callChangeIndex = util.findLowerBound(stepIndex, this.traceCache.callChanges) + var callChange = this.traceCache.callChanges[callChangeIndex] if (callChange === undefined) { callback(null, 0) } else { @@ -203,7 +208,8 @@ TraceManager.prototype.getMemoryAt = function (stepIndex, callback) { if (check) { return callback(check, null) } - var lastChanges = traceHelper.findLowerBound(stepIndex, this.traceCache.memoryChanges) + var lastChangesIndex = util.findLowerBound(stepIndex, this.traceCache.memoryChanges) + var lastChanges = this.traceCache.memoryChanges[lastChangesIndex] if (lastChanges === undefined) return callback('no memory found', null) callback(null, this.trace[lastChanges].memory) } diff --git a/src/ui/Ethdebugger.js b/src/ui/Ethdebugger.js index 17aa5357a0..0599ba039a 100644 --- a/src/ui/Ethdebugger.js +++ b/src/ui/Ethdebugger.js @@ -28,7 +28,7 @@ function Ethdebugger () { this.codeManager = new CodeManager(this.traceManager) var self = this - this.traceManager.register('indexChanged', this, function (index) { + this.register('indexChanged', this, function (index) { self.codeManager.resolveStep(index, self.tx) }) diff --git a/src/util/sourceMappingDecoder.js b/src/util/sourceMappingDecoder.js new file mode 100644 index 0000000000..697a1e47fa --- /dev/null +++ b/src/util/sourceMappingDecoder.js @@ -0,0 +1,134 @@ +'use strict' +var util = require('../helpers/util') + +/** + * Decompress the source mapping given by solc-bin.js + */ +function SourceMappingDecoder () { + // s:l:f:j +} + +/** + * Decode the source mapping for the given @arg index + * + * @param {Integer} index - source mapping index to decode + * @param {String} mapping - compressed source mapping given by solc-bin + * @return {Object} returns the decompressed source mapping for the given index {start, length, file, jump} + */ +SourceMappingDecoder.prototype.atIndex = function (index, mapping) { + var ret = {} + var map = mapping.split(';') + for (var k = index; k >= 0; k--) { + var current = map[k] + if (!current.length) { + continue + } + current = current.split(':') + if (ret.start === undefined && current[0] && current[0].length) { + ret.start = parseInt(current[0]) + } + if (ret.length === undefined && current[1] && current[1].length) { + ret.length = parseInt(current[1]) + } + if (ret.file === undefined && current[2] && current[2].length) { + ret.file = parseInt(current[2]) + } + if (ret.jump === undefined && current[3] && current[3].length) { + ret.jump = current[3] + } + if (ret.start !== undefined && ret.length !== undefined && ret.file !== undefined && ret.jump !== undefined) { + break + } + } + return ret +} + +/** + * Decode the source mapping for the given compressed mapping + * + * @param {String} mapping - compressed source mapping given by solc-bin + * @return {Array} returns the decompressed source mapping. Array of {start, length, file, jump} + */ +SourceMappingDecoder.prototype.decompressAll = function (mapping) { + var map = mapping.split(';') + var ret = [] + for (var k in map) { + var compressed = map[k].split(':') + var sourceMap = { + start: compressed[0] ? parseInt(compressed[0]) : ret[ret.length - 1].start, + length: compressed[1] ? parseInt(compressed[1]) : ret[ret.length - 1].length, + file: compressed[2] ? parseInt(compressed[2]) : ret[ret.length - 1].file, + jump: compressed[3] ? compressed[3] : ret[ret.length - 1].jump + } + ret.push(sourceMap) + } + return ret +} + +/** + * Retrieve line/column position of each source char + * + * @param {String} source - contract source code + * @return {Arrray} returns an array containing offset of line breaks + */ +SourceMappingDecoder.prototype.getLinebreakPositions = function (source) { + var ret = [] + for (var pos = source.indexOf('\n'); pos >= 0; pos = source.indexOf('\n', pos + 1)) { + ret.push(pos) + } + return ret + /* + var lines = source.split('\n') + var currentPos = 0 + var ret = [] + for (var k in lines) { + ret.push(currentPos + lines[k].length) + currentPos += lines[k].length + 1 + } + return ret*/ +} + +/** + * Retrieve the line/colum position for the given source mapping + * + * @param {Object} sourceLocation - object containing attributes {source} and {length} + * @param {Array} lineBreakPositions - array returned by the function 'getLinebreakPositions' + * @@return {Object} returns an object {start: {line, column}, end: {line, column}} + */ +SourceMappingDecoder.prototype.convertOffsetToLineColumn = function (sourceLocation, lineBreakPositions) { + if (sourceLocation.start >= 0 && sourceLocation.length >= 0) { + return { + start: convertFromCharPosition(sourceLocation.start, lineBreakPositions), + end: convertFromCharPosition(sourceLocation.start + sourceLocation.length, lineBreakPositions) + } + } else { + return { + start: -1, + end: -1 + } + } +} + +function convertFromCharPosition (pos, lineColumnLayout) { + var lowerBound = util.findLowerBound(pos, lineColumnLayout) + var line + if (lowerBound === 0) { + line = 0 + } else if (lowerBound < pos) { + line = lowerBound + 1 + } else { + line = lowerBound + } + var column + if (lowerBound === 0) { + column = pos + } else { + column = pos - 1 - lineColumnLayout[line - 1] + } + return { + line: line, + column: column + } +} + +module.exports = SourceMappingDecoder diff --git a/test/resources/sourceMapping.js b/test/resources/sourceMapping.js new file mode 100644 index 0000000000..f2f64a830e --- /dev/null +++ b/test/resources/sourceMapping.js @@ -0,0 +1,22 @@ +var sourceRuntimeMapping = {} +sourceRuntimeMapping.mapping = '0:205:4:-;;;;;;;;;;;;;;;;;;;;;;55:74;;;;;;;;;;;;;;;;;;;;;;;;;;142:61;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;55:74;103:2;99:1;;:6;;;;;120:2;116:1;;:6;;;;;55:74;;;:::o;142:61::-;166:6;174;142:61;;;:::o' +sourceRuntimeMapping.source = `contract test { + int x; + + int y; + + function set(int _x, int _y) + { + x = _x; + y = _y; + } + + function get() returns (uint x, uint y) + { + + } +}` + +if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { + module.exports = sourceRuntimeMapping +} diff --git a/test/tests.js b/test/tests.js index 96ea8c66d4..7959d8769a 100644 --- a/test/tests.js +++ b/test/tests.js @@ -2,3 +2,4 @@ require('./index.js') require('./traceManager.js') require('./codeManager.js') +require('./util.js') diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000000..962125d929 --- /dev/null +++ b/test/util.js @@ -0,0 +1,79 @@ +'use strict' +var sourceMapping = require('./resources/sourceMapping') +var index = require('../src/index') +var tape = require('tape') + +tape('Util', function (t) { + var testSourceMapping = {} + t.test('sourceMappingDecoder', function (st) { + st.plan(28) + var sourceMappingDecoder = new index.util.SourceMappingDecoder() + console.log('test decompressAll') + var result = sourceMappingDecoder.decompressAll(sourceMapping.mapping) + st.ok(result[0].start === 0) + st.ok(result[0].length === 205) + st.ok(result[0].file === 4) + st.ok(result[0].jump === '-') + + st.ok(result[21].start === 0) + st.ok(result[21].length === 205) + st.ok(result[21].file === 4) + st.ok(result[21].jump === '-') + testSourceMapping[21] = result[21] + + st.ok(result[22].start === 55) + st.ok(result[22].length === 74) + st.ok(result[22].file === 4) + st.ok(result[22].jump === '-') + + var last = result.length - 1 + st.ok(result[last].start === 142) + st.ok(result[last].length === 61) + st.ok(result[last].file === 4) + st.ok(result[last].jump === 'o') + testSourceMapping['last'] = result[last] + + console.log('test decompress') + result = sourceMappingDecoder.atIndex(22, sourceMapping.mapping) + console.log(result) + st.ok(result.start === 55) + st.ok(result.length === 74) + st.ok(result.file === 4) + st.ok(result.jump === '-') + testSourceMapping[22] = result + + result = sourceMappingDecoder.atIndex(82, sourceMapping.mapping) + console.log(result) + st.ok(result.start === 103) + st.ok(result.length === 2) + st.ok(result.file === 4) + st.ok(result.jump === '-') + testSourceMapping[82] = result + + result = sourceMappingDecoder.atIndex(85, sourceMapping.mapping) + console.log(result) + st.ok(result.start === 99) + st.ok(result.length === 6) + st.ok(result.file === 4) + st.ok(result.jump === '-') + testSourceMapping[85] = result + }) + + t.test('sourceMappingLineColumnConverter', function (st) { + st.plan(10) + var sourceMappingDecoder = new index.util.SourceMappingDecoder() + var linesbreak = sourceMappingDecoder.getLinebreakPositions(sourceMapping.source) + st.ok(linesbreak[0] === 16) + st.ok(linesbreak[5] === 84) + var result = sourceMappingDecoder.convertOffsetToLineColumn(testSourceMapping[21], linesbreak) + st.ok(result.start.line === 0) + st.ok(result.start.column === 0) + st.ok(result.end.line === 15) + st.ok(result.end.column === 1) + result = sourceMappingDecoder.convertOffsetToLineColumn(testSourceMapping[82], linesbreak) + st.ok(result.start.line === 7) + st.ok(result.start.column === 12) + st.ok(result.end.line === 7) + st.ok(result.end.column === 14) + }) +})