summaryrefslogtreecommitdiffstats
path: root/toolkit/components/lz4/lz4.js
blob: 8d4ffcf8e58e39731bb349ec94120f40aca8d7fa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var SharedAll;
var Primitives;
if (typeof Components != "undefined") {
  let Cu = Components.utils;
  SharedAll = {};
  Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
  Cu.import("resource://gre/modules/lz4_internal.js");
  Cu.import("resource://gre/modules/ctypes.jsm");

  this.EXPORTED_SYMBOLS = [
    "Lz4"
  ];
  this.exports = {};
} else if (typeof module != "undefined" && typeof require != "undefined") {
  SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
  Primitives = require("resource://gre/modules/lz4_internal.js");
} else {
  throw new Error("Please load this module with Component.utils.import or with require()");
}

const MAGIC_NUMBER = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]); // "mozLz4a\0"

const BYTES_IN_SIZE_HEADER = ctypes.uint32_t.size;

const HEADER_SIZE = MAGIC_NUMBER.byteLength + BYTES_IN_SIZE_HEADER;

const EXPECTED_HEADER_TYPE = new ctypes.ArrayType(ctypes.uint8_t, HEADER_SIZE);
const EXPECTED_SIZE_BUFFER_TYPE = new ctypes.ArrayType(ctypes.uint8_t, BYTES_IN_SIZE_HEADER);

/**
 * An error during (de)compression
 *
 * @param {string} operation The name of the operation ("compress", "decompress")
 * @param {string} reason A reason to be used when matching errors. Must start
 * with "because", e.g. "becauseInvalidContent".
 * @param {string} message A human-readable message.
 */
function LZError(operation, reason, message) {
  SharedAll.OSError.call(this);
  this.operation = operation;
  this[reason] = true;
  this.message = message;
}
LZError.prototype = Object.create(SharedAll.OSError);
LZError.prototype.toString = function toString() {
  return this.message;
};
exports.Error = LZError;

/**
 * Compress a block to a form suitable for writing to disk.
 *
 * Compatibility note: For the moment, we are basing our code on lz4
 * 1.3, which does not specify a *file* format. Therefore, we define
 * our own format. Once lz4 defines a complete file format, we will
 * migrate both |compressFileContent| and |decompressFileContent| to this file
 * format. For backwards-compatibility, |decompressFileContent| will however
 * keep the ability to decompress files provided with older versions of
 * |compressFileContent|.
 *
 * Compressed files have the following layout:
 *
 * | MAGIC_NUMBER (8 bytes) | content size (uint32_t, little endian) | content, as obtained from lz4_compress |
 *
 * @param {TypedArray|void*} buffer The buffer to write to the disk.
 * @param {object=} options An object that may contain the following fields:
 *  - {number} bytes The number of bytes to read from |buffer|. If |buffer|
 *    is an |ArrayBuffer|, |bytes| defaults to |buffer.byteLength|. If
 *    |buffer| is a |void*|, |bytes| MUST be provided.
 * @return {Uint8Array} An array of bytes suitable for being written to the
 * disk.
 */
function compressFileContent(array, options = {}) {
  // Prepare the output array
  let inputBytes;
  if (SharedAll.isTypedArray(array) && !(options && "bytes" in options)) {
    inputBytes = array.byteLength;
  } else if (options && options.bytes) {
    inputBytes = options.bytes;
  } else {
    throw new TypeError("compressFileContent requires a size");
  }
  let maxCompressedSize = Primitives.maxCompressedSize(inputBytes);
  let outputArray = new Uint8Array(HEADER_SIZE + maxCompressedSize);

  // Compress to output array
  let payload = new Uint8Array(outputArray.buffer, outputArray.byteOffset + HEADER_SIZE);
  let compressedSize = Primitives.compress(array, inputBytes, payload);

  // Add headers
  outputArray.set(MAGIC_NUMBER);
  let view = new DataView(outputArray.buffer);
  view.setUint32(MAGIC_NUMBER.byteLength, inputBytes, true);

  return new Uint8Array(outputArray.buffer, 0, HEADER_SIZE + compressedSize);
}
exports.compressFileContent = compressFileContent;

function decompressFileContent(array, options = {}) {
  let bytes = SharedAll.normalizeBufferArgs(array, options.bytes || null);
  if (bytes < HEADER_SIZE) {
    throw new LZError("decompress", "becauseLZNoHeader",
      `Buffer is too short (no header) - Data: ${ options.path || array }`);
  }

  // Read headers
  let expectMagicNumber = new DataView(array.buffer, 0, MAGIC_NUMBER.byteLength);
  for (let i = 0; i < MAGIC_NUMBER.byteLength; ++i) {
    if (expectMagicNumber.getUint8(i) != MAGIC_NUMBER[i]) {
      throw new LZError("decompress", "becauseLZWrongMagicNumber",
        `Invalid header (no magic number) - Data: ${ options.path || array }`);
    }
  }

  let sizeBuf = new DataView(array.buffer, MAGIC_NUMBER.byteLength, BYTES_IN_SIZE_HEADER);
  let expectDecompressedSize =
    sizeBuf.getUint8(0) +
    (sizeBuf.getUint8(1) << 8) +
    (sizeBuf.getUint8(2) << 16) +
    (sizeBuf.getUint8(3) << 24);
  if (expectDecompressedSize == 0) {
    // The underlying algorithm cannot handle a size of 0
    return new Uint8Array(0);
  }

  // Prepare the input buffer
  let inputData = new DataView(array.buffer, HEADER_SIZE);

  // Prepare the output buffer
  let outputBuffer = new Uint8Array(expectDecompressedSize);
  let decompressedBytes = (new SharedAll.Type.size_t.implementation(0));

  // Decompress
  let success = Primitives.decompress(inputData, bytes - HEADER_SIZE,
                                      outputBuffer, outputBuffer.byteLength,
                                      decompressedBytes.address());
  if (!success) {
    throw new LZError("decompress", "becauseLZInvalidContent",
      `Invalid content: Decompression stopped at ${decompressedBytes.value} - Data: ${ options.path || array }`);
  }
  return new Uint8Array(outputBuffer.buffer, outputBuffer.byteOffset, decompressedBytes.value);
}
exports.decompressFileContent = decompressFileContent;

if (typeof Components != "undefined") {
  this.Lz4 = {
    compressFileContent: compressFileContent,
    decompressFileContent: decompressFileContent
  };
}