| // Copyright 2011 The Closure Library Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS-IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| /** |
| * @fileoverview Asynchronous hash computer for the Blob interface. |
| * |
| * The Blob interface, part of the HTML5 File API, is supported on Chrome 7+, |
| * Firefox 4.0 and Opera 11. No Blob interface implementation is expected on |
| * Internet Explorer 10. Chrome 11, Firefox 5.0 and the subsequent release of |
| * Opera are supposed to use vendor prefixes due to evolving API, see |
| * http://dev.w3.org/2006/webapi/FileAPI/ for details. |
| * |
| * This implementation currently uses upcoming Chrome and Firefox prefixes, |
| * plus the original Blob.slice specification, as implemented on Chrome 10 |
| * and Firefox 4.0. |
| * |
| */ |
| |
| goog.provide('goog.crypt.BlobHasher'); |
| goog.provide('goog.crypt.BlobHasher.EventType'); |
| |
| goog.require('goog.asserts'); |
| goog.require('goog.events.EventTarget'); |
| goog.require('goog.fs'); |
| goog.require('goog.log'); |
| |
| |
| |
| /** |
| * Construct the hash computer. |
| * |
| * @param {!goog.crypt.Hash} hashFn The hash function to use. |
| * @param {number=} opt_blockSize Processing block size. |
| * @constructor |
| * @extends {goog.events.EventTarget} |
| * @final |
| */ |
| goog.crypt.BlobHasher = function(hashFn, opt_blockSize) { |
| goog.crypt.BlobHasher.base(this, 'constructor'); |
| |
| /** |
| * The actual hash function. |
| * @type {!goog.crypt.Hash} |
| * @private |
| */ |
| this.hashFn_ = hashFn; |
| |
| /** |
| * The blob being processed or null if no blob is being processed. |
| * @type {Blob} |
| * @private |
| */ |
| this.blob_ = null; |
| |
| /** |
| * Computed hash value. |
| * @type {Array<number>} |
| * @private |
| */ |
| this.hashVal_ = null; |
| |
| /** |
| * Number of bytes already processed. |
| * @type {number} |
| * @private |
| */ |
| this.bytesProcessed_ = 0; |
| |
| /** |
| * The number of bytes to hash or Infinity for no limit. |
| * @type {number} |
| * @private |
| */ |
| this.hashingLimit_ = Infinity; |
| |
| /** |
| * Processing block size. |
| * @type {number} |
| * @private |
| */ |
| this.blockSize_ = opt_blockSize || 5000000; |
| |
| /** |
| * File reader object. Will be null if no chunk is currently being read. |
| * @type {FileReader} |
| * @private |
| */ |
| this.fileReader_ = null; |
| |
| /** |
| * The logger used by this object. |
| * @type {goog.log.Logger} |
| * @private |
| */ |
| this.logger_ = goog.log.getLogger('goog.crypt.BlobHasher'); |
| }; |
| goog.inherits(goog.crypt.BlobHasher, goog.events.EventTarget); |
| |
| |
| /** |
| * Event names for hash computation events |
| * @enum {string} |
| */ |
| goog.crypt.BlobHasher.EventType = { |
| STARTED: 'started', |
| PROGRESS: 'progress', |
| THROTTLED: 'throttled', |
| COMPLETE: 'complete', |
| ABORT: 'abort', |
| ERROR: 'error' |
| }; |
| |
| |
| /** |
| * Start the hash computation. |
| * @param {!Blob} blob The blob of data to compute the hash for. |
| */ |
| goog.crypt.BlobHasher.prototype.hash = function(blob) { |
| this.abort(); |
| this.hashFn_.reset(); |
| this.blob_ = blob; |
| this.hashVal_ = null; |
| this.bytesProcessed_ = 0; |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.STARTED); |
| |
| this.processNextBlock_(); |
| }; |
| |
| |
| /** |
| * Sets the maximum number of bytes to hash or Infinity for no limit. Can be |
| * called before hash() to throttle the hash computation. The hash computation |
| * can then be continued by repeatedly calling setHashingLimit() with greater |
| * byte offsets. This is useful if you don't need the hash until some time in |
| * the future, for example when uploading a file and you don't need the hash |
| * until the transfer is complete. |
| * @param {number} byteOffset The byte offset to compute the hash up to. |
| * Should be a non-negative integer or Infinity for no limit. Negative |
| * values are not allowed. |
| */ |
| goog.crypt.BlobHasher.prototype.setHashingLimit = function(byteOffset) { |
| goog.asserts.assert(byteOffset >= 0, 'Hashing limit must be non-negative.'); |
| this.hashingLimit_ = byteOffset; |
| |
| // Resume processing if a blob is currently being hashed, but no block read |
| // is currently in progress. |
| if (this.blob_ && !this.fileReader_) { |
| this.processNextBlock_(); |
| } |
| }; |
| |
| |
| /** |
| * Abort hash computation. |
| */ |
| goog.crypt.BlobHasher.prototype.abort = function() { |
| if (this.fileReader_) { |
| this.fileReader_.abort(); |
| this.fileReader_ = null; |
| } |
| |
| if (this.blob_) { |
| this.blob_ = null; |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.ABORT); |
| } |
| }; |
| |
| |
| /** |
| * @return {number} Number of bytes processed so far. |
| */ |
| goog.crypt.BlobHasher.prototype.getBytesProcessed = function() { |
| return this.bytesProcessed_; |
| }; |
| |
| |
| /** |
| * @return {Array<number>} The computed hash value or null if not ready. |
| */ |
| goog.crypt.BlobHasher.prototype.getHash = function() { |
| return this.hashVal_; |
| }; |
| |
| |
| /** |
| * Helper function setting up the processing for the next block, or finalizing |
| * the computation if all blocks were processed. |
| * @private |
| */ |
| goog.crypt.BlobHasher.prototype.processNextBlock_ = function() { |
| goog.asserts.assert(this.blob_, 'A hash computation must be in progress.'); |
| |
| if (this.bytesProcessed_ < this.blob_.size) { |
| |
| if (this.hashingLimit_ <= this.bytesProcessed_) { |
| // Throttle limit reached. Wait until we are allowed to hash more bytes. |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.THROTTLED); |
| return; |
| } |
| |
| // We have to reset the FileReader every time, otherwise it fails on |
| // Chrome, including the latest Chrome 12 beta. |
| // http://code.google.com/p/chromium/issues/detail?id=82346 |
| this.fileReader_ = new FileReader(); |
| this.fileReader_.onload = goog.bind(this.onLoad_, this); |
| this.fileReader_.onerror = goog.bind(this.onError_, this); |
| |
| var endOffset = Math.min(this.hashingLimit_, this.blob_.size); |
| var size = Math.min(endOffset - this.bytesProcessed_, this.blockSize_); |
| var chunk = goog.fs.sliceBlob(this.blob_, this.bytesProcessed_, |
| this.bytesProcessed_ + size); |
| if (!chunk || chunk.size != size) { |
| goog.log.error(this.logger_, 'Failed slicing the blob'); |
| this.onError_(); |
| return; |
| } |
| |
| if (this.fileReader_.readAsArrayBuffer) { |
| this.fileReader_.readAsArrayBuffer(chunk); |
| } else if (this.fileReader_.readAsBinaryString) { |
| this.fileReader_.readAsBinaryString(chunk); |
| } else { |
| goog.log.error(this.logger_, 'Failed calling the chunk reader'); |
| this.onError_(); |
| } |
| } else { |
| this.hashVal_ = this.hashFn_.digest(); |
| this.blob_ = null; |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.COMPLETE); |
| } |
| }; |
| |
| |
| /** |
| * Handle processing block loaded. |
| * @private |
| */ |
| goog.crypt.BlobHasher.prototype.onLoad_ = function() { |
| goog.log.info(this.logger_, 'Successfully loaded a chunk'); |
| |
| var array = null; |
| if (this.fileReader_.result instanceof Array || |
| goog.isString(this.fileReader_.result)) { |
| array = this.fileReader_.result; |
| } else if (goog.global['ArrayBuffer'] && goog.global['Uint8Array'] && |
| this.fileReader_.result instanceof ArrayBuffer) { |
| array = new Uint8Array(this.fileReader_.result); |
| } |
| if (!array) { |
| goog.log.error(this.logger_, 'Failed reading the chunk'); |
| this.onError_(); |
| return; |
| } |
| |
| this.hashFn_.update(array); |
| this.bytesProcessed_ += array.length; |
| this.fileReader_ = null; |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.PROGRESS); |
| |
| this.processNextBlock_(); |
| }; |
| |
| |
| /** |
| * Handles error. |
| * @private |
| */ |
| goog.crypt.BlobHasher.prototype.onError_ = function() { |
| this.fileReader_ = null; |
| this.blob_ = null; |
| this.dispatchEvent(goog.crypt.BlobHasher.EventType.ERROR); |
| }; |