I'm an enthusiastic software developer who tries to keep up with the all-time changing trends in software development. My interests go from trying new coding stuff, new design and architectural techniques, to studying how to encourage the adoption of new methodologies and tools in my organization. Martín has posted 8 posts at DZone. You can read more from them at their website. View Full User Profile

How to read code from Mars: A story of unmasking some Javascript code

12.05.2013
| 10755 views |
  • submit to reddit

I'm soooo ignorant. There are so many things I don't know. Like when I found this piece of Javascript code:

for (var i=0; i<6; i++) {
    var row = document.querySelector("table").insertRow(-1);
    for (var j=0; j<6; j++) {
        var letter = String.fromCharCode("A".charCodeAt(0)+j-1);
        row.insertCell(-1).innerHTML = i&&j ? "<input id='"+ letter+i +"'/>" : i||letter;
    }
}

var DATA={}, INPUTS=[].slice.call(document.querySelectorAll("input"));
INPUTS.forEach(function(elm) {
    elm.onfocus = function(e) {
        e.target.value = localStorage[e.target.id] || "";
    };
    elm.onblur = function(e) {
        localStorage[e.target.id] = e.target.value;
        computeAll();
    };
    var getter = function() {
        var value = localStorage[elm.id] || "";
        if (value.charAt(0) == "=") {
            with (DATA) return eval(value.substring(1));
        } else { return isNaN(parseFloat(value)) ? value : parseFloat(value); }
    };
    Object.defineProperty(DATA, elm.id, {get:getter});
    Object.defineProperty(DATA, elm.id.toLowerCase(), {get:getter});
});
(window.computeAll = function() {
    INPUTS.forEach(function(elm) { try { elm.value = DATA[elm.id]; } catch(e) {} });
})();

First look at what the code does and then comeback.

OK, it's a spreadsheet that supports an Excel-like formula. But there are some things I couldn't understand quickly when inspecting this code. I spent 3 minutes looking at it and couldn't tell a thing about what was happening. I knew it was something, taking into account it was really short and accomplished so many things.

I had a disadvantage since I'm not a JavaScript expert and hadn't seen some of the syntax and functions used in this piece of code. But I got ready to unveil what was happening under the cover. For this, I pair-reviewed it with a friend of mine, and both of us got ready to attack it without looking for further information, just based on intuition, logic and experiments. I leave you with our review:

At the end of this article there is a roadmap image showing our thoughts didactically.  It might help you understand better and faster how we went through this, in case you get bored.

OK, where is the magic coming from? I can't see a formula parser- I said- Ah wait, there's the eval function, and above it has an if (value.charAt(0) == "=") clause that's detecting if value is a formula. That's the only occurrence of eval, so there must be happening most of it.

But, what's that with that with clause?. What does it mean?- I asked.

Let's do an experiment for something I'm suspecting -said my friend. And after some tweaks we got this:

var z;
with({
	a:1, 
	b:2, 
	c:3
}) z = eval("(a + b) * c");
alert(z);

9!!!. That's it. At least we know now how the eval function works with a with clause in front of it. This is new for us.

Great. Now, what is DATA?. What value could it possibly have that works with the with clause?- I asked- It must hold the values of every cell in the sheet, right?. But what if a formula references a cell that also contains a formula. I don't see any recursion here. Now we are stock... Let's investigate more on DATA. Let's point out an occurrence of DATA and let our editor highlight every other occurrences.

It's declared as an empty object. Then it's...mmm. I don't see it being assigned a value anywhere. Rare. And what does that line of code means?:

Object.defineProperty(DATA, elm.id, {get:getter});

Well, reading it looks like it defines a property in the DATA object, and also establishes a callback to the getter function for when the property is accessed -we both agreed.

Yeah, but what does that give us?- I questioned. Nothing up to what I know -I responded myself. Let's move on.

When is the moment when a formula calculation triggers?. Ah, there it is: the onblur event of the cell (input element: elm). It stores the cell value inside localStorage (which was something new to us too, but something that wouldn't entertain us) and then calls computeAll function... and computeAll function is supposed to compute the values of every cell for what I see, isn't it?. But it doesn't call anything further!!!. What's happening here?.

Aha, each cell is assigned a supposedly precomputed value in DATA[elm.id]. Could it be that the getter function is called when we access an index in DATA, right at this time, just as we guessed before?. That must be it -said my friend.

Yeah, and the getter function returns an evaluation of an expression previously stored inside localStorage, taking into account the index of the cell -I replied. If it's a formulal, it uses DATA as the datasource to evaluate the expression. But again, DATA isn't assigned any value... or perhaps it's being used just as a justification or placeholder for calling the getter function, which is the one that makes the actual calculations using localStorage?.

DATA is proxying localStorage? DATA is proxying localStorage!!!- asserted my friend, almost screaming.

Of course!!!- I did scream. And the recursion happens as soon as Javascript tries to access the indexes in DATA when it executes the with(DATA) clause, which iterates through the needed DATA's properties. It starts calling the getter function inside the getter function, a.k.a. recursion.

That must be it -said my friend.

OK, OK, OK. Wait. Let's do the following:

var target = {a:1, b:2, c:3, d:"(a + b) * c"};
var proxy = {};

["a", "b", "c", "d"].forEach(function(idx) {		
	var getter = function() {
		with (proxy) return eval(target[idx]);
	};
	Object.defineProperty(proxy, idx, {get:getter});
});

alert(proxy["d"]);

9!!!. Pum, that's it. We got it. DATA IS PROXYING LOCALSTORAGE. In this last experiment we can see it clearly... Wow, nicely done.

Now we had the whole picture and were ready to recreate the crime scene events. I mean, the program execution:

When a cell loses focus, its value expression (formula or number) is stored in localStorage, and every cell value is recomputed in the computeAll function. The computeAll function simply assigns each cell a value that's supposedly stored in DATA[cellIndex]; only that that value isn't there yet. The expression Object.defineProperty(DATA, elm.id, {get:getter}) makes sure that everytime I try to access an index in DATA, the getter function is triggered. So when we say DATA[cellIndex], it triggers. The getter function returns a value according to the expression stored in localStorage for the current cell's index. If the expression is a formula, it runs the expression with (DATA) return eval(value.substring(1)). This is where the recursion happens, because with(DATA) return eval(value.substring(1)) is going to iterate through DATA's properties, which as stated, will trigger the getter function in each access: a recursion is happening; the termination clause of the recursion is the sentence else { return isNaN(parseFloat(value)) ? value : parseFloat(value); }. There you have it. A combination of proxying localStorage and intercepting DATA's properties accesses is the cause of death... I mean, the key for the formula calculations.

And we stayed with that. We didn't have to look any further. We hadn't debuged or inspected the code execution with sample values, or hadn't made an exhaustive mental analysis of the possible execution paths. We only looked at the code and made some guesses. Whether our conclusions are right or wrong we are not sure; but that's something we can validate now by searching in Internet. Whatever we find, it was a good excercise.

Roadmap


Note: Up to the date of writting this article, we hadn't researched on any of this, so we wouldn't be biased to correcting our thoughts. A real life excercise of analysing code has some value anyway.

Published at DZone with permission of its author, Martín Proenza.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)