diff options
Diffstat (limited to 'shared/js/blobview.js')
-rw-r--r-- | shared/js/blobview.js | 412 |
1 files changed, 412 insertions, 0 deletions
diff --git a/shared/js/blobview.js b/shared/js/blobview.js new file mode 100644 index 0000000..eda519a --- /dev/null +++ b/shared/js/blobview.js @@ -0,0 +1,412 @@ +'use strict'; + +var BlobView = (function() { + function fail(msg) { + throw Error(msg); + } + + // This constructor is for internal use only. + // Use the BlobView.get() factory function or the getMore instance method + // to obtain a BlobView object. + function BlobView(blob, sliceOffset, sliceLength, slice, + viewOffset, viewLength, littleEndian) + { + this.blob = blob; // The parent blob that the data is from + this.sliceOffset = sliceOffset; // The start address within the blob + this.sliceLength = sliceLength; // How long the slice is + this.slice = slice; // The ArrayBuffer of slice data + this.viewOffset = viewOffset; // The start of the view within the slice + this.viewLength = viewLength; // The length of the view + this.littleEndian = littleEndian; // Read little endian by default? + + // DataView wrapper around the ArrayBuffer + this.view = new DataView(slice, viewOffset, viewLength); + + // These fields mirror those of DataView + this.buffer = slice; + this.byteLength = viewLength; + this.byteOffset = viewOffset; + + this.index = 0; // The read methods keep track of the read position + } + + // Async factory function + BlobView.get = function(blob, offset, length, callback, littleEndian) { + if (offset < 0) + fail('negative offset'); + if (length < 0) + fail('negative length'); + if (offset > blob.size) + fail('offset larger than blob size'); + + // Don't fail if the length is too big; just reduce the length + if (offset + length > blob.size) + length = blob.size - offset; + + var slice = blob.slice(offset, offset + length); + var reader = new FileReader(); + reader.readAsArrayBuffer(slice); + reader.onloadend = function() { + var result = null; + if (reader.result) { + result = new BlobView(blob, offset, length, reader.result, + 0, length, littleEndian || false); + } + callback(result, reader.error); + } + }; + + BlobView.prototype = { + constructor: BlobView, + + // This instance method is like the BlobView.get() factory method, + // but it is here because if the current buffer includes the requested + // range of bytes, they can be passed directly to the callback without + // going back to the blob to read them + getMore: function(offset, length, callback) { + if (offset >= this.sliceOffset && + offset + length <= this.sliceOffset + this.sliceLength) { + // The quick case: we already have that region of the blob + callback(new BlobView(this.blob, + this.sliceOffset, this.sliceLength, this.slice, + offset - this.sliceOffset, length, + this.littleEndian)); + } + else { + // Otherwise, we have to do an async read to get more bytes + BlobView.get(this.blob, offset, length, callback, this.littleEndian); + } + }, + + // Set the default endianness for the other methods + littleEndian: function() { + this.littleEndian = true; + }, + bigEndian: function() { + this.littleEndian = false; + }, + + // These "get" methods are just copies of the DataView methods, except + // that they honor the default endianness + getUint8: function(offset) { + return this.view.getUint8(offset); + }, + getInt8: function(offset) { + return this.view.getInt8(offset); + }, + getUint16: function(offset, le) { + return this.view.getUint16(offset, + le !== undefined ? le : this.littleEndian); + }, + getInt16: function(offset, le) { + return this.view.getInt16(offset, + le !== undefined ? le : this.littleEndian); + }, + getUint32: function(offset, le) { + return this.view.getUint32(offset, + le !== undefined ? le : this.littleEndian); + }, + getInt32: function(offset, le) { + return this.view.getInt32(offset, + le !== undefined ? le : this.littleEndian); + }, + getFloat32: function(offset, le) { + return this.view.getFloat32(offset, + le !== undefined ? le : this.littleEndian); + }, + getFloat64: function(offset, le) { + return this.view.getFloat64(offset, + le !== undefined ? le : this.littleEndian); + }, + + // These "read" methods read from the current position in the view and + // update that position accordingly + readByte: function() { + return this.view.getInt8(this.index++); + }, + readUnsignedByte: function() { + return this.view.getUint8(this.index++); + }, + readShort: function(le) { + var val = this.view.getInt16(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 2; + return val; + }, + readUnsignedShort: function(le) { + var val = this.view.getUint16(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 2; + return val; + }, + readInt: function(le) { + var val = this.view.getInt32(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 4; + return val; + }, + readUnsignedInt: function(le) { + var val = this.view.getUint32(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 4; + return val; + }, + readFloat: function(le) { + var val = this.view.getFloat32(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 4; + return val; + }, + readDouble: function(le) { + var val = this.view.getFloat64(this.index, + le !== undefined ? le : this.littleEndian); + this.index += 8; + return val; + }, + + // Methods to get and set the current position + tell: function() { + return this.index; + }, + seek: function(index) { + if (index < 0) + fail('negative index'); + if (index >= this.byteLength) + fail('index greater than buffer size'); + this.index = index; + }, + advance: function(n) { + var index = this.index + n; + if (index < 0) + fail('advance past beginning of buffer'); + if (index >= this.byteLength) + fail('advance past end of buffer'); + this.index = index; + }, + + // Additional methods to read other useful things + getUnsignedByteArray: function(offset, n) { + return new Uint8Array(this.buffer, offset + this.viewOffset, n); + }, + + // Additional methods to read other useful things + readUnsignedByteArray: function(n) { + var val = new Uint8Array(this.buffer, this.index + this.viewOffset, n); + this.index += n; + return val; + }, + + getBit: function(offset, bit) { + var byte = this.view.getUint8(offset); + return (byte & (1 << bit)) !== 0; + }, + + getUint24: function(offset, le) { + var b1, b2, b3; + if (le !== undefined ? le : this.littleEndian) { + b1 = this.view.getUint8(offset); + b2 = this.view.getUint8(offset + 1); + b3 = this.view.getUint8(offset + 2); + } + else { // big end first + b3 = this.view.getUint8(offset); + b2 = this.view.getUint8(offset + 1); + b1 = this.view.getUint8(offset + 2); + } + + return (b3 << 16) + (b2 << 8) + b1; + }, + + readUint24: function(le) { + var value = this.getUint24(this.index, le); + this.index += 3; + return value; + }, + + // There are lots of ways to read strings. + // ASCII, UTF-8, UTF-16. + // null-terminated, character length, byte length + // I'll implement string reading methods as needed + + getASCIIText: function(offset, len) { + var bytes = new Uint8Array(this.buffer, offset + this.viewOffset, len); + return String.fromCharCode.apply(String, bytes); + }, + + readASCIIText: function(len) { + var bytes = new Uint8Array(this.buffer, + this.index + this.viewOffset, + len); + this.index += len; + return String.fromCharCode.apply(String, bytes); + }, + + // Replace this with the StringEncoding API when we've got it. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=764234 + getUTF8Text: function(offset, len) { + function fail() { throw new Error('Illegal UTF-8'); } + + var pos = offset; // Current position in this.view + var end = offset + len; // Last position + var charcode; // Current charcode + var s = ''; // Accumulate the string + var b1, b2, b3, b4; // Up to 4 bytes per charcode + + // See http://en.wikipedia.org/wiki/UTF-8 + while (pos < end) { + var b1 = this.view.getUint8(pos); + if (b1 < 128) { + s += String.fromCharCode(b1); + pos += 1; + } + else if (b1 < 194) { + // unexpected continuation character... + fail(); + } + else if (b1 < 224) { + // 2-byte sequence + if (pos + 1 >= end) + fail(); + b2 = this.view.getUint8(pos + 1); + if (b2 < 128 || b2 > 191) + fail(); + charcode = ((b1 & 0x1f) << 6) + (b2 & 0x3f); + s += String.fromCharCode(charcode); + pos += 2; + } + else if (b1 < 240) { + // 3-byte sequence + if (pos + 3 >= end) + fail(); + b2 = this.view.getUint8(pos + 1); + if (b2 < 128 || b2 > 191) + fail(); + b3 = this.view.getUint8(pos + 2); + if (b3 < 128 || b3 > 191) + fail(); + charcode = ((b1 & 0x0f) << 12) + ((b2 & 0x3f) << 6) + (b3 & 0x3f); + s += String.fromCharCode(charcode); + pos += 3; + } + else if (b1 < 245) { + // 4-byte sequence + if (pos + 3 >= end) + fail(); + b2 = this.view.getUint8(pos + 1); + if (b2 < 128 || b2 > 191) + fail(); + b3 = this.view.getUint8(pos + 2); + if (b3 < 128 || b3 > 191) + fail(); + b4 = this.view.getUint8(pos + 3); + if (b4 < 128 || b4 > 191) + fail(); + charcode = ((b1 & 0x07) << 18) + + ((b2 & 0x3f) << 12) + + ((b3 & 0x3f) << 6) + + (b4 & 0x3f); + + // Now turn this code point into two surrogate pairs + charcode -= 0x10000; + s += String.fromCharCode(0xd800 + ((charcode & 0x0FFC00) >>> 10)); + s += String.fromCharCode(0xdc00 + (charcode & 0x0003FF)); + + pos += 4; + } + else { + // Illegal byte + fail(); + } + } + + return s; + }, + + readUTF8Text: function(len) { + try { + return this.getUTF8Text(this.index, len); + } + finally { + this.index += len; + } + }, + + // Read 4 bytes, ignore the high bit and combine them into a 28-bit + // big-endian unsigned integer. + // This format is used by the ID3v2 metadata. + getID3Uint28BE: function(offset) { + var b1 = this.view.getUint8(offset) & 0x7f; + var b2 = this.view.getUint8(offset + 1) & 0x7f; + var b3 = this.view.getUint8(offset + 2) & 0x7f; + var b4 = this.view.getUint8(offset + 3) & 0x7f; + return (b1 << 21) | (b2 << 14) | (b3 << 7) | b4; + }, + + readID3Uint28BE: function() { + var value = this.getID3Uint28BE(this.index); + this.index += 4; + return value; + }, + + // Read bytes up to and including a null terminator, but never + // more than size bytes. And return as a Latin1 string + readNullTerminatedLatin1Text: function(size) { + var s = ''; + for (var i = 0; i < size; i++) { + var charcode = this.view.getUint8(this.index + i); + if (charcode === 0) { + i++; + break; + } + s += String.fromCharCode(charcode); + } + this.index += i; + return s; + }, + + // Read bytes up to and including a null terminator, but never + // more than size bytes. And return as a UTF8 string + readNullTerminatedUTF8Text: function(size) { + for (var len = 0; len < size; len++) { + if (this.view.getUint8(this.index + len) === 0) + break; + } + var s = this.readUTF8Text(len); + if (len < size) // skip the null terminator if we found one + this.advance(1); + return s; + }, + + // Read UTF16 text. If le is not specified, expect a BOM to define + // endianness. If le is true, read UTF16LE, if false, UTF16BE + // Read until we find a null-terminator, but never more than size bytes + readNullTerminatedUTF16Text: function(size, le) { + if (le == null) { + var BOM = this.readUnsignedShort(); + size -= 2; + if (BOM === 0xFEFF) + le = false; + else + le = true; + } + + var s = ''; + for (var i = 0; i < size; i += 2) { + var charcode = this.getUint16(this.index + i, le); + if (charcode === 0) { + i += 2; + break; + } + s += String.fromCharCode(charcode); + } + this.index += i; + return s; + } + }; + + // We don't want users of this library to accidentally call the constructor + // instead of using the factory function, so we return a dummy object + // instead of the real constructor. If someone really needs to get at the + // real constructor, the contructor property of the prototype refers to it. + return { get: BlobView.get }; +}()); |