Sunday, November 10, 2013

NUL Characters in Dart and JavaScript


Thanks some ugly keyboard hackery, I have nearly all of the tests in the ICE Code Editor passing. In addition to the aforementioned keyboard test hackery, I also recently had to update some js-interop changes (mostly removing no-longer needed code, which is always nice). I think I still have a few js-interop changes that I missed.

The tests that make me think that there is a js-interop problem involve decoding GZIP text (used for sharing projects):
Expectation: gzipping it can decode as text. 
  Expected: 'Howdy, Bob!' 
  Actual: '' 
  Which: is different. Both strings start the same, but the given value is missing the following trailing characters: Howdy, Bob ...
package:unittest/src/expect.dart 75:29                                                                                               expect
../gzip_test.dart 10:13
Interestingly, the test that encodes the content of a project as a GZIP string is passing:
    test("it can encode text", (){
      expect(Gzip.encode("Howdy, Bob!"), equals("88gvT6nUUXDKT1IEAA=="));
    });
The Gzip.encode() method comes from JavaScript just like the Gzip.decode() in the test that is currently failing:
    test("it can decode as text", (){
      expect(Gzip.decode("88gvT6nUUXDKT1IEAA=="), equals("Howdy, Bob!"));
    });
So the question is what I have I done with the decode functionality that I have not done with encode? Hopefully this is a simple solution because Dart still lacks a dart:html usable solution for GZIPing data.

The solution turns out to be fairly simple, but the cause is tough to track down. I finally isolate the cause by printing out the number of characters being jammed into the gzip string that is passed via js-interop to Rawdeflate.inflate():
  static String decode(String string) {
    var bytes = CryptoUtils.base64StringToBytes(string);
    print(bytes);
    var gzip = new String.fromCharCodes(bytes);
    print(gzip);
    print(gzip.length);
    return js.context.RawDeflate.inflate(gzip);
  }
This gets logged as:
[243, 200, 47, 79, 169, 212, 81, 112, 202, 79, 82, 4, 0]
óÈ/O©ÔQpÊOR 
13
So the number of characters in my string is 13. But if I log the characters that JavaScript sees:
var zip_inflate = function(str) {
  console.log(str)
  console.log(str.length)
  // ...
}
It reports only 12:
óÈ/O©ÔQpÊOR  
12 
The GZIP'd string may look the same, but it is missing the very last character when passed to JavaScript. Looking back up to the list of bytes of bytes that Dart is using, the last one is zero—the NUL character. My guess would be that js-interop is treating that NUL character as a terminating character. And, if I add more junk characters to then end of gzip in Dart:
  static String decode(String string) {
    var bytes = CryptoUtils.base64StringToBytes(string);
    print(bytes);
    var gzip = new String.fromCharCodes(bytes) + 'asdf';
    print(gzip);
    print(gzip.length);
    return js.context.RawDeflate.inflate(gzip);
  }
Then JavaScript still only sees 12 characters:
[243, 200, 47, 79, 169, 212, 81, 112, 202, 79, 82, 4, 0]
óÈ/O©ÔQpÊOR 
17
óÈ/O©ÔQpÊOR  
12 
So that seems like a bug in js-interop or dart:js. I will file the bug accordingly.

But, for now, I get the test passing by doing the Base64 decoding in JavaScript is well. There are no NUL characters in Base64, so this will work just fine.


Day #931

No comments:

Post a Comment