// 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 * @struct * @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} * @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} 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); };