1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Support for parsing binary sequences encoded as readable strings 6// or ".data" files. The input format is described here: 7// mojo/public/cpp/bindings/tests/validation_test_input_parser.h 8 9define([ 10 "mojo/public/js/bindings/buffer" 11 ], function(buffer) { 12 13 // Files and Lines represent the raw text from an input string 14 // or ".data" file. 15 16 function InputError(message, line) { 17 this.message = message; 18 this.line = line; 19 } 20 21 InputError.prototype.toString = function() { 22 var s = 'Error: ' + this.message; 23 if (this.line) 24 s += ', at line ' + 25 (this.line.number + 1) + ': "' + this.line.contents + '"'; 26 return s; 27 } 28 29 function File(contents) { 30 this.contents = contents; 31 this.index = 0; 32 this.lineNumber = 0; 33 } 34 35 File.prototype.endReached = function() { 36 return this.index >= this.contents.length; 37 } 38 39 File.prototype.nextLine = function() { 40 if (this.endReached()) 41 return null; 42 var start = this.index; 43 var end = this.contents.indexOf('\n', start); 44 if (end == -1) 45 end = this.contents.length; 46 this.index = end + 1; 47 return new Line(this.contents.substring(start, end), this.lineNumber++); 48 } 49 50 function Line(contents, number) { 51 var i = contents.indexOf('//'); 52 var s = (i == -1) ? contents.trim() : contents.substring(0, i).trim(); 53 this.contents = contents; 54 this.items = (s.length > 0) ? s.split(/\s+/) : []; 55 this.index = 0; 56 this.number = number; 57 } 58 59 Line.prototype.endReached = function() { 60 return this.index >= this.items.length; 61 } 62 63 var ITEM_TYPE_SIZES = { 64 u1: 1, u2: 2, u4: 4, u8: 8, s1: 1, s2: 2, s4: 4, s8: 8, b: 1, f: 4, d: 8, 65 dist4: 4, dist8: 8, anchr: 0, handles: 0 66 }; 67 68 function isValidItemType(type) { 69 return ITEM_TYPE_SIZES[type] !== undefined; 70 } 71 72 Line.prototype.nextItem = function() { 73 if (this.endReached()) 74 return null; 75 76 var itemString = this.items[this.index++]; 77 var type = 'u1'; 78 var value = itemString; 79 80 if (itemString.charAt(0) == '[') { 81 var i = itemString.indexOf(']'); 82 if (i != -1 && i + 1 < itemString.length) { 83 type = itemString.substring(1, i); 84 value = itemString.substring(i + 1); 85 } else { 86 throw new InputError('invalid item', this); 87 } 88 } 89 if (!isValidItemType(type)) 90 throw new InputError('invalid item type', this); 91 92 return new Item(this, type, value); 93 } 94 95 // The text for each whitespace delimited binary data "item" is represented 96 // by an Item. 97 98 function Item(line, type, value) { 99 this.line = line; 100 this.type = type; 101 this.value = value; 102 this.size = ITEM_TYPE_SIZES[type]; 103 } 104 105 Item.prototype.isFloat = function() { 106 return this.type == 'f' || this.type == 'd'; 107 } 108 109 Item.prototype.isInteger = function() { 110 return ['u1', 'u2', 'u4', 'u8', 111 's1', 's2', 's4', 's8'].indexOf(this.type) != -1; 112 } 113 114 Item.prototype.isNumber = function() { 115 return this.isFloat() || this.isInteger(); 116 } 117 118 Item.prototype.isByte = function() { 119 return this.type == 'b'; 120 } 121 122 Item.prototype.isDistance = function() { 123 return this.type == 'dist4' || this.type == 'dist8'; 124 } 125 126 Item.prototype.isAnchor = function() { 127 return this.type == 'anchr'; 128 } 129 130 Item.prototype.isHandles = function() { 131 return this.type == 'handles'; 132 } 133 134 // A TestMessage represents the complete binary message loaded from an input 135 // string or ".data" file. The parseTestMessage() function below constructs 136 // a TestMessage from a File. 137 138 function TestMessage(byteLength) { 139 this.index = 0; 140 this.buffer = new buffer.Buffer(byteLength); 141 this.distances = {}; 142 this.handleCount = 0; 143 } 144 145 function checkItemNumberValue(item, n, min, max) { 146 if (n < min || n > max) 147 throw new InputError('invalid item value', item.line); 148 } 149 150 TestMessage.prototype.addNumber = function(item) { 151 var n = item.isInteger() ? parseInt(item.value) : parseFloat(item.value); 152 if (Number.isNaN(n)) 153 throw new InputError("can't parse item value", item.line); 154 155 switch(item.type) { 156 case 'u1': 157 checkItemNumberValue(item, n, 0, 0xFF); 158 this.buffer.setUint8(this.index, n); 159 break; 160 case 'u2': 161 checkItemNumberValue(item, n, 0, 0xFFFF); 162 this.buffer.setUint16(this.index, n); 163 break; 164 case 'u4': 165 checkItemNumberValue(item, n, 0, 0xFFFFFFFF); 166 this.buffer.setUint32(this.index, n); 167 break; 168 case 'u8': 169 checkItemNumberValue(item, n, 0, Number.MAX_SAFE_INTEGER); 170 this.buffer.setUint64(this.index, n); 171 break; 172 case 's1': 173 checkItemNumberValue(item, n, -128, 127); 174 this.buffer.setInt8(this.index, n); 175 break; 176 case 's2': 177 checkItemNumberValue(item, n, -32768, 32767); 178 this.buffer.setInt16(this.index, n); 179 break; 180 case 's4': 181 checkItemNumberValue(item, n, -2147483648, 2147483647); 182 this.buffer.setInt32(this.index, n); 183 break; 184 case 's8': 185 checkItemNumberValue(item, n, 186 Number.MIN_SAFE_INTEGER, 187 Number.MAX_SAFE_INTEGER); 188 this.buffer.setInt64(this.index, n); 189 break; 190 case 'f': 191 this.buffer.setFloat32(this.index, n); 192 break; 193 case 'd': 194 this.buffer.setFloat64(this.index, n); 195 break; 196 197 default: 198 throw new InputError('unrecognized item type', item.line); 199 } 200 } 201 202 TestMessage.prototype.addByte = function(item) { 203 if (!/^[01]{8}$/.test(item.value)) 204 throw new InputError('invalid byte item value', item.line); 205 function b(i) { 206 return (item.value.charAt(7 - i) == '1') ? 1 << i : 0; 207 } 208 var n = b(0) | b(1) | b(2) | b(3) | b(4) | b(5) | b(6) | b(7); 209 this.buffer.setUint8(this.index, n); 210 } 211 212 TestMessage.prototype.addDistance = function(item) { 213 if (this.distances[item.value]) 214 throw new InputError('duplicate distance item', item.line); 215 this.distances[item.value] = {index: this.index, item: item}; 216 } 217 218 TestMessage.prototype.addAnchor = function(item) { 219 var dist = this.distances[item.value]; 220 if (!dist) 221 throw new InputError('unmatched anchor item', item.line); 222 delete this.distances[item.value]; 223 224 var n = this.index - dist.index; 225 // TODO(hansmuller): validate n 226 227 if (dist.item.type == 'dist4') 228 this.buffer.setUint32(dist.index, n); 229 else if (dist.item.type == 'dist8') 230 this.buffer.setUint64(dist.index, n); 231 else 232 throw new InputError('unrecognzed distance item type', dist.item.line); 233 } 234 235 TestMessage.prototype.addHandles = function(item) { 236 this.handleCount = parseInt(item.value); 237 if (Number.isNaN(this.handleCount)) 238 throw new InputError("can't parse handleCount", item.line); 239 } 240 241 TestMessage.prototype.addItem = function(item) { 242 if (item.isNumber()) 243 this.addNumber(item); 244 else if (item.isByte()) 245 this.addByte(item); 246 else if (item.isDistance()) 247 this.addDistance(item); 248 else if (item.isAnchor()) 249 this.addAnchor(item); 250 else if (item.isHandles()) 251 this.addHandles(item); 252 else 253 throw new InputError('unrecognized item type', item.line); 254 255 this.index += item.size; 256 } 257 258 TestMessage.prototype.unanchoredDistances = function() { 259 var names = null; 260 for (var name in this.distances) { 261 if (this.distances.hasOwnProperty(name)) 262 names = (names === null) ? name : names + ' ' + name; 263 } 264 return names; 265 } 266 267 function parseTestMessage(text) { 268 var file = new File(text); 269 var items = []; 270 var messageLength = 0; 271 while(!file.endReached()) { 272 var line = file.nextLine(); 273 while (!line.endReached()) { 274 var item = line.nextItem(); 275 if (item.isHandles() && items.length > 0) 276 throw new InputError('handles item is not first'); 277 messageLength += item.size; 278 items.push(item); 279 } 280 } 281 282 var msg = new TestMessage(messageLength); 283 for (var i = 0; i < items.length; i++) 284 msg.addItem(items[i]); 285 286 if (messageLength != msg.index) 287 throw new InputError('failed to compute message length'); 288 var names = msg.unanchoredDistances(); 289 if (names) 290 throw new InputError('no anchors for ' + names, 0); 291 292 return msg; 293 } 294 295 var exports = {}; 296 exports.parseTestMessage = parseTestMessage; 297 exports.InputError = InputError; 298 return exports; 299}); 300