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 An array of buffers to be read from.
10 * @param {number=} opt_offset Offset to read bytes at.
11 * @param {number=} opt_length Number of bytes to read.
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 Position in the file.
62 * @param {number} size Number of bytes to read.
63 * @param {number} end Maximum position to read from.
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 Data view instance.
80 * @param {number} pos Position in bytes to read from.
81 * @param {number} size Number of bytes to read.
82 * @param {number=} opt_end Maximum position to read from.
83 * @return {string} Read string.
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 Data view instance.
103 * @param {number} pos Position in bytes to read from.
104 * @param {number} size Number of bytes to read.
105 * @param {number=} opt_end Maximum position to read from.
106 * @return {string} Read string.
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 Data view instance.
129 * @param {number} pos Position in bytes to read from.
130 * @param {boolean} bom True if BOM should be parsed.
131 * @param {number} size Number of bytes to read.
132 * @param {number=} opt_end Maximum position to read from.
133 * @return {string} Read string.
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 Data view instance.
175 * @param {number} pos Position in bytes to read from.
176 * @param {number} size Number of bytes to read.
177 * @param {number=} opt_end Maximum position to read from.
178 * @return {string} Base 64 encoded value.
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 Data view instance.
225 * @param {number} pos Position in bytes to read from.
226 * @param {number} size Number of bytes to read.
227 * @param {number=} opt_end Maximum position to read from.
228 * @return {string} Image as a data url.
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 Number of bytes to read.
256 * @return {boolean} True if allowed, false otherwise.
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} True if EOF, otherwise false.
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} True if BOF, otherwise false.
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} True if outside, false if inside.
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 Byte order. Either LITTLE_ENDIAN or BIG_ENDIAN.
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 Number of bytes to read.
302 * @param {number=} opt_end Maximum position to read from.
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 Number of bytes to read.
313 * @param {boolean=} opt_signed True if signed, false otherwise.
314 * @param {number=} opt_end Maximum position to read from.
315 * @return {string} Scalar value.
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 Number of bytes to read.
355 * @param {number=} opt_end Maximum position to read from.
356 * @return {string} String value.
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 Number of bytes to read.
372 * @param {number=} opt_end Maximum position to read from.
373 * @return {string} Null-terminated string value.
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 True if BOM should be parsed.
399 * @param {number} size Number of bytes to read.
400 * @param {number=} opt_end Maximum position to read from.
401 * @return {string} Read string.
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 Number of bytes to read.
432 * @param {number=} opt_end Maximum position to read from.
433 * @param {function(new:Array.<*>)=} opt_arrayConstructor Array constructor.
434 * @return {Array.<*>} Array of bytes.
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 Number of bytes to read.
456 * @param {number=} opt_end Maximum position to read from.
457 * @return {string} Base 64 encoded value.
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 Number of bytes to read.
472 * @param {number=} opt_end Maximum position to read from.
473 * @return {string} Image as a data url.
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 Position in bytes to seek to.
485 * @param {number=} opt_seekStart Relative position in bytes.
486 * @param {number=} opt_end Maximum position to seek to.
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 Position in bytes to seek to.
513 * @param {number=} opt_seekStart Relative position in bytes.
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} Current position in bytes.
532 */
533ByteReader.prototype.tell = function() {
534  return this.pos_;
535};
536