Reference Source Test

src/index.js

import {blobToBinaryString, blobToArrayBuffer, compareBuffers} from './lib';
import WebworkerPromise from 'webworker-promise';
import Worker from './main.worker';

/**
 * Detect if workers are enabled in current browser
 * @type {Boolean}
 */
export let workersEnabled = Boolean(window.Worker);

/**
 * Utility class to nest all methods
 *
 * ## Conversion tools ###
 All conversions are run asynchronously.

 Method  |  Description
 --|--
 `blobCompare::toArrayBuffer` |  Converts a blob to an ArrayBuffer. it can be optionnally chunked and assigned to a web worker. Conversion is run asynchronously.
 `blobCompare::toBinaryString`  |  Converts a blob to a BinaryString. it can be optionnally chunked and assigned to a web worker. Conversion is run asynchronously.

 ## Comparison tools ###
 Method  |  Description | Sync/Async
 --|--|:--:
 `blobCompare::sizeEqual` | Compares size of two blobs | sync
 `blobCompare::typeEqual`  | Compares types of two blobs. Types are not really reliable as they can be tricked when creating a blob |  sync
 `blobCompare::magicNumbersEqual`  | Compares magic numbers of two blobs. A quick comparison is done, therefore weird data types may not be compared with 100% accuracy. In that case, simply clone repo and override this function to fit your needs | async
 `blobCompare::bytesEqualWithArrayBuffer` | Converts blobs or chunk blobs to ArrayBuffers and performs a byte to byte comparison | async
 `blobCompare::bytesEqualWithBinaryString`  | Converts blobs or chunk blobs to BinaryString and performs a byte to byte comparison | async
 `blobCompare::isEqual`  | The swiss army knife to bundle multiple comparison methods above in one single call | async
 */
export default class blobCompare {

  /**
   * Convert a blob to a binary string through a web worker thread
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}    blob      Blob
   * @param   {Number}  chunk     Size in bytes to slice blob
   * @return  {Promise<String>}   Raw binary data as a string
   */
  static async toBinaryStringWithWorker(blob, chunk) {
    const worker = new WebworkerPromise(new Worker());
    const response = await worker.exec('binary', {blob, chunk});

    worker.terminate();
    return response;
  }

  /**
   * Convert a blob to a binary string through main thread
   *
   * The blob can optionnaly be sliced with the chunk arguments
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  blob Blob to convert and optionnally sample
   * @param   {Number}  chunk Size in bytes to slice blob
   * @return  {Promise<String>}       Binary data as a string
   */
  static toBinaryStringWithoutWorker(blob, chunk) {
    return blobToBinaryString(blob, chunk);
  }

  /**
   * Convert a blob to a binary string
   *
   * The blob can optionnaly be sliced with the chunk arguments
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  blob Blob to convert and optionnally sample
   * @param   {Number}  chunk Size in bytes to slice blob
   * @param   {Boolean} [worker=true] Wether to use webworkers if available
   * @return  {Promise<String>}       Binary data as a string
   */
  static toBinaryString(blob, chunk, worker = true) {
    return (worker && workersEnabled) ? this.toBinaryStringWithWorker(blob, chunk) : this.toBinaryStringWithoutWorker(blob, chunk);
  }

  /**
   * Convert a blob to an ArrayBuffer through a web worker
   *
   * The blob can optionnally be sliced with the `chunk`argument
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  blob Blob
   * @param   {Number}  chunk Size in bytes to slice blob
   * @return  {Promise<ArrayBuffer>}       Binary data as a buffer
   */
  static async toArrayBufferWithWorker(blob, chunk) {
    const worker = new WebworkerPromise(new Worker());
    const response = await worker.exec('buffer', {blob, chunk});

    worker.terminate();
    return response;
  }

  /**
   * Convert a blob to an ArrayBuffer through main thread
   *
   * The blob can optionnally be sliced with the `chunk`argument
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  blob Blob
   * @param   {Number}  chunk Size in bytes to slice blob
   * @return  {Promise<ArrayBuffer>}       Binary data as a buffer
   */
  static toArrayBufferWithoutWorker(blob, chunk) {
    return blobToArrayBuffer(blob, chunk);
  }

  /**
   * Convert a blob to an ArrayBuffer
   *
   * The blob can optionnally be sliced with the `chunk`argument
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  blob Blob
   * @param   {Number}  chunk Size in bytes to slice blob
   * @param   {Boolean} [worker=true] Wether to use webworkers if available
   * @return  {Promise<ArrayBuffer>}       Binary data as a buffer
   */
  static toArrayBuffer(blob, chunk, worker = true) {
    return (worker && workersEnabled) ? this.toArrayBufferWithWorker(blob, chunk) : this.toArrayBufferWithoutWorker(blob, chunk);
  }

  /**
   * Compares two buffers byte through web worker
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {ArrayBuffer}  buf1          First buffer
   * @param   {ArrayBuffer}  buf2          Second buffer
   * @return  {Promise<Boolean>}           `true` if buffers are equal
   */
  static async compareBuffersWithWorker(buf1, buf2) {
    if (buf1 === buf2) return true;

    const worker = new WebworkerPromise(new Worker());
    const response = await worker.exec('compare', {buf1, buf2}, [buf1, buf2]);

    worker.terminate();
    return response;
  }

  /**
   * Compares two buffers byte to byte through main thread
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {ArrayBuffer}  buf1          First buffer
   * @param   {ArrayBuffer}  buf2          Second buffer
   * @return  {Boolean}           `true` if buffers are equal
   */
  static compareBuffersWithoutWorker(buf1, buf2) {
    return compareBuffers(buf1, buf2);
  }

  /**
   * Compares two buffers byte to byte
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {ArrayBuffer}  buf1          First buffer
   * @param   {ArrayBuffer}  buf2          Second buffer
   * @param   {Boolean} [worker=true]      Whether to use worker or not
   * @return  {Promise<Boolean>|Boolean}   `true` if buffers are equal
   */
  static compareBuffers(buf1, buf2, worker = true) {
    return (worker && workersEnabled) ? this.compareBuffersWithWorker(buf1, buf2) : this.compareBuffersWithoutWorker(buf1, buf2);
  }

  /**
   * Compare size of two blobs
   *
   * Obviously, two different blobs in content can have the same size and this method is only useful
   * to discriminate blobs
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  b1 First blob
   * @param   {Blob}  b2 Second Blob
   * @return  {Boolean}     `true` if sizes are equal
   */
  static sizeEqual(b1, b2) {
    return b1.size === b2.size;
  }

  /**
   * Compare type of two blobs
   *
   * Never rely solely on this method to discriminate blobs or, worse, consider them as equal
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}    b1  First blob
   * @param   {Blob}    b2  Second blob
   * @return  {Boolean}     `true` if types are equal
   */
  static typeEqual(b1, b2) {
    return b1.type === b2.type;
  }

  /**
   * Compares the magic numbers of two blobs
   *
   * This method simply compare byte to byte at the start of data where magic numbers are usually located in most cases. You can find a quite
   * exhaustive list of file signatures on [wikipedia](https://en.wikipedia.org/wiki/List_of_file_signatures)
   *
   * It does not provide any informations about file type, but you can easily use a library like [`file-type`](https://www.npmjs.com/package/file-type) to parse
   * more informations about data if needed.
   *
   * Be warned that this method can lead to false negative/positive for some file types given the currently naive algorithm.
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  b1 First blob
   * @param   {Blob}  b2 Second blob
   * @param   {Boolean} [worker=true] Wether to use webworkers if available
   * @return  {Promise<Boolean>}   `true` if magic numbers string is matching between two blogs   *
  */
  static async magicNumbersEqual(b1, b2, worker = true) {
    if (b1 === b2) return true;

    const sizes = [24, 16, 14, 12, 8, 6, 4];

    let [s1, s2] = await Promise.all([this.toBinaryString(b1, 24, worker), this.toBinaryString(b2, 24, worker)]);

    for (let size of sizes) {
      /* istanbul ignore else */
      if (s1.substring(0, size) === s2.substring(0, size)) return true;
    }

    return false;
  }

  /**
   * Compares two blobs by using binary strings
   *
   * This is not the default method to byte compare two blobs as benchmarks shows it's a little bit slower than using array buffers in most cases.
   *
   * There's still at least two cases where using binary string is much faster :
   * - Empty blobs
   * - Blobs much prone to have difference at the start of the data
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  b1   First blob
   * @param   {Blob}  b2   Second blob
   * @param   {Number}  size Size in bytes to slice blobs
   * @param   {Boolean} [worker=true] Wether to use webworkers if available
   * @return  {Promise<Boolean>}      Evaluates to `true` if blobs (or sliced blobs) are equals byte to byte
   */
  static async bytesEqualWithBinaryString(b1, b2, size, worker = true) {
    if (b1 === b2) return true;

    const [s1, s2] = await Promise.all([this.toBinaryString(b1, size, worker), this.toBinaryString(b2, size, worker)]);

    return s1 === s2;
  }

  /**
   * Compares two blobs by using arraybuffers
   *
   * This is the default comparison method
   *
   * @version 1.0.0
   * @since   1.0.0
   * @param   {Blob}  b1   First blob
   * @param   {Blob}  b2   Second blob
   * @param   {Number}  size Size in bytes to slice blobs
   * @param   {Boolean} [worker=true] Wether to use webworkers if available
   * @return  {Promise<Boolean>}      Evaluates to `true` if blobs (or sliced blobs) are equals byte to byte
   * @throws  Error When comparison method is not recognized
   */
  static async bytesEqualWithArrayBuffer(b1, b2, size, worker = true) {
    if (b1 === b2) return true;

    const [buf1, buf2] = await Promise.all([this.toArrayBuffer(b1, size, worker), this.toArrayBuffer(b2, size, worker)]);

    return this.compareBuffers(buf1, buf2, worker);
  }

  /**
   * Automatically compares two blobs by using the given methods
   *
   * Allowed methods are with aliases :
   * - `byte`, `bytes`, `content` : Performs a byte comparison between the two blogs. The optional `sizes` option parameter can be used to provide sizes
   * to perform comparison on sliced blobs. See {@link blobCompare.bytesEqualWithArrayBuffer} and {@link blobCompare.bytesEqualWithBinaryString} for more informations;
   * - `magic`, `headers`, `numbers`, `mime` : Compare two blobs based on magic numbers. See {@link blobCompare.magicNumbersEqual} for more informations;
   * - `size`, `sizes` : Compare two blobs based on their size in bytes. See {@link blobCompare.sizeEqual} for more informations;
   * - `type`, `types` : Compare two blobs based on their type. See {@link blobCompare.typeEqual} for more informations.
   *
   *
   * Using the `partial` option can be tricky as it's easy to have false positive.
   *
   * As default, `isEqual` performs first a check on `size` method to discrimate blobs, then `type`, then `magic` and fallback on `byte` comparison on full data.
   * This default order ensures the most optimized resource cost, though performing a complete comparison. For huge blobs, one may think about doing chunks comparison.
   *
   * Workers can be disabled through options
   *
   * @version 1.1.0
   * @since   1.0.0
   * @param   {Blob}  b1                First blob
   * @param   {Blob}  b2                Second blob
   * @param   {Object}  [options]   Configuration to use when performing comparison
   * @param   {Array}   [options.methods=['size', 'type', 'magic', 'byte']] Default methods used for comparison. Methods are applied in the same order
   * @param   {String}  [options.byte='buffer']   If set to `buffer`, byte comparison will be based on arraybuffers. Otherwise, it will use binary strings
   * @param   {Boolean} [options.partial=false]   When set to `true`, the first successful comparison method will prevent further evaluations and return true immediately
   * @param   {Array}   [options.chunks=null]      Custom sizes to use when performing a byte comparison. It really have few usage as one must ensure a regular pattern in blobs data to avoid false positive
   * @param   {Boolean} [options.worker=true]      Wether to use web workers if available
   * @return  {Promise<Boolean>}                   If `true`, blobs are equals given the used methods
   */
  static async isEqual(b1, b2, {methods = ['size', 'type', 'magic', 'byte'], byte = 'buffer', partial = false, chunks = null, worker = true} = {}) {
    let passed = null;

    for (let method of methods) {
      if (passed === false) break;
      if (partial && passed === true) break;

      switch (method) {
        case 'byte':
        case 'bytes':
        case 'content':
          chunks = chunks instanceof Array ? chunks : [b1.size];
          passed = true;

          for (let chunk of chunks) {
            let chunkPassed = false;

            chunkPassed = byte === 'buffer' ? await this.bytesEqualWithArrayBuffer(b1, b2, chunk, worker) : await this.bytesEqualWithBinaryString(b1, b2, chunk, worker);
            if (!chunkPassed) passed = false;
          }
          break;

        case 'magic':
        case 'headers':
        case 'numbers':
        case 'mime':
          passed = await this.magicNumbersEqual(b1, b2, worker);
          break;

        case 'size':
        case 'sizes':
          passed = this.sizeEqual(b1, b2);
          break;

        case 'type':
        case 'types':
          passed = this.typeEqual(b1, b2);
          break;

        default: throw new Error('Blob-compare : Unknown comparison method');
      }
    }

    return passed;
  }
}