1// Copyright (c) 2012 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'use strict'; 6 7/** 8 * @constructor 9 * @param {ArrayBuffer} arrayBuffer // TODO(JSDOC). 10 * @param {number=} opt_offset // TODO(JSDOC). 11 * @param {number=} opt_length // TODO(JSDOC). 12 */ 13function ByteReader(arrayBuffer, opt_offset, opt_length) { 14 opt_offset = opt_offset || 0; 15 opt_length = opt_length || (arrayBuffer.byteLength - opt_offset); 16 this.view_ = new DataView(arrayBuffer, opt_offset, opt_length); 17 this.pos_ = 0; 18 this.seekStack_ = []; 19 this.setByteOrder(ByteReader.BIG_ENDIAN); 20} 21 22// Static constants and methods. 23 24/** 25 * Intel, 0x1234 is [0x34, 0x12] 26 * @const 27 * @type {number} 28 */ 29ByteReader.LITTLE_ENDIAN = 0; 30/** 31 * Motorola, 0x1234 is [0x12, 0x34] 32 * @const 33 * @type {number} 34 */ 35ByteReader.BIG_ENDIAN = 1; 36 37/** 38 * Seek relative to the beginning of the buffer. 39 * @const 40 * @type {number} 41 */ 42ByteReader.SEEK_BEG = 0; 43/** 44 * Seek relative to the current position. 45 * @const 46 * @type {number} 47 */ 48ByteReader.SEEK_CUR = 1; 49/** 50 * Seek relative to the end of the buffer. 51 * @const 52 * @type {number} 53 */ 54ByteReader.SEEK_END = 2; 55 56/** 57 * Throw an error if (0 > pos >= end) or if (pos + size > end). 58 * 59 * Static utility function. 60 * 61 * @param {number} pos // TODO(JSDOC). 62 * @param {number} size // TODO(JSDOC). 63 * @param {number} end // TODO(JSDOC). 64 */ 65ByteReader.validateRead = function(pos, size, end) { 66 if (pos < 0 || pos >= end) 67 throw new Error('Invalid read position'); 68 69 if (pos + size > end) 70 throw new Error('Read past end of buffer'); 71}; 72 73/** 74 * Read as a sequence of characters, returning them as a single string. 75 * 76 * This is a static utility function. There is a member function with the 77 * same name which side-effects the current read position. 78 * 79 * @param {DataView} dataView // TODO(JSDOC). 80 * @param {number} pos // TODO(JSDOC). 81 * @param {number} size // TODO(JSDOC). 82 * @param {number=} opt_end // TODO(JSDOC). 83 * @return {string} // TODO(JSDOC). 84 */ 85ByteReader.readString = function(dataView, pos, size, opt_end) { 86 ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); 87 88 var codes = []; 89 90 for (var i = 0; i < size; ++i) 91 codes.push(dataView.getUint8(pos + i)); 92 93 return String.fromCharCode.apply(null, codes); 94}; 95 96/** 97 * Read as a sequence of characters, returning them as a single string. 98 * 99 * This is a static utility function. There is a member function with the 100 * same name which side-effects the current read position. 101 * 102 * @param {DataView} dataView // TODO(JSDOC). 103 * @param {number} pos // TODO(JSDOC). 104 * @param {number} size // TODO(JSDOC). 105 * @param {number=} opt_end // TODO(JSDOC). 106 * @return {string} // TODO(JSDOC). 107 */ 108ByteReader.readNullTerminatedString = function(dataView, pos, size, opt_end) { 109 ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); 110 111 var codes = []; 112 113 for (var i = 0; i < size; ++i) { 114 var code = dataView.getUint8(pos + i); 115 if (code == 0) break; 116 codes.push(code); 117 } 118 119 return String.fromCharCode.apply(null, codes); 120}; 121 122/** 123 * Read as a sequence of UTF16 characters, returning them as a single string. 124 * 125 * This is a static utility function. There is a member function with the 126 * same name which side-effects the current read position. 127 * 128 * @param {DataView} dataView // TODO(JSDOC). 129 * @param {number} pos // TODO(JSDOC). 130 * @param {boolean} bom // TODO(JSDOC). 131 * @param {number} size // TODO(JSDOC). 132 * @param {number=} opt_end // TODO(JSDOC). 133 * @return {string} // TODO(JSDOC). 134 */ 135ByteReader.readNullTerminatedStringUTF16 = function( 136 dataView, pos, bom, size, opt_end) { 137 ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); 138 139 var littleEndian = false; 140 var start = 0; 141 142 if (bom) { 143 littleEndian = (dataView.getUint8(pos) == 0xFF); 144 start = 2; 145 } 146 147 var codes = []; 148 149 for (var i = start; i < size; i += 2) { 150 var code = dataView.getUint16(pos + i, littleEndian); 151 if (code == 0) break; 152 codes.push(code); 153 } 154 155 return String.fromCharCode.apply(null, codes); 156}; 157 158/** 159 * @const 160 * @type {Array.<string>} 161 * @private 162 */ 163ByteReader.base64Alphabet_ = 164 ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'). 165 split(''); 166 167/** 168 * Read as a sequence of bytes, returning them as a single base64 encoded 169 * string. 170 * 171 * This is a static utility function. There is a member function with the 172 * same name which side-effects the current read position. 173 * 174 * @param {DataView} dataView // TODO(JSDOC). 175 * @param {number} pos // TODO(JSDOC). 176 * @param {number} size // TODO(JSDOC). 177 * @param {number=} opt_end // TODO(JSDOC). 178 * @return {string} // TODO(JSDOC). 179 */ 180ByteReader.readBase64 = function(dataView, pos, size, opt_end) { 181 ByteReader.validateRead(pos, size, opt_end || dataView.byteLength); 182 183 var rv = []; 184 var chars = []; 185 var padding = 0; 186 187 for (var i = 0; i < size; /* incremented inside */) { 188 var bits = dataView.getUint8(pos + (i++)) << 16; 189 190 if (i < size) { 191 bits |= dataView.getUint8(pos + (i++)) << 8; 192 193 if (i < size) { 194 bits |= dataView.getUint8(pos + (i++)); 195 } else { 196 padding = 1; 197 } 198 } else { 199 padding = 2; 200 } 201 202 chars[3] = ByteReader.base64Alphabet_[bits & 63]; 203 chars[2] = ByteReader.base64Alphabet_[(bits >> 6) & 63]; 204 chars[1] = ByteReader.base64Alphabet_[(bits >> 12) & 63]; 205 chars[0] = ByteReader.base64Alphabet_[(bits >> 18) & 63]; 206 207 rv.push.apply(rv, chars); 208 } 209 210 if (padding > 0) 211 rv[rv.length - 1] = '='; 212 if (padding > 1) 213 rv[rv.length - 2] = '='; 214 215 return rv.join(''); 216}; 217 218/** 219 * Read as an image encoded in a data url. 220 * 221 * This is a static utility function. There is a member function with the 222 * same name which side-effects the current read position. 223 * 224 * @param {DataView} dataView // TODO(JSDOC). 225 * @param {number} pos // TODO(JSDOC). 226 * @param {number} size // TODO(JSDOC). 227 * @param {number=} opt_end // TODO(JSDOC). 228 * @return {string} // TODO(JSDOC). 229 */ 230ByteReader.readImage = function(dataView, pos, size, opt_end) { 231 opt_end = opt_end || dataView.byteLength; 232 ByteReader.validateRead(pos, size, opt_end); 233 234 // Two bytes is enough to identify the mime type. 235 var prefixToMime = { 236 '\x89P' : 'png', 237 '\xFF\xD8' : 'jpeg', 238 'BM' : 'bmp', 239 'GI' : 'gif' 240 }; 241 242 var prefix = ByteReader.readString(dataView, pos, 2, opt_end); 243 var mime = prefixToMime[prefix] || 244 dataView.getUint16(pos, false).toString(16); // For debugging. 245 246 var b64 = ByteReader.readBase64(dataView, pos, size, opt_end); 247 return 'data:image/' + mime + ';base64,' + b64; 248}; 249 250// Instance methods. 251 252/** 253 * Return true if the requested number of bytes can be read from the buffer. 254 * 255 * @param {number} size // TODO(JSDOC). 256 * @return {boolean} // TODO(JSDOC). 257 */ 258ByteReader.prototype.canRead = function(size) { 259 return this.pos_ + size <= this.view_.byteLength; 260}; 261 262/** 263 * Return true if the current position is past the end of the buffer. 264 * @return {boolean} // TODO(JSDOC). 265 */ 266ByteReader.prototype.eof = function() { 267 return this.pos_ >= this.view_.byteLength; 268}; 269 270/** 271 * Return true if the current position is before the beginning of the buffer. 272 * @return {boolean} // TODO(JSDOC). 273 */ 274ByteReader.prototype.bof = function() { 275 return this.pos_ < 0; 276}; 277 278/** 279 * Return true if the current position is outside the buffer. 280 * @return {boolean} // TODO(JSDOC). 281 */ 282ByteReader.prototype.beof = function() { 283 return this.pos_ >= this.view_.byteLength || this.pos_ < 0; 284}; 285 286/** 287 * Set the expected byte ordering for future reads. 288 * @param {number} order // TODO(JSDOC). 289 */ 290ByteReader.prototype.setByteOrder = function(order) { 291 this.littleEndian_ = order == ByteReader.LITTLE_ENDIAN; 292}; 293 294/** 295 * Throw an error if the reader is at an invalid position, or if a read a read 296 * of |size| would put it in one. 297 * 298 * You may optionally pass opt_end to override what is considered to be the 299 * end of the buffer. 300 * 301 * @param {number} size // TODO(JSDOC). 302 * @param {number=} opt_end // TODO(JSDOC). 303 */ 304ByteReader.prototype.validateRead = function(size, opt_end) { 305 if (typeof opt_end == 'undefined') 306 opt_end = this.view_.byteLength; 307 308 ByteReader.validateRead(this.view_, this.pos_, size, opt_end); 309}; 310 311/** 312 * @param {number} width // TODO(JSDOC). 313 * @param {boolean=} opt_signed // TODO(JSDOC). 314 * @param {number=} opt_end // TODO(JSDOC). 315 * @return {string} // TODO(JSDOC). 316 */ 317ByteReader.prototype.readScalar = function(width, opt_signed, opt_end) { 318 var method = opt_signed ? 'getInt' : 'getUint'; 319 320 switch (width) { 321 case 1: 322 method += '8'; 323 break; 324 325 case 2: 326 method += '16'; 327 break; 328 329 case 4: 330 method += '32'; 331 break; 332 333 case 8: 334 method += '64'; 335 break; 336 337 default: 338 throw new Error('Invalid width: ' + width); 339 break; 340 } 341 342 this.validateRead(width, opt_end); 343 var rv = this.view_[method](this.pos_, this.littleEndian_); 344 this.pos_ += width; 345 return rv; 346}; 347 348/** 349 * Read as a sequence of characters, returning them as a single string. 350 * 351 * Adjusts the current position on success. Throws an exception if the 352 * read would go past the end of the buffer. 353 * 354 * @param {number} size // TODO(JSDOC). 355 * @param {number=} opt_end // TODO(JSDOC). 356 * @return {string} // TODO(JSDOC). 357 */ 358ByteReader.prototype.readString = function(size, opt_end) { 359 var rv = ByteReader.readString(this.view_, this.pos_, size, opt_end); 360 this.pos_ += size; 361 return rv; 362}; 363 364 365/** 366 * Read as a sequence of characters, returning them as a single string. 367 * 368 * Adjusts the current position on success. Throws an exception if the 369 * read would go past the end of the buffer. 370 * 371 * @param {number} size // TODO(JSDOC). 372 * @param {number=} opt_end // TODO(JSDOC). 373 * @return {string} // TODO(JSDOC). 374 */ 375ByteReader.prototype.readNullTerminatedString = function(size, opt_end) { 376 var rv = ByteReader.readNullTerminatedString(this.view_, 377 this.pos_, 378 size, 379 opt_end); 380 this.pos_ += rv.length; 381 382 if (rv.length < size) { 383 // If we've stopped reading because we found '0' but didn't hit size limit 384 // then we should skip additional '0' character 385 this.pos_++; 386 } 387 388 return rv; 389}; 390 391 392/** 393 * Read as a sequence of UTF16 characters, returning them as a single string. 394 * 395 * Adjusts the current position on success. Throws an exception if the 396 * read would go past the end of the buffer. 397 * 398 * @param {boolean} bom // TODO(JSDOC). 399 * @param {number} size // TODO(JSDOC). 400 * @param {number=} opt_end // TODO(JSDOC). 401 * @return {string} // TODO(JSDOC). 402 */ 403ByteReader.prototype.readNullTerminatedStringUTF16 = 404 function(bom, size, opt_end) { 405 var rv = ByteReader.readNullTerminatedStringUTF16( 406 this.view_, this.pos_, bom, size, opt_end); 407 408 if (bom) { 409 // If the BOM word was present advance the position. 410 this.pos_ += 2; 411 } 412 413 this.pos_ += rv.length; 414 415 if (rv.length < size) { 416 // If we've stopped reading because we found '0' but didn't hit size limit 417 // then we should skip additional '0' character 418 this.pos_ += 2; 419 } 420 421 return rv; 422}; 423 424 425/** 426 * Read as an array of numbers. 427 * 428 * Adjusts the current position on success. Throws an exception if the 429 * read would go past the end of the buffer. 430 * 431 * @param {number} size // TODO(JSDOC). 432 * @param {number=} opt_end // TODO(JSDOC). 433 * @param {function(new:Array.<*>)=} opt_arrayConstructor // TODO(JSDOC). 434 * @return {Array.<*>} // TODO(JSDOC). 435 */ 436ByteReader.prototype.readSlice = function(size, opt_end, 437 opt_arrayConstructor) { 438 this.validateRead(size, opt_end); 439 440 var arrayConstructor = opt_arrayConstructor || Uint8Array; 441 var slice = new arrayConstructor( 442 this.view_.buffer, this.view_.byteOffset + this.pos, size); 443 this.pos_ += size; 444 445 return slice; 446}; 447 448/** 449 * Read as a sequence of bytes, returning them as a single base64 encoded 450 * string. 451 * 452 * Adjusts the current position on success. Throws an exception if the 453 * read would go past the end of the buffer. 454 * 455 * @param {number} size // TODO(JSDOC). 456 * @param {number=} opt_end // TODO(JSDOC). 457 * @return {string} // TODO(JSDOC). 458 */ 459ByteReader.prototype.readBase64 = function(size, opt_end) { 460 var rv = ByteReader.readBase64(this.view_, this.pos_, size, opt_end); 461 this.pos_ += size; 462 return rv; 463}; 464 465/** 466 * Read an image returning it as a data url. 467 * 468 * Adjusts the current position on success. Throws an exception if the 469 * read would go past the end of the buffer. 470 * 471 * @param {number} size // TODO(JSDOC). 472 * @param {number=} opt_end // TODO(JSDOC). 473 * @return {string} // TODO(JSDOC). 474 */ 475ByteReader.prototype.readImage = function(size, opt_end) { 476 var rv = ByteReader.readImage(this.view_, this.pos_, size, opt_end); 477 this.pos_ += size; 478 return rv; 479}; 480 481/** 482 * Seek to a give position relative to opt_seekStart. 483 * 484 * @param {number} pos // TODO(JSDOC). 485 * @param {number=} opt_seekStart // TODO(JSDOC). 486 * @param {number=} opt_end // TODO(JSDOC). 487 */ 488ByteReader.prototype.seek = function(pos, opt_seekStart, opt_end) { 489 opt_end = opt_end || this.view_.byteLength; 490 491 var newPos; 492 if (opt_seekStart == ByteReader.SEEK_CUR) { 493 newPos = this.pos_ + pos; 494 } else if (opt_seekStart == ByteReader.SEEK_END) { 495 newPos = opt_end + pos; 496 } else { 497 newPos = pos; 498 } 499 500 if (newPos < 0 || newPos > this.view_.byteLength) 501 throw new Error('Seek outside of buffer: ' + (newPos - opt_end)); 502 503 this.pos_ = newPos; 504}; 505 506/** 507 * Seek to a given position relative to opt_seekStart, saving the current 508 * position. 509 * 510 * Recover the current position with a call to seekPop. 511 * 512 * @param {number} pos // TODO(JSDOC). 513 * @param {number=} opt_seekStart // TODO(JSDOC). 514 */ 515ByteReader.prototype.pushSeek = function(pos, opt_seekStart) { 516 var oldPos = this.pos_; 517 this.seek(pos, opt_seekStart); 518 // Alter the seekStack_ after the call to seek(), in case it throws. 519 this.seekStack_.push(oldPos); 520}; 521 522/** 523 * Undo a previous seekPush. 524 */ 525ByteReader.prototype.popSeek = function() { 526 this.seek(this.seekStack_.pop()); 527}; 528 529/** 530 * Return the current read position. 531 * @return {number} // TODO(JSDOC). 532 */ 533ByteReader.prototype.tell = function() { 534 return this.pos_; 535}; 536