mirror of
https://github.com/asadbek064/hyparquet-compressors.git
synced 2026-01-11 21:26:38 +00:00
Brotli native
This commit is contained in:
parent
e2dae829b2
commit
209ca7d791
@ -52,7 +52,7 @@
|
||||
"no-var": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"prefer-const": "error",
|
||||
"prefer-destructuring": ["warn", {"object": true, "array": false}],
|
||||
"prefer-destructuring": ["warn", { "VariableDeclarator": { "object": true, "array": false }} ],
|
||||
"prefer-promise-reject-errors": "error",
|
||||
"quotes": ["error", "single"],
|
||||
"require-await": "warn",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
[](https://www.npmjs.com/package/hyparquet-compressors)
|
||||
[](https://github.com/hyparam/hyparquet-compressors/actions)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
|
||||
This package exports a `compressors` object intended to be passed into [hyparquet](https://github.com/hyparam/hyparquet).
|
||||
|
||||
@ -47,7 +47,7 @@ Includes modifications to handle repeated back-to-back gzip streams that sometim
|
||||
|
||||
## Brotli
|
||||
|
||||
Uses [brotli.js](https://github.com/foliojs/brotli.js) for brotli decompression.
|
||||
Includes a minimal port of [brotli.js](https://github.com/foliojs/brotli.js) which compresses the brotli dictionary using gzip and base64 to minimize the distribution bundle size.
|
||||
|
||||
## LZ4
|
||||
|
||||
@ -61,8 +61,8 @@ Uses [fzstd](https://github.com/101arrowz/fzstd) for Zstandard decompression.
|
||||
|
||||
| File | Size |
|
||||
| - | - |
|
||||
| hyparquet-compressors.min.js | 502.1kb |
|
||||
| hyparquet-compressors.min.js.gz | 102.2kb |
|
||||
| hyparquet-compressors.min.js | 116.1kb |
|
||||
| hyparquet-compressors.min.js.gz | 75.2kb |
|
||||
|
||||
# References
|
||||
|
||||
|
||||
@ -32,7 +32,6 @@
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"brotli": "1.3.3",
|
||||
"fzstd": "0.1.1",
|
||||
"hysnappy": "0.3.1"
|
||||
},
|
||||
@ -41,7 +40,6 @@
|
||||
"@rollup/plugin-commonjs": "25.0.8",
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@types/brotli": "1.3.4",
|
||||
"@types/node": "20.12.12",
|
||||
"@vitest/coverage-v8": "1.6.0",
|
||||
"eslint": "8.57.0",
|
||||
|
||||
706
src/brotli.js
Normal file
706
src/brotli.js
Normal file
@ -0,0 +1,706 @@
|
||||
/* Adapted from https://github.com/foliojs/brotli.js
|
||||
* Copyright 2015 Devon Govett, MIT License
|
||||
* Copyright 2013 Google Inc, Apache License 2.0
|
||||
*/
|
||||
|
||||
import BrotliBitReader from './brotliBitReader.js'
|
||||
import { lookup, lookupOffsets } from './brotliContext.js'
|
||||
import { HuffmanCode, readHuffmanCode, readSymbol } from './brotliHuffman.js'
|
||||
import { kBlockLengthPrefixCode, kCopyLengthPrefixCode, kCopyRangeLut, kInsertLengthPrefixCode, kInsertRangeLut } from './brotliPrefix.js'
|
||||
import { BrotliInput, BrotliOutput } from './brotliStreams.js'
|
||||
import { kNumTransforms, transformDictionaryWord } from './brotliTransform.js'
|
||||
|
||||
const kNumLiteralCodes = 256
|
||||
const kNumInsertAndCopyCodes = 704
|
||||
const kNumBlockLengthCodes = 26
|
||||
const kLiteralContextBits = 6
|
||||
const kDistanceContextBits = 2
|
||||
|
||||
/* Maximum possible Huffman table size for an alphabet size of 704, max code
|
||||
* length 15 and root table bits 8. */
|
||||
const HUFFMAN_MAX_TABLE_SIZE = 1080
|
||||
|
||||
const NUM_DISTANCE_SHORT_CODES = 16
|
||||
const kDistanceShortCodeIndexOffset = new Uint8Array([
|
||||
3, 2, 1, 0, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2,
|
||||
])
|
||||
|
||||
const kDistanceShortCodeValueOffset = new Int8Array([
|
||||
0, 0, 0, 0, -1, 1, -2, 2, -3, 3, -1, 1, -2, 2, -3, 3,
|
||||
])
|
||||
|
||||
const kMaxHuffmanTableSize = new Uint16Array([
|
||||
256, 402, 436, 468, 500, 534, 566, 598, 630, 662, 694, 726, 758, 790, 822,
|
||||
854, 886, 920, 952, 984, 1016, 1048, 1080,
|
||||
])
|
||||
|
||||
// Brotli dictionary
|
||||
const offsetsByLength = new Uint32Array([
|
||||
0, 0, 0, 0, 0, 4096, 9216, 21504, 35840, 44032,
|
||||
53248, 63488, 74752, 87040, 93696, 100864, 104704, 106752, 108928, 113536,
|
||||
115968, 118528, 119872, 121280, 122016,
|
||||
])
|
||||
|
||||
const sizeBitsByLength = new Uint8Array([
|
||||
0, 0, 0, 0, 10, 10, 11, 11, 10, 10,
|
||||
10, 10, 10, 9, 9, 8, 7, 7, 8, 7,
|
||||
7, 6, 6, 5, 5,
|
||||
])
|
||||
|
||||
const minDictionaryWordLength = 4
|
||||
const maxDictionaryWordLength = 24
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} input
|
||||
* @param {number} outputLength
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function BROTLI(input, outputLength) {
|
||||
const output = new Uint8Array(outputLength)
|
||||
const brotliInput = new BrotliInput(input)
|
||||
const brotliOutput = new BrotliOutput(output)
|
||||
brotli(brotliInput, brotliOutput)
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BrotliInput} input
|
||||
* @param {BrotliOutput} output
|
||||
*/
|
||||
function brotli(input, output) {
|
||||
let pos = 0
|
||||
let input_end = 0
|
||||
let window_bits = 0
|
||||
let max_distance = 0
|
||||
/* This ring buffer holds a few past copy distances that will be used by */
|
||||
/* some special distance codes. */
|
||||
const dist_rb = [ 16, 15, 11, 4 ]
|
||||
let dist_rb_idx = 0
|
||||
/* The previous 2 bytes used for context */
|
||||
let prev_byte1 = 0
|
||||
let prev_byte2 = 0
|
||||
const hgroup = [new HuffmanTreeGroup(0, 0), new HuffmanTreeGroup(0, 0), new HuffmanTreeGroup(0, 0)]
|
||||
|
||||
/* We need the slack region for the following reasons:
|
||||
- always doing two 8-byte copies for fast backward copying
|
||||
- transforms
|
||||
- flushing the input ringbuffer when decoding uncompressed blocks */
|
||||
const kRingBufferWriteAheadSlack = 128 + BrotliBitReader.READ_SIZE
|
||||
|
||||
const br = new BrotliBitReader(input)
|
||||
|
||||
/* Decode window size. */
|
||||
window_bits = decodeWindowBits(br)
|
||||
const max_backward_distance = (1 << window_bits) - 16
|
||||
|
||||
const ringbuffer_size = 1 << window_bits
|
||||
const ringbuffer_mask = ringbuffer_size - 1
|
||||
const ringbuffer = new Uint8Array(ringbuffer_size + kRingBufferWriteAheadSlack + maxDictionaryWordLength)
|
||||
const ringbuffer_end = ringbuffer_size
|
||||
|
||||
const block_type_trees = []
|
||||
const block_len_trees = []
|
||||
for (let x = 0; x < 3 * HUFFMAN_MAX_TABLE_SIZE; x++) {
|
||||
block_type_trees[x] = new HuffmanCode(0, 0)
|
||||
block_len_trees[x] = new HuffmanCode(0, 0)
|
||||
}
|
||||
|
||||
while (!input_end) {
|
||||
let meta_block_remaining_len = 0
|
||||
const block_length = [ 1 << 28, 1 << 28, 1 << 28 ]
|
||||
const block_type = [ 0 ]
|
||||
const num_block_types = [ 1, 1, 1 ]
|
||||
const block_type_rb = [ 0, 1, 0, 1, 0, 1 ]
|
||||
const block_type_rb_index = [ 0 ]
|
||||
let context_offset = 0
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
hgroup[i].codes = []
|
||||
hgroup[i].htrees = new Uint32Array()
|
||||
}
|
||||
|
||||
br.readMoreInput()
|
||||
|
||||
const _out = DecodeMetaBlockLength(br)
|
||||
meta_block_remaining_len = _out.meta_block_length
|
||||
if (pos + meta_block_remaining_len > output.buffer.length) {
|
||||
/* We need to grow the output buffer to fit the additional data. */
|
||||
const tmp = new Uint8Array( pos + meta_block_remaining_len )
|
||||
tmp.set( output.buffer )
|
||||
output.buffer = tmp
|
||||
}
|
||||
input_end = _out.input_end
|
||||
|
||||
if (_out.is_metadata) {
|
||||
jumpToByteBoundary(br)
|
||||
|
||||
for (; meta_block_remaining_len > 0; --meta_block_remaining_len) {
|
||||
br.readMoreInput()
|
||||
/* Read one byte and ignore it. */
|
||||
br.readBits(8)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (meta_block_remaining_len === 0) continue
|
||||
|
||||
if (_out.is_uncompressed) {
|
||||
br.bit_pos_ = br.bit_pos_ + 7 & ~7
|
||||
copyUncompressedBlockToOutput(output, meta_block_remaining_len, pos, ringbuffer, ringbuffer_mask, br)
|
||||
pos += meta_block_remaining_len
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
num_block_types[i] = decodeVarLenUint8(br) + 1
|
||||
if (num_block_types[i] >= 2) {
|
||||
readHuffmanCode(num_block_types[i] + 2, block_type_trees, i * HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
readHuffmanCode(kNumBlockLengthCodes, block_len_trees, i * HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
block_length[i] = readBlockLength(block_len_trees, i * HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
block_type_rb_index[i] = 1
|
||||
}
|
||||
}
|
||||
|
||||
br.readMoreInput()
|
||||
|
||||
const distance_postfix_bits = br.readBits(2)
|
||||
const num_direct_distance_codes = NUM_DISTANCE_SHORT_CODES + (br.readBits(4) << distance_postfix_bits)
|
||||
const distance_postfix_mask = (1 << distance_postfix_bits) - 1
|
||||
const num_distance_codes = num_direct_distance_codes + (48 << distance_postfix_bits)
|
||||
const context_modes = new Uint8Array(num_block_types[0])
|
||||
|
||||
for (let i = 0; i < num_block_types[0]; i++) {
|
||||
br.readMoreInput()
|
||||
context_modes[i] = br.readBits(2) << 1
|
||||
}
|
||||
|
||||
const _o1 = DecodeContextMap(num_block_types[0] << kLiteralContextBits, br)
|
||||
const num_literal_htrees = _o1.num_htrees
|
||||
const { context_map } = _o1
|
||||
|
||||
const _o2 = DecodeContextMap(num_block_types[2] << kDistanceContextBits, br)
|
||||
const num_dist_htrees = _o2.num_htrees
|
||||
const dist_context_map = _o2.context_map
|
||||
|
||||
hgroup[0] = new HuffmanTreeGroup(kNumLiteralCodes, num_literal_htrees)
|
||||
hgroup[1] = new HuffmanTreeGroup(kNumInsertAndCopyCodes, num_block_types[1])
|
||||
hgroup[2] = new HuffmanTreeGroup(num_distance_codes, num_dist_htrees)
|
||||
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
hgroup[i].decode(br)
|
||||
}
|
||||
|
||||
let context_map_slice = 0
|
||||
let dist_context_map_slice = 0
|
||||
let context_mode = context_modes[block_type[0]]
|
||||
let context_lookup_offset1 = lookupOffsets[context_mode]
|
||||
let context_lookup_offset2 = lookupOffsets[context_mode + 1]
|
||||
let htree_command = hgroup[1].htrees[0]
|
||||
|
||||
while (meta_block_remaining_len > 0) {
|
||||
let distance_code
|
||||
|
||||
br.readMoreInput()
|
||||
|
||||
if (block_length[1] === 0) {
|
||||
decodeBlockType(num_block_types[1],
|
||||
block_type_trees, 1, block_type, block_type_rb,
|
||||
block_type_rb_index, br)
|
||||
block_length[1] = readBlockLength(block_len_trees, HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
htree_command = hgroup[1].htrees[block_type[1]]
|
||||
}
|
||||
block_length[1]--
|
||||
const cmd_code = readSymbol(hgroup[1].codes, htree_command, br)
|
||||
let range_idx = cmd_code >> 6
|
||||
if (range_idx >= 2) {
|
||||
range_idx -= 2
|
||||
distance_code = -1
|
||||
} else {
|
||||
distance_code = 0
|
||||
}
|
||||
const insert_code = kInsertRangeLut[range_idx] + (cmd_code >> 3 & 7)
|
||||
const copy_code = kCopyRangeLut[range_idx] + (cmd_code & 7)
|
||||
const insert_length = kInsertLengthPrefixCode[insert_code].offset +
|
||||
br.readBits(kInsertLengthPrefixCode[insert_code].nbits)
|
||||
const copy_length = kCopyLengthPrefixCode[copy_code].offset +
|
||||
br.readBits(kCopyLengthPrefixCode[copy_code].nbits)
|
||||
prev_byte1 = ringbuffer[pos - 1 & ringbuffer_mask]
|
||||
prev_byte2 = ringbuffer[pos - 2 & ringbuffer_mask]
|
||||
for (let j = 0; j < insert_length; j++) {
|
||||
br.readMoreInput()
|
||||
|
||||
if (block_length[0] === 0) {
|
||||
decodeBlockType(num_block_types[0],
|
||||
block_type_trees, 0, block_type, block_type_rb,
|
||||
block_type_rb_index, br)
|
||||
block_length[0] = readBlockLength(block_len_trees, 0, br)
|
||||
context_offset = block_type[0] << kLiteralContextBits
|
||||
context_map_slice = context_offset
|
||||
context_mode = context_modes[block_type[0]]
|
||||
context_lookup_offset1 = lookupOffsets[context_mode]
|
||||
context_lookup_offset2 = lookupOffsets[context_mode + 1]
|
||||
}
|
||||
const context = lookup[context_lookup_offset1 + prev_byte1] |
|
||||
lookup[context_lookup_offset2 + prev_byte2]
|
||||
const literal_htree_index = context_map[context_map_slice + context]
|
||||
block_length[0]--
|
||||
prev_byte2 = prev_byte1
|
||||
prev_byte1 = readSymbol(hgroup[0].codes, hgroup[0].htrees[literal_htree_index], br)
|
||||
ringbuffer[pos & ringbuffer_mask] = prev_byte1
|
||||
if ((pos & ringbuffer_mask) === ringbuffer_mask) {
|
||||
output.write(ringbuffer, ringbuffer_size)
|
||||
}
|
||||
pos++
|
||||
}
|
||||
meta_block_remaining_len -= insert_length
|
||||
if (meta_block_remaining_len <= 0) break
|
||||
|
||||
if (distance_code < 0) {
|
||||
br.readMoreInput()
|
||||
if (block_length[2] === 0) {
|
||||
decodeBlockType(num_block_types[2],
|
||||
block_type_trees, 2, block_type, block_type_rb,
|
||||
block_type_rb_index, br)
|
||||
block_length[2] = readBlockLength(block_len_trees, 2 * HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
dist_context_map_slice = block_type[2] << kDistanceContextBits
|
||||
}
|
||||
block_length[2]--
|
||||
const context = (copy_length > 4 ? 3 : copy_length - 2) & 0xff
|
||||
const dist_htree_index = dist_context_map[dist_context_map_slice + context]
|
||||
distance_code = readSymbol(hgroup[2].codes, hgroup[2].htrees[dist_htree_index], br)
|
||||
if (distance_code >= num_direct_distance_codes) {
|
||||
distance_code -= num_direct_distance_codes
|
||||
const postfix = distance_code & distance_postfix_mask
|
||||
distance_code >>= distance_postfix_bits
|
||||
const nbits = (distance_code >> 1) + 1
|
||||
const offset = (2 + (distance_code & 1) << nbits) - 4
|
||||
distance_code = num_direct_distance_codes +
|
||||
(offset + br.readBits(nbits) <<
|
||||
distance_postfix_bits) + postfix
|
||||
}
|
||||
}
|
||||
|
||||
/* Convert the distance code to the actual distance by possibly looking */
|
||||
/* up past distnaces from the ringbuffer. */
|
||||
const distance = translateShortCodes(distance_code, dist_rb, dist_rb_idx)
|
||||
if (distance < 0) throw new Error('[BrotliDecompress] invalid distance')
|
||||
|
||||
if (pos < max_backward_distance && max_distance !== max_backward_distance) {
|
||||
max_distance = pos
|
||||
} else {
|
||||
max_distance = max_backward_distance
|
||||
}
|
||||
|
||||
let copy_dst = pos & ringbuffer_mask
|
||||
|
||||
if (distance > max_distance) {
|
||||
if (copy_length >= minDictionaryWordLength && copy_length <= maxDictionaryWordLength) {
|
||||
let offset = offsetsByLength[copy_length]
|
||||
const word_id = distance - max_distance - 1
|
||||
const shift = sizeBitsByLength[copy_length]
|
||||
const mask = (1 << shift) - 1
|
||||
const word_idx = word_id & mask
|
||||
const transform_idx = word_id >> shift
|
||||
offset += word_idx * copy_length
|
||||
if (transform_idx < kNumTransforms) {
|
||||
const len = transformDictionaryWord(ringbuffer, copy_dst, offset, copy_length, transform_idx)
|
||||
copy_dst += len
|
||||
pos += len
|
||||
meta_block_remaining_len -= len
|
||||
if (copy_dst >= ringbuffer_end) {
|
||||
output.write(ringbuffer, ringbuffer_size)
|
||||
|
||||
for (let _x = 0; _x < copy_dst - ringbuffer_end; _x++)
|
||||
ringbuffer[_x] = ringbuffer[ringbuffer_end + _x]
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid backward reference')
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid backward reference')
|
||||
}
|
||||
} else {
|
||||
if (distance_code > 0) {
|
||||
dist_rb[dist_rb_idx & 3] = distance
|
||||
dist_rb_idx++
|
||||
}
|
||||
|
||||
if (copy_length > meta_block_remaining_len) {
|
||||
throw new Error('Invalid backward reference')
|
||||
}
|
||||
|
||||
for (let j = 0; j < copy_length; j++) {
|
||||
ringbuffer[pos & ringbuffer_mask] = ringbuffer[pos - distance & ringbuffer_mask]
|
||||
if ((pos & ringbuffer_mask) === ringbuffer_mask) {
|
||||
output.write(ringbuffer, ringbuffer_size)
|
||||
}
|
||||
pos++
|
||||
meta_block_remaining_len--
|
||||
}
|
||||
}
|
||||
|
||||
/* When we get here, we must have inserted at least one literal and */
|
||||
/* made a copy of at least length two, therefore accessing the last 2 */
|
||||
/* bytes is valid. */
|
||||
prev_byte1 = ringbuffer[pos - 1 & ringbuffer_mask]
|
||||
prev_byte2 = ringbuffer[pos - 2 & ringbuffer_mask]
|
||||
}
|
||||
|
||||
/* Protect pos from overflow, wrap it around at every GB of input data */
|
||||
pos &= 0x3fffffff
|
||||
}
|
||||
|
||||
output.write(ringbuffer, pos & ringbuffer_mask)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {number}
|
||||
*/
|
||||
function decodeWindowBits(br) {
|
||||
if (br.readBits(1) === 0) return 16
|
||||
|
||||
let n = br.readBits(3)
|
||||
if (n > 0) return 17 + n
|
||||
|
||||
n = br.readBits(3)
|
||||
if (n > 0) return 8 + n
|
||||
|
||||
return 17
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} max_block_type
|
||||
* @param {HuffmanCode[]} trees
|
||||
* @param {number} tree_type
|
||||
* @param {number[]} block_types
|
||||
* @param {number[]} ringbuffers
|
||||
* @param {number[]} indexes
|
||||
* @param {BrotliBitReader} br
|
||||
*/
|
||||
function decodeBlockType(max_block_type, trees, tree_type, block_types, ringbuffers, indexes, br) {
|
||||
const ringbuffer = tree_type * 2
|
||||
const index = tree_type
|
||||
const type_code = readSymbol(trees, tree_type * HUFFMAN_MAX_TABLE_SIZE, br)
|
||||
let block_type
|
||||
if (type_code === 0) {
|
||||
block_type = ringbuffers[ringbuffer + (indexes[index] & 1)]
|
||||
} else if (type_code === 1) {
|
||||
block_type = ringbuffers[ringbuffer + (indexes[index] - 1 & 1)] + 1
|
||||
} else {
|
||||
block_type = type_code - 2
|
||||
}
|
||||
if (block_type >= max_block_type) {
|
||||
block_type -= max_block_type
|
||||
}
|
||||
block_types[tree_type] = block_type
|
||||
ringbuffers[ringbuffer + (indexes[index] & 1)] = block_type
|
||||
++indexes[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains a collection of huffman trees with the same alphabet size.
|
||||
*
|
||||
* @param {number} alphabet_size
|
||||
* @param {number} num_htrees
|
||||
*/
|
||||
function HuffmanTreeGroup(alphabet_size, num_htrees) {
|
||||
this.alphabet_size = alphabet_size
|
||||
this.num_htrees = num_htrees
|
||||
this.codes = new Array(num_htrees + num_htrees * kMaxHuffmanTableSize[alphabet_size + 31 >>> 5])
|
||||
this.htrees = new Uint32Array(num_htrees)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BrotliBitReader} br
|
||||
*/
|
||||
HuffmanTreeGroup.prototype.decode = function(br) {
|
||||
let next = 0
|
||||
for (let i = 0; i < this.num_htrees; i++) {
|
||||
this.htrees[i] = next
|
||||
next += readHuffmanCode(this.alphabet_size, this.codes, next, br)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HuffmanCode[]} table
|
||||
* @param {number} index
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {number}
|
||||
*/
|
||||
function readBlockLength(table, index, br) {
|
||||
const code = readSymbol(table, index, br)
|
||||
const { nbits } = kBlockLengthPrefixCode[code]
|
||||
return kBlockLengthPrefixCode[code].offset + br.readBits(nbits)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} code
|
||||
* @param {number[]} ringbuffer
|
||||
* @param {number} index
|
||||
* @returns {number}
|
||||
*/
|
||||
function translateShortCodes(code, ringbuffer, index) {
|
||||
if (code < NUM_DISTANCE_SHORT_CODES) {
|
||||
index += kDistanceShortCodeIndexOffset[code]
|
||||
index &= 3
|
||||
return ringbuffer[index] + kDistanceShortCodeValueOffset[code]
|
||||
} else {
|
||||
return code - NUM_DISTANCE_SHORT_CODES + 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} output
|
||||
* @param {number} len
|
||||
* @param {number} pos
|
||||
* @param {Uint8Array} ringbuffer
|
||||
* @param {number} ringbuffer_mask
|
||||
* @param {BrotliBitReader} br
|
||||
*/
|
||||
function copyUncompressedBlockToOutput(output, len, pos, ringbuffer, ringbuffer_mask, br) {
|
||||
const rb_size = ringbuffer_mask + 1
|
||||
let rb_pos = pos & ringbuffer_mask
|
||||
let br_pos = br.pos_ & BrotliBitReader.IBUF_MASK
|
||||
|
||||
/* For short lengths copy byte-by-byte */
|
||||
if (len < 8 || br.bit_pos_ + (len << 3) < br.bit_end_pos_) {
|
||||
while (len-- > 0) {
|
||||
br.readMoreInput()
|
||||
ringbuffer[rb_pos++] = br.readBits(8)
|
||||
if (rb_pos === rb_size) {
|
||||
output.write(ringbuffer, rb_size)
|
||||
rb_pos = 0
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (br.bit_end_pos_ < 32) {
|
||||
throw new Error('[CopyUncompressedBlockToOutput] br.bit_end_pos_ < 32')
|
||||
}
|
||||
|
||||
/* Copy remaining 0-4 bytes from br.val_ to ringbuffer. */
|
||||
while (br.bit_pos_ < 32) {
|
||||
ringbuffer[rb_pos] = br.val_ >>> br.bit_pos_
|
||||
br.bit_pos_ += 8
|
||||
rb_pos++
|
||||
len--
|
||||
}
|
||||
|
||||
/* Copy remaining bytes from br.buf_ to ringbuffer. */
|
||||
let nbytes = br.bit_end_pos_ - br.bit_pos_ >> 3
|
||||
if (br_pos + nbytes > BrotliBitReader.IBUF_MASK) {
|
||||
const tail = BrotliBitReader.IBUF_MASK + 1 - br_pos
|
||||
for (let x = 0; x < tail; x++)
|
||||
ringbuffer[rb_pos + x] = br.buf_[br_pos + x]
|
||||
|
||||
nbytes -= tail
|
||||
rb_pos += tail
|
||||
len -= tail
|
||||
br_pos = 0
|
||||
}
|
||||
|
||||
for (let x = 0; x < nbytes; x++)
|
||||
ringbuffer[rb_pos + x] = br.buf_[br_pos + x]
|
||||
|
||||
rb_pos += nbytes
|
||||
len -= nbytes
|
||||
|
||||
/* If we wrote past the logical end of the ringbuffer, copy the tail of the
|
||||
ringbuffer to its beginning and flush the ringbuffer to the output. */
|
||||
if (rb_pos >= rb_size) {
|
||||
output.write(ringbuffer, rb_size)
|
||||
rb_pos -= rb_size
|
||||
for (let x = 0; x < rb_pos; x++)
|
||||
ringbuffer[x] = ringbuffer[rb_size + x]
|
||||
}
|
||||
|
||||
/* If we have more to copy than the remaining size of the ringbuffer, then we
|
||||
first fill the ringbuffer from the input and then flush the ringbuffer to
|
||||
the output */
|
||||
while (rb_pos + len >= rb_size) {
|
||||
nbytes = rb_size - rb_pos
|
||||
if (br.input_.read(ringbuffer, rb_pos, nbytes) < nbytes) {
|
||||
throw new Error('[CopyUncompressedBlockToOutput] not enough bytes')
|
||||
}
|
||||
output.write(ringbuffer, rb_size)
|
||||
len -= nbytes
|
||||
rb_pos = 0
|
||||
}
|
||||
|
||||
/* Copy straight from the input onto the ringbuffer. The ringbuffer will be
|
||||
flushed to the output at a later time. */
|
||||
if (br.input_.read(ringbuffer, rb_pos, len) < len) {
|
||||
throw new Error('[CopyUncompressedBlockToOutput] not enough bytes')
|
||||
}
|
||||
|
||||
/* Restore the state of the bit reader. */
|
||||
br.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a number in the range [0..255], by reading 1 - 11 bits.
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {number}
|
||||
*/
|
||||
function decodeVarLenUint8(br) {
|
||||
if (br.readBits(1)) {
|
||||
const nbits = br.readBits(3)
|
||||
if (nbits === 0) {
|
||||
return 1
|
||||
} else {
|
||||
return br.readBits(nbits) + (1 << nbits)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
function MetaBlockLength() {
|
||||
this.meta_block_length = 0
|
||||
this.input_end = 0
|
||||
this.is_uncompressed = 0
|
||||
this.is_metadata = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {MetaBlockLength}
|
||||
*/
|
||||
function DecodeMetaBlockLength(br) {
|
||||
const out = new MetaBlockLength
|
||||
|
||||
out.input_end = br.readBits(1)
|
||||
if (out.input_end && br.readBits(1)) {
|
||||
return out
|
||||
}
|
||||
|
||||
const size_nibbles = br.readBits(2) + 4
|
||||
if (size_nibbles === 7) {
|
||||
out.is_metadata = true
|
||||
|
||||
if (br.readBits(1) !== 0)
|
||||
throw new Error('Invalid reserved bit')
|
||||
|
||||
const size_bytes = br.readBits(2)
|
||||
if (size_bytes === 0)
|
||||
return out
|
||||
|
||||
for (let i = 0; i < size_bytes; i++) {
|
||||
const next_byte = br.readBits(8)
|
||||
if (i + 1 === size_bytes && size_bytes > 1 && next_byte === 0)
|
||||
throw new Error('Invalid size byte')
|
||||
|
||||
out.meta_block_length |= next_byte << i * 8
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < size_nibbles; i++) {
|
||||
const next_nibble = br.readBits(4)
|
||||
if (i + 1 === size_nibbles && size_nibbles > 4 && next_nibble === 0)
|
||||
throw new Error('Invalid size nibble')
|
||||
|
||||
out.meta_block_length |= next_nibble << i * 4
|
||||
}
|
||||
}
|
||||
|
||||
out.meta_block_length++
|
||||
|
||||
if (!out.input_end && !out.is_metadata) {
|
||||
out.is_uncompressed = br.readBits(1)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} context_map_size
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {{ num_htrees: number, context_map: Uint8Array }}
|
||||
*/
|
||||
function DecodeContextMap(context_map_size, br) {
|
||||
let max_run_length_prefix = 0
|
||||
|
||||
br.readMoreInput()
|
||||
const num_htrees = decodeVarLenUint8(br) + 1
|
||||
|
||||
const context_map = new Uint8Array(context_map_size)
|
||||
if (num_htrees <= 1) {
|
||||
return { num_htrees, context_map }
|
||||
}
|
||||
|
||||
const use_rle_for_zeros = br.readBits(1)
|
||||
if (use_rle_for_zeros) {
|
||||
max_run_length_prefix = br.readBits(4) + 1
|
||||
}
|
||||
|
||||
const table = []
|
||||
for (let i = 0; i < HUFFMAN_MAX_TABLE_SIZE; i++) {
|
||||
table[i] = new HuffmanCode(0, 0)
|
||||
}
|
||||
|
||||
readHuffmanCode(num_htrees + max_run_length_prefix, table, 0, br)
|
||||
|
||||
for (let i = 0; i < context_map_size;) {
|
||||
br.readMoreInput()
|
||||
const code = readSymbol(table, 0, br)
|
||||
if (code === 0) {
|
||||
context_map[i] = 0
|
||||
i++
|
||||
} else if (code <= max_run_length_prefix) {
|
||||
let reps = 1 + (1 << code) + br.readBits(code)
|
||||
while (--reps) {
|
||||
if (i >= context_map_size) {
|
||||
throw new Error('[DecodeContextMap] i >= context_map_size')
|
||||
}
|
||||
context_map[i] = 0
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
context_map[i] = code - max_run_length_prefix
|
||||
i++
|
||||
}
|
||||
}
|
||||
if (br.readBits(1)) {
|
||||
inverseMoveToFrontTransform(context_map, context_map_size)
|
||||
}
|
||||
|
||||
return { num_htrees, context_map }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} v
|
||||
* @param {number} index
|
||||
*/
|
||||
function moveToFront(v, index) {
|
||||
const value = v[index]
|
||||
for (let i = index; i; i--) v[i] = v[i - 1]
|
||||
v[0] = value
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} v
|
||||
* @param {number} v_len
|
||||
*/
|
||||
function inverseMoveToFrontTransform(v, v_len) {
|
||||
const mtf = new Uint8Array(256)
|
||||
for (let i = 0; i < 256; i++) {
|
||||
mtf[i] = i
|
||||
}
|
||||
for (let i = 0; i < v_len; i++) {
|
||||
const index = v[i]
|
||||
v[i] = mtf[index]
|
||||
if (index) moveToFront(mtf, index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Advances the bit reader position to the next byte boundary and verifies
|
||||
* that any skipped bits are set to zero.
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function jumpToByteBoundary(br) {
|
||||
const new_bit_pos = br.bit_pos_ + 7 & ~7
|
||||
return !br.readBits(new_bit_pos - br.bit_pos_)
|
||||
}
|
||||
136
src/brotliBitReader.js
Normal file
136
src/brotliBitReader.js
Normal file
@ -0,0 +1,136 @@
|
||||
/* Copyright 2013 Google Inc. 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.
|
||||
|
||||
Bit reading helpers
|
||||
*/
|
||||
|
||||
const BROTLI_READ_SIZE = 4096
|
||||
const BROTLI_IBUF_SIZE = 2 * BROTLI_READ_SIZE + 32
|
||||
const BROTLI_IBUF_MASK = 2 * BROTLI_READ_SIZE - 1
|
||||
|
||||
const kBitMask = new Uint32Array([
|
||||
0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767,
|
||||
65535, 131071, 262143, 524287, 1048575, 2097151, 4194303, 8388607, 16777215,
|
||||
])
|
||||
|
||||
/**
|
||||
* Input byte buffer, consist of a ringbuffer and a "slack" region where
|
||||
* bytes from the start of the ringbuffer are copied.
|
||||
*
|
||||
* @typedef {import('./brotliStreams.js').BrotliInput} BrotliInput
|
||||
* @param {BrotliInput} input
|
||||
*/
|
||||
function BrotliBitReader(input) {
|
||||
this.buf_ = new Uint8Array(BROTLI_IBUF_SIZE)
|
||||
this.input_ = input /* input callback */
|
||||
|
||||
this.buf_ptr_ = 0 /* next input will write here */
|
||||
this.val_ = 0 /* pre-fetched bits */
|
||||
this.pos_ = 0 /* byte position in stream */
|
||||
|
||||
this.reset()
|
||||
}
|
||||
|
||||
BrotliBitReader.READ_SIZE = BROTLI_READ_SIZE
|
||||
BrotliBitReader.IBUF_MASK = BROTLI_IBUF_MASK
|
||||
|
||||
BrotliBitReader.prototype.reset = function() {
|
||||
this.buf_ptr_ = 0 /* next input will write here */
|
||||
this.val_ = 0 /* pre-fetched bits */
|
||||
this.pos_ = 0 /* byte position in stream */
|
||||
this.bit_pos_ = 0 /* current bit-reading position in val_ */
|
||||
this.bit_end_pos_ = 0 /* bit-reading end position from LSB of val_ */
|
||||
this.eos_ = 0 /* input stream is finished */
|
||||
|
||||
this.readMoreInput()
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.val_ |= this.buf_[this.pos_] << 8 * i
|
||||
this.pos_++
|
||||
}
|
||||
|
||||
return this.bit_end_pos_ > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills up the input ringbuffer by calling the input callback.
|
||||
*
|
||||
* Does nothing if there are at least 32 bytes present after current position.
|
||||
*
|
||||
* Returns 0 if either:
|
||||
* - the input callback returned an error, or
|
||||
* - there is no more input and the position is past the end of the stream.
|
||||
*
|
||||
* After encountering the end of the input stream, 32 additional zero bytes are
|
||||
* copied to the ringbuffer, therefore it is safe to call this function after
|
||||
* every 32 bytes of input is read.
|
||||
*/
|
||||
BrotliBitReader.prototype.readMoreInput = function() {
|
||||
if (this.bit_end_pos_ > 256) {
|
||||
// return
|
||||
} else if (this.eos_) {
|
||||
if (this.bit_pos_ > this.bit_end_pos_)
|
||||
throw new Error('Unexpected end of input ' + this.bit_pos_ + ' ' + this.bit_end_pos_)
|
||||
} else {
|
||||
const dst = this.buf_ptr_
|
||||
const bytes_read = this.input_.read(this.buf_, dst, BROTLI_READ_SIZE)
|
||||
if (bytes_read < 0) {
|
||||
throw new Error('Unexpected end of input')
|
||||
}
|
||||
|
||||
if (bytes_read < BROTLI_READ_SIZE) {
|
||||
this.eos_ = 1
|
||||
/* Store 32 bytes of zero after the stream end. */
|
||||
for (let p = 0; p < 32; p++)
|
||||
this.buf_[dst + bytes_read + p] = 0
|
||||
}
|
||||
|
||||
if (dst === 0) {
|
||||
/* Copy the head of the ringbuffer to the slack region. */
|
||||
for (let p = 0; p < 32; p++)
|
||||
this.buf_[(BROTLI_READ_SIZE << 1) + p] = this.buf_[p]
|
||||
|
||||
this.buf_ptr_ = BROTLI_READ_SIZE
|
||||
} else {
|
||||
this.buf_ptr_ = 0
|
||||
}
|
||||
|
||||
this.bit_end_pos_ += bytes_read << 3
|
||||
}
|
||||
}
|
||||
|
||||
/* Guarantees that there are at least 24 bits in the buffer. */
|
||||
BrotliBitReader.prototype.fillBitWindow = function() {
|
||||
while (this.bit_pos_ >= 8) {
|
||||
this.val_ >>>= 8
|
||||
this.val_ |= this.buf_[this.pos_ & BROTLI_IBUF_MASK] << 24
|
||||
this.pos_++
|
||||
this.bit_pos_ = this.bit_pos_ - 8 >>> 0
|
||||
this.bit_end_pos_ = this.bit_end_pos_ - 8 >>> 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the specified number of bits from Read Buffer.
|
||||
*
|
||||
* @param {number} n_bits
|
||||
* @returns {number}
|
||||
*/
|
||||
BrotliBitReader.prototype.readBits = function(n_bits) {
|
||||
if (32 - this.bit_pos_ < n_bits) this.fillBitWindow()
|
||||
const val = this.val_ >>> this.bit_pos_ & kBitMask[n_bits]
|
||||
this.bit_pos_ += n_bits
|
||||
return val
|
||||
}
|
||||
|
||||
export default BrotliBitReader
|
||||
245
src/brotliContext.js
Normal file
245
src/brotliContext.js
Normal file
@ -0,0 +1,245 @@
|
||||
/* Copyright 2013 Google Inc. 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.
|
||||
|
||||
Lookup table to map the previous two bytes to a context id.
|
||||
|
||||
There are four different context modeling modes defined here:
|
||||
CONTEXT_LSB6: context id is the least significant 6 bits of the last byte,
|
||||
CONTEXT_MSB6: context id is the most significant 6 bits of the last byte,
|
||||
CONTEXT_UTF8: second-order context model tuned for UTF8-encoded text,
|
||||
CONTEXT_SIGNED: second-order context model tuned for signed integers.
|
||||
|
||||
The context id for the UTF8 context model is calculated as follows. If p1
|
||||
and p2 are the previous two bytes, we calcualte the context as
|
||||
|
||||
context = kContextLookup[p1] | kContextLookup[p2 + 256].
|
||||
|
||||
If the previous two bytes are ASCII characters (i.e. < 128), this will be
|
||||
equivalent to
|
||||
|
||||
context = 4 * context1(p1) + context2(p2),
|
||||
|
||||
where context1 is based on the previous byte in the following way:
|
||||
|
||||
0 : non-ASCII control
|
||||
1 : \t, \n, \r
|
||||
2 : space
|
||||
3 : other punctuation
|
||||
4 : " '
|
||||
5 : %
|
||||
6 : ( < [ {
|
||||
7 : ) > ] }
|
||||
8 : , ; :
|
||||
9 : .
|
||||
10 : =
|
||||
11 : number
|
||||
12 : upper-case vowel
|
||||
13 : upper-case consonant
|
||||
14 : lower-case vowel
|
||||
15 : lower-case consonant
|
||||
|
||||
and context2 is based on the second last byte:
|
||||
|
||||
0 : control, space
|
||||
1 : punctuation
|
||||
2 : upper-case letter, number
|
||||
3 : lower-case letter
|
||||
|
||||
If the last byte is ASCII, and the second last byte is not (in a valid UTF8
|
||||
stream it will be a continuation byte, value between 128 and 191), the
|
||||
context is the same as if the second last byte was an ASCII control or space.
|
||||
|
||||
If the last byte is a UTF8 lead byte (value >= 192), then the next byte will
|
||||
be a continuation byte and the context id is 2 or 3 depending on the LSB of
|
||||
the last byte and to a lesser extent on the second last byte if it is ASCII.
|
||||
|
||||
If the last byte is a UTF8 continuation byte, the second last byte can be:
|
||||
- continuation byte: the next byte is probably ASCII or lead byte (assuming
|
||||
4-byte UTF8 characters are rare) and the context id is 0 or 1.
|
||||
- lead byte (192 - 207): next byte is ASCII or lead byte, context is 0 or 1
|
||||
- lead byte (208 - 255): next byte is continuation byte, context is 2 or 3
|
||||
|
||||
The possible value combinations of the previous two bytes, the range of
|
||||
context ids and the type of the next byte is summarized in the table below:
|
||||
|
||||
|--------\-----------------------------------------------------------------|
|
||||
| \ Last byte |
|
||||
| Second \---------------------------------------------------------------|
|
||||
| last byte \ ASCII | cont. byte | lead byte |
|
||||
| \ (0-127) | (128-191) | (192-) |
|
||||
|=============|===================|=====================|==================|
|
||||
| ASCII | next: ASCII/lead | not valid | next: cont. |
|
||||
| (0-127) | context: 4 - 63 | | context: 2 - 3 |
|
||||
|-------------|-------------------|---------------------|------------------|
|
||||
| cont. byte | next: ASCII/lead | next: ASCII/lead | next: cont. |
|
||||
| (128-191) | context: 4 - 63 | context: 0 - 1 | context: 2 - 3 |
|
||||
|-------------|-------------------|---------------------|------------------|
|
||||
| lead byte | not valid | next: ASCII/lead | not valid |
|
||||
| (192-207) | | context: 0 - 1 | |
|
||||
|-------------|-------------------|---------------------|------------------|
|
||||
| lead byte | not valid | next: cont. | not valid |
|
||||
| (208-) | | context: 2 - 3 | |
|
||||
|-------------|-------------------|---------------------|------------------|
|
||||
|
||||
The context id for the signed context mode is calculated as:
|
||||
|
||||
context = (kContextLookup[512 + p1] << 3) | kContextLookup[512 + p2].
|
||||
|
||||
For any context modeling modes, the context ids can be calculated by |-ing
|
||||
together two lookups from one table using context model dependent offsets:
|
||||
|
||||
context = kContextLookup[offset1 + p1] | kContextLookup[offset2 + p2].
|
||||
|
||||
where offset1 and offset2 are dependent on the context mode.
|
||||
*/
|
||||
|
||||
/* Common context lookup table for all context modes. */
|
||||
export const lookup = new Uint8Array([
|
||||
/* CONTEXT_UTF8, last byte. */
|
||||
/* ASCII range. */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 4, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
8, 12, 16, 12, 12, 20, 12, 16, 24, 28, 12, 12, 32, 12, 36, 12,
|
||||
44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 32, 32, 24, 40, 28, 12,
|
||||
12, 48, 52, 52, 52, 48, 52, 52, 52, 48, 52, 52, 52, 52, 52, 48,
|
||||
52, 52, 52, 52, 52, 48, 52, 52, 52, 52, 52, 24, 12, 28, 12, 12,
|
||||
12, 56, 60, 60, 60, 56, 60, 60, 60, 56, 60, 60, 60, 60, 60, 56,
|
||||
60, 60, 60, 60, 60, 56, 60, 60, 60, 60, 60, 24, 12, 28, 12, 0,
|
||||
/* UTF8 continuation byte range. */
|
||||
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
|
||||
0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
|
||||
/* UTF8 lead byte range. */
|
||||
2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3,
|
||||
2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3,
|
||||
2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3,
|
||||
2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3,
|
||||
/* CONTEXT_UTF8 second last byte. */
|
||||
/* ASCII range. */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1,
|
||||
1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1,
|
||||
1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 0,
|
||||
/* UTF8 continuation byte range. */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
/* UTF8 lead byte range. */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
/* CONTEXT_SIGNED, second last byte. */
|
||||
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,
|
||||
6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7,
|
||||
/* CONTEXT_SIGNED, last byte, same as the above values shifted by 3 bits. */
|
||||
0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
|
||||
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
|
||||
40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
|
||||
40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
|
||||
40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40,
|
||||
48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 56,
|
||||
/* CONTEXT_LSB6, last byte. */
|
||||
0, 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,
|
||||
0, 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,
|
||||
0, 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,
|
||||
0, 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,
|
||||
/* CONTEXT_MSB6, last byte. */
|
||||
0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
|
||||
4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7,
|
||||
8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11,
|
||||
12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15,
|
||||
16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19,
|
||||
20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23,
|
||||
24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27,
|
||||
28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31,
|
||||
32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35,
|
||||
36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 39, 39,
|
||||
40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 43,
|
||||
44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47,
|
||||
48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51,
|
||||
52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55,
|
||||
56, 56, 56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59,
|
||||
60, 60, 60, 60, 61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63,
|
||||
/* CONTEXT_{M,L}SB6, second last byte, */
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
])
|
||||
|
||||
export const lookupOffsets = new Uint16Array([
|
||||
/* CONTEXT_LSB6 */
|
||||
1024, 1536,
|
||||
/* CONTEXT_MSB6 */
|
||||
1280, 1536,
|
||||
/* CONTEXT_UTF8 */
|
||||
0, 256,
|
||||
/* CONTEXT_SIGNED */
|
||||
768, 512,
|
||||
])
|
||||
1006
src/brotliDictionary.js
Normal file
1006
src/brotliDictionary.js
Normal file
File diff suppressed because it is too large
Load Diff
349
src/brotliHuffman.js
Normal file
349
src/brotliHuffman.js
Normal file
@ -0,0 +1,349 @@
|
||||
|
||||
const kDefaultCodeLength = 8
|
||||
|
||||
const HUFFMAN_TABLE_BITS = 8
|
||||
const HUFFMAN_TABLE_MASK = 0xff
|
||||
|
||||
const CODE_LENGTH_CODES = 18
|
||||
const kCodeLengthCodeOrder = new Uint8Array([
|
||||
1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {number} bits
|
||||
* @param {number} value
|
||||
*/
|
||||
export function HuffmanCode(bits, value) {
|
||||
this.bits = bits /* number of bits used for this symbol */
|
||||
this.value = value /* symbol value or table offset */
|
||||
}
|
||||
|
||||
const kCodeLengthRepeatCode = 16
|
||||
const MAX_LENGTH = 15
|
||||
|
||||
/**
|
||||
* Returns reverse(reverse(key, len) + 1, len), where reverse(key, len) is the
|
||||
* bit-wise reversal of the len least significant bits of key.
|
||||
* @param {number} key
|
||||
* @param {number} len
|
||||
* @returns {number}
|
||||
*/
|
||||
function getNextKey(key, len) {
|
||||
let step = 1 << len - 1
|
||||
while (key & step) {
|
||||
step >>= 1
|
||||
}
|
||||
return (key & step - 1) + step
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores code in table[0], table[step], table[2*step], ..., table[end]
|
||||
* Assumes that end is an integer multiple of step
|
||||
* @param {HuffmanCode[]} table
|
||||
* @param {number} i
|
||||
* @param {number} step
|
||||
* @param {number} end
|
||||
* @param {HuffmanCode} code
|
||||
*/
|
||||
function replicateValue(table, i, step, end, code) {
|
||||
do {
|
||||
end -= step
|
||||
table[i + end] = new HuffmanCode(code.bits, code.value)
|
||||
} while (end > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table width of the next 2nd level table. count is the histogram
|
||||
* of bit lengths for the remaining symbols, len is the code length of the next
|
||||
* processed symbol
|
||||
* @param {Int32Array} count
|
||||
* @param {number} len
|
||||
* @param {number} root_bits
|
||||
* @returns {number}
|
||||
*/
|
||||
function nextTableBitSize(count, len, root_bits) {
|
||||
let left = 1 << len - root_bits
|
||||
while (len < MAX_LENGTH) {
|
||||
left -= count[len]
|
||||
if (left <= 0) break
|
||||
++len
|
||||
left <<= 1
|
||||
}
|
||||
return len - root_bits
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HuffmanCode[]} root_table
|
||||
* @param {number} table
|
||||
* @param {number} root_bits
|
||||
* @param {Uint8Array} code_lengths
|
||||
* @param {number} code_lengths_size
|
||||
* @returns {number}
|
||||
*/
|
||||
export function buildHuffmanTable(root_table, table, root_bits, code_lengths, code_lengths_size) {
|
||||
const start_table = table
|
||||
const count = new Int32Array(MAX_LENGTH + 1) /* number of codes of each length */
|
||||
const offset = new Int32Array(MAX_LENGTH + 1) /* offsets in sorted table for each length */
|
||||
const sorted = new Int32Array(code_lengths_size) /* symbols sorted by code length */
|
||||
|
||||
/* build histogram of code lengths */
|
||||
for (let i = 0; i < code_lengths_size; i++) {
|
||||
count[code_lengths[i]]++
|
||||
}
|
||||
|
||||
/* generate offsets into sorted symbol table by code length */
|
||||
offset[1] = 0
|
||||
for (let i = 1; i < MAX_LENGTH; i++) {
|
||||
offset[i + 1] = offset[i] + count[i]
|
||||
}
|
||||
|
||||
/* sort symbols by length, by symbol order within each length */
|
||||
for (let i = 0; i < code_lengths_size; i++) {
|
||||
if (code_lengths[i] !== 0) {
|
||||
sorted[offset[code_lengths[i]]++] = i
|
||||
}
|
||||
}
|
||||
|
||||
let table_bits = root_bits // key length of current table
|
||||
let table_size = 1 << table_bits
|
||||
let total_size = table_size // sum of root table size and 2nd level table sizes
|
||||
|
||||
/* special case code with only one value */
|
||||
if (offset[MAX_LENGTH] === 1) {
|
||||
for (let key = 0; key < total_size; ++key) {
|
||||
root_table[table + key] = new HuffmanCode(0, sorted[0] & 0xffff)
|
||||
}
|
||||
|
||||
return total_size
|
||||
}
|
||||
|
||||
/* fill in root table */
|
||||
let key = 0 // reversed prefix code
|
||||
let symbol = 0 // symbol index in original or sorted table
|
||||
for (let len = 1, step = 2; len <= root_bits; ++len, step <<= 1) {
|
||||
for (; count[len] > 0; --count[len]) {
|
||||
const code = new HuffmanCode(len & 0xff, sorted[symbol++] & 0xffff)
|
||||
replicateValue(root_table, table + key, step, table_size, code)
|
||||
key = getNextKey(key, len)
|
||||
}
|
||||
}
|
||||
|
||||
/* fill in 2nd level tables and add pointers to root table */
|
||||
const mask = total_size - 1
|
||||
let low = -1 // low bits for current root entry
|
||||
for (let len = root_bits + 1, step = 2; len <= MAX_LENGTH; ++len, step <<= 1) {
|
||||
for (; count[len] > 0; --count[len]) {
|
||||
if ((key & mask) !== low) {
|
||||
table += table_size
|
||||
table_bits = nextTableBitSize(count, len, root_bits)
|
||||
table_size = 1 << table_bits
|
||||
total_size += table_size
|
||||
low = key & mask
|
||||
root_table[start_table + low] = new HuffmanCode(table_bits + root_bits & 0xff, table - start_table - low & 0xffff)
|
||||
}
|
||||
const code = new HuffmanCode(len - root_bits & 0xff, sorted[symbol++] & 0xffff)
|
||||
replicateValue(root_table, table + (key >> root_bits), step, table_size, code)
|
||||
key = getNextKey(key, len)
|
||||
}
|
||||
}
|
||||
|
||||
return total_size
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./brotliBitReader.js').default} BrotliBitReader
|
||||
* @param {number} alphabet_size
|
||||
* @param {HuffmanCode[]} tables
|
||||
* @param {number} table
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {number}
|
||||
*/
|
||||
export function readHuffmanCode(alphabet_size, tables, table, br) {
|
||||
const code_lengths = new Uint8Array(alphabet_size)
|
||||
|
||||
br.readMoreInput()
|
||||
|
||||
/* simple_code_or_skip is used as follows:
|
||||
1 for simple code;
|
||||
0 for no skipping, 2 skips 2 code lengths, 3 skips 3 code lengths */
|
||||
const simple_code_or_skip = br.readBits(2)
|
||||
if (simple_code_or_skip === 1) {
|
||||
/* Read symbols, codes & code lengths directly. */
|
||||
let max_bits_counter = alphabet_size - 1
|
||||
let max_bits = 0
|
||||
const symbols = new Int32Array(4)
|
||||
const num_symbols = br.readBits(2) + 1
|
||||
while (max_bits_counter) {
|
||||
max_bits_counter >>= 1
|
||||
max_bits++
|
||||
}
|
||||
|
||||
for (let i = 0; i < num_symbols; i++) {
|
||||
symbols[i] = br.readBits(max_bits) % alphabet_size
|
||||
code_lengths[symbols[i]] = 2
|
||||
}
|
||||
code_lengths[symbols[0]] = 1
|
||||
switch (num_symbols) {
|
||||
case 1:
|
||||
break
|
||||
case 3:
|
||||
if (symbols[0] === symbols[1] ||
|
||||
symbols[0] === symbols[2] ||
|
||||
symbols[1] === symbols[2]) {
|
||||
throw new Error('[ReadHuffmanCode] invalid symbols')
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
if (symbols[0] === symbols[1]) {
|
||||
throw new Error('[ReadHuffmanCode] invalid symbols')
|
||||
}
|
||||
|
||||
code_lengths[symbols[1]] = 1
|
||||
break
|
||||
case 4:
|
||||
if (symbols[0] === symbols[1] ||
|
||||
symbols[0] === symbols[2] ||
|
||||
symbols[0] === symbols[3] ||
|
||||
symbols[1] === symbols[2] ||
|
||||
symbols[1] === symbols[3] ||
|
||||
symbols[2] === symbols[3]) {
|
||||
throw new Error('[ReadHuffmanCode] invalid symbols')
|
||||
}
|
||||
|
||||
if (br.readBits(1)) {
|
||||
code_lengths[symbols[2]] = 3
|
||||
code_lengths[symbols[3]] = 3
|
||||
} else {
|
||||
code_lengths[symbols[0]] = 2
|
||||
}
|
||||
break
|
||||
}
|
||||
} else { /* Decode Huffman-coded code lengths. */
|
||||
const code_length_code_lengths = new Uint8Array(CODE_LENGTH_CODES)
|
||||
let space = 32
|
||||
let num_codes = 0
|
||||
/* Static Huffman code for the code length code lengths */
|
||||
const huff = [
|
||||
new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(3, 2),
|
||||
new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(4, 1),
|
||||
new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(3, 2),
|
||||
new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(4, 5),
|
||||
]
|
||||
for (let i = simple_code_or_skip; i < CODE_LENGTH_CODES && space > 0; i++) {
|
||||
const code_len_idx = kCodeLengthCodeOrder[i]
|
||||
let p = 0
|
||||
br.fillBitWindow()
|
||||
p += br.val_ >>> br.bit_pos_ & 15
|
||||
br.bit_pos_ += huff[p].bits
|
||||
const v = huff[p].value
|
||||
code_length_code_lengths[code_len_idx] = v
|
||||
if (v !== 0) {
|
||||
space -= 32 >> v
|
||||
num_codes++
|
||||
}
|
||||
}
|
||||
|
||||
if (!(num_codes === 1 || space === 0))
|
||||
throw new Error('[ReadHuffmanCode] invalid num_codes or space')
|
||||
|
||||
readHuffmanCodeLengths(code_length_code_lengths, alphabet_size, code_lengths, br)
|
||||
}
|
||||
|
||||
const table_size = buildHuffmanTable(tables, table, HUFFMAN_TABLE_BITS, code_lengths, alphabet_size)
|
||||
if (!table_size) throw new Error('brotli BuildHuffmanTable failed')
|
||||
return table_size
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the next Huffman code from bit-stream.
|
||||
* @param {HuffmanCode[]} table
|
||||
* @param {number} index
|
||||
* @param {BrotliBitReader} br
|
||||
* @returns {number}
|
||||
|
||||
*/
|
||||
export function readSymbol(table, index, br) {
|
||||
br.fillBitWindow()
|
||||
index += br.val_ >>> br.bit_pos_ & HUFFMAN_TABLE_MASK
|
||||
const nbits = table[index].bits - HUFFMAN_TABLE_BITS
|
||||
if (nbits > 0) {
|
||||
br.bit_pos_ += HUFFMAN_TABLE_BITS
|
||||
index += table[index].value
|
||||
index += br.val_ >>> br.bit_pos_ & (1 << nbits) - 1
|
||||
}
|
||||
br.bit_pos_ += table[index].bits
|
||||
return table[index].value
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} code_length_code_lengths
|
||||
* @param {number} num_symbols
|
||||
* @param {Uint8Array} code_lengths
|
||||
* @param {BrotliBitReader} br
|
||||
*/
|
||||
function readHuffmanCodeLengths(code_length_code_lengths, num_symbols, code_lengths, br) {
|
||||
let symbol = 0
|
||||
let prev_code_len = kDefaultCodeLength
|
||||
let repeat = 0
|
||||
let repeat_code_len = 0
|
||||
let space = 32768
|
||||
|
||||
const table = []
|
||||
for (let i = 0; i < 32; i++)
|
||||
table.push(new HuffmanCode(0, 0))
|
||||
|
||||
buildHuffmanTable(table, 0, 5, code_length_code_lengths, CODE_LENGTH_CODES)
|
||||
|
||||
while (symbol < num_symbols && space > 0) {
|
||||
let p = 0
|
||||
|
||||
br.readMoreInput()
|
||||
br.fillBitWindow()
|
||||
p += br.val_ >>> br.bit_pos_ & 31
|
||||
br.bit_pos_ += table[p].bits
|
||||
const code_len = table[p].value & 0xff
|
||||
if (code_len < kCodeLengthRepeatCode) {
|
||||
repeat = 0
|
||||
code_lengths[symbol++] = code_len
|
||||
if (code_len !== 0) {
|
||||
prev_code_len = code_len
|
||||
space -= 32768 >> code_len
|
||||
}
|
||||
} else {
|
||||
const extra_bits = code_len - 14
|
||||
let new_len = 0
|
||||
if (code_len === kCodeLengthRepeatCode) {
|
||||
new_len = prev_code_len
|
||||
}
|
||||
if (repeat_code_len !== new_len) {
|
||||
repeat = 0
|
||||
repeat_code_len = new_len
|
||||
}
|
||||
const old_repeat = repeat
|
||||
if (repeat > 0) {
|
||||
repeat -= 2
|
||||
repeat <<= extra_bits
|
||||
}
|
||||
repeat += br.readBits(extra_bits) + 3
|
||||
const repeat_delta = repeat - old_repeat
|
||||
if (symbol + repeat_delta > num_symbols) {
|
||||
throw new Error('[ReadHuffmanCodeLengths] symbol + repeat_delta > num_symbols')
|
||||
}
|
||||
|
||||
for (let x = 0; x < repeat_delta; x++)
|
||||
code_lengths[symbol + x] = repeat_code_len
|
||||
|
||||
symbol += repeat_delta
|
||||
|
||||
if (repeat_code_len !== 0) {
|
||||
space -= repeat_delta << 15 - repeat_code_len
|
||||
}
|
||||
}
|
||||
}
|
||||
if (space !== 0) {
|
||||
throw new Error('[ReadHuffmanCodeLengths] space = ' + space)
|
||||
}
|
||||
|
||||
for (; symbol < num_symbols; symbol++)
|
||||
code_lengths[symbol] = 0
|
||||
}
|
||||
64
src/brotliPrefix.js
Normal file
64
src/brotliPrefix.js
Normal file
@ -0,0 +1,64 @@
|
||||
/* Copyright 2013 Google Inc. 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.
|
||||
|
||||
Lookup tables to map prefix codes to value ranges. This is used during
|
||||
decoding of the block lengths, literal insertion lengths and copy lengths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the range of values belonging to a prefix code:
|
||||
* [offset, offset + 2^nbits)
|
||||
* @param {number} offset
|
||||
* @param {number} nbits
|
||||
*/
|
||||
function PrefixCodeRange(offset, nbits) {
|
||||
this.offset = offset
|
||||
this.nbits = nbits
|
||||
}
|
||||
|
||||
export const kBlockLengthPrefixCode = [
|
||||
new PrefixCodeRange(1, 2), new PrefixCodeRange(5, 2), new PrefixCodeRange(9, 2), new PrefixCodeRange(13, 2),
|
||||
new PrefixCodeRange(17, 3), new PrefixCodeRange(25, 3), new PrefixCodeRange(33, 3), new PrefixCodeRange(41, 3),
|
||||
new PrefixCodeRange(49, 4), new PrefixCodeRange(65, 4), new PrefixCodeRange(81, 4), new PrefixCodeRange(97, 4),
|
||||
new PrefixCodeRange(113, 5), new PrefixCodeRange(145, 5), new PrefixCodeRange(177, 5), new PrefixCodeRange(209, 5),
|
||||
new PrefixCodeRange(241, 6), new PrefixCodeRange(305, 6), new PrefixCodeRange(369, 7), new PrefixCodeRange(497, 8),
|
||||
new PrefixCodeRange(753, 9), new PrefixCodeRange(1265, 10), new PrefixCodeRange(2289, 11), new PrefixCodeRange(4337, 12),
|
||||
new PrefixCodeRange(8433, 13), new PrefixCodeRange(16625, 24),
|
||||
]
|
||||
|
||||
export const kInsertLengthPrefixCode = [
|
||||
new PrefixCodeRange(0, 0), new PrefixCodeRange(1, 0), new PrefixCodeRange(2, 0), new PrefixCodeRange(3, 0),
|
||||
new PrefixCodeRange(4, 0), new PrefixCodeRange(5, 0), new PrefixCodeRange(6, 1), new PrefixCodeRange(8, 1),
|
||||
new PrefixCodeRange(10, 2), new PrefixCodeRange(14, 2), new PrefixCodeRange(18, 3), new PrefixCodeRange(26, 3),
|
||||
new PrefixCodeRange(34, 4), new PrefixCodeRange(50, 4), new PrefixCodeRange(66, 5), new PrefixCodeRange(98, 5),
|
||||
new PrefixCodeRange(130, 6), new PrefixCodeRange(194, 7), new PrefixCodeRange(322, 8), new PrefixCodeRange(578, 9),
|
||||
new PrefixCodeRange(1090, 10), new PrefixCodeRange(2114, 12), new PrefixCodeRange(6210, 14), new PrefixCodeRange(22594, 24),
|
||||
]
|
||||
|
||||
export const kCopyLengthPrefixCode = [
|
||||
new PrefixCodeRange(2, 0), new PrefixCodeRange(3, 0), new PrefixCodeRange(4, 0), new PrefixCodeRange(5, 0),
|
||||
new PrefixCodeRange(6, 0), new PrefixCodeRange(7, 0), new PrefixCodeRange(8, 0), new PrefixCodeRange(9, 0),
|
||||
new PrefixCodeRange(10, 1), new PrefixCodeRange(12, 1), new PrefixCodeRange(14, 2), new PrefixCodeRange(18, 2),
|
||||
new PrefixCodeRange(22, 3), new PrefixCodeRange(30, 3), new PrefixCodeRange(38, 4), new PrefixCodeRange(54, 4),
|
||||
new PrefixCodeRange(70, 5), new PrefixCodeRange(102, 5), new PrefixCodeRange(134, 6), new PrefixCodeRange(198, 7),
|
||||
new PrefixCodeRange(326, 8), new PrefixCodeRange(582, 9), new PrefixCodeRange(1094, 10), new PrefixCodeRange(2118, 24),
|
||||
]
|
||||
|
||||
export const kInsertRangeLut = [
|
||||
0, 0, 8, 8, 0, 16, 8, 16, 16,
|
||||
]
|
||||
|
||||
export const kCopyRangeLut = [
|
||||
0, 8, 0, 8, 16, 0, 16, 8, 16,
|
||||
]
|
||||
46
src/brotliStreams.js
Normal file
46
src/brotliStreams.js
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @param {Uint8Array} buffer
|
||||
*/
|
||||
export function BrotliInput(buffer) {
|
||||
this.buffer = buffer
|
||||
this.pos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
* @param {number} i
|
||||
* @param {number} count
|
||||
* @returns {number}
|
||||
*/
|
||||
BrotliInput.prototype.read = function(buf, i, count) {
|
||||
if (this.pos + count > this.buffer.length) {
|
||||
count = this.buffer.length - this.pos
|
||||
}
|
||||
|
||||
for (let p = 0; p < count; p++)
|
||||
buf[i + p] = this.buffer[this.pos + p]
|
||||
|
||||
this.pos += count
|
||||
return count
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
*/
|
||||
export function BrotliOutput(buf) {
|
||||
this.buffer = buf
|
||||
this.pos = 0
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} buf
|
||||
* @param {number} count
|
||||
* @returns {number}
|
||||
*/
|
||||
BrotliOutput.prototype.write = function(buf, count) {
|
||||
if (this.pos + count > this.buffer.length) throw new Error('brotli output buffer is not large enough')
|
||||
|
||||
this.buffer.set(buf.subarray(0, count), this.pos)
|
||||
this.pos += count
|
||||
return count
|
||||
}
|
||||
256
src/brotliTransform.js
Normal file
256
src/brotliTransform.js
Normal file
@ -0,0 +1,256 @@
|
||||
/* Copyright 2013 Google Inc. 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.
|
||||
|
||||
Transformations on dictionary words.
|
||||
*/
|
||||
|
||||
import { getDictionary } from './brotliDictionary.js'
|
||||
|
||||
const kIdentity = 0
|
||||
const kOmitLast1 = 1
|
||||
const kOmitLast2 = 2
|
||||
const kOmitLast3 = 3
|
||||
const kOmitLast4 = 4
|
||||
const kOmitLast5 = 5
|
||||
const kOmitLast6 = 6
|
||||
const kOmitLast7 = 7
|
||||
const kOmitLast8 = 8
|
||||
const kOmitLast9 = 9
|
||||
const kUppercaseFirst = 10
|
||||
const kUppercaseAll = 11
|
||||
const kOmitFirst1 = 12
|
||||
const kOmitFirst2 = 13
|
||||
const kOmitFirst3 = 14
|
||||
const kOmitFirst4 = 15
|
||||
const kOmitFirst5 = 16
|
||||
const kOmitFirst6 = 17
|
||||
const kOmitFirst7 = 18
|
||||
// const kOmitFirst8 = 19
|
||||
const kOmitFirst9 = 20
|
||||
|
||||
/**
|
||||
* @param {string} prefix
|
||||
* @param {number} transform
|
||||
* @param {string} suffix
|
||||
*/
|
||||
function Transform(prefix, transform, suffix) {
|
||||
this.prefix = new Uint8Array(prefix.length)
|
||||
this.transform = transform
|
||||
this.suffix = new Uint8Array(suffix.length)
|
||||
|
||||
for (let i = 0; i < prefix.length; i++) this.prefix[i] = prefix.charCodeAt(i)
|
||||
for (let i = 0; i < suffix.length; i++) this.suffix[i] = suffix.charCodeAt(i)
|
||||
}
|
||||
|
||||
export const kTransforms = [
|
||||
new Transform( '', kIdentity, '' ),
|
||||
new Transform( '', kIdentity, ' ' ),
|
||||
new Transform( ' ', kIdentity, ' ' ),
|
||||
new Transform( '', kOmitFirst1, '' ),
|
||||
new Transform( '', kUppercaseFirst, ' ' ),
|
||||
new Transform( '', kIdentity, ' the ' ),
|
||||
new Transform( ' ', kIdentity, '' ),
|
||||
new Transform( 's ', kIdentity, ' ' ),
|
||||
new Transform( '', kIdentity, ' of ' ),
|
||||
new Transform( '', kUppercaseFirst, '' ),
|
||||
new Transform( '', kIdentity, ' and ' ),
|
||||
new Transform( '', kOmitFirst2, '' ),
|
||||
new Transform( '', kOmitLast1, '' ),
|
||||
new Transform( ', ', kIdentity, ' ' ),
|
||||
new Transform( '', kIdentity, ', ' ),
|
||||
new Transform( ' ', kUppercaseFirst, ' ' ),
|
||||
new Transform( '', kIdentity, ' in ' ),
|
||||
new Transform( '', kIdentity, ' to ' ),
|
||||
new Transform( 'e ', kIdentity, ' ' ),
|
||||
new Transform( '', kIdentity, '"' ),
|
||||
new Transform( '', kIdentity, '.' ),
|
||||
new Transform( '', kIdentity, '">' ),
|
||||
new Transform( '', kIdentity, '\n' ),
|
||||
new Transform( '', kOmitLast3, '' ),
|
||||
new Transform( '', kIdentity, ']' ),
|
||||
new Transform( '', kIdentity, ' for ' ),
|
||||
new Transform( '', kOmitFirst3, '' ),
|
||||
new Transform( '', kOmitLast2, '' ),
|
||||
new Transform( '', kIdentity, ' a ' ),
|
||||
new Transform( '', kIdentity, ' that ' ),
|
||||
new Transform( ' ', kUppercaseFirst, '' ),
|
||||
new Transform( '', kIdentity, '. ' ),
|
||||
new Transform( '.', kIdentity, '' ),
|
||||
new Transform( ' ', kIdentity, ', ' ),
|
||||
new Transform( '', kOmitFirst4, '' ),
|
||||
new Transform( '', kIdentity, ' with ' ),
|
||||
new Transform( '', kIdentity, '\'' ),
|
||||
new Transform( '', kIdentity, ' from ' ),
|
||||
new Transform( '', kIdentity, ' by ' ),
|
||||
new Transform( '', kOmitFirst5, '' ),
|
||||
new Transform( '', kOmitFirst6, '' ),
|
||||
new Transform( ' the ', kIdentity, '' ),
|
||||
new Transform( '', kOmitLast4, '' ),
|
||||
new Transform( '', kIdentity, '. The ' ),
|
||||
new Transform( '', kUppercaseAll, '' ),
|
||||
new Transform( '', kIdentity, ' on ' ),
|
||||
new Transform( '', kIdentity, ' as ' ),
|
||||
new Transform( '', kIdentity, ' is ' ),
|
||||
new Transform( '', kOmitLast7, '' ),
|
||||
new Transform( '', kOmitLast1, 'ing ' ),
|
||||
new Transform( '', kIdentity, '\n\t' ),
|
||||
new Transform( '', kIdentity, ':' ),
|
||||
new Transform( ' ', kIdentity, '. ' ),
|
||||
new Transform( '', kIdentity, 'ed ' ),
|
||||
new Transform( '', kOmitFirst9, '' ),
|
||||
new Transform( '', kOmitFirst7, '' ),
|
||||
new Transform( '', kOmitLast6, '' ),
|
||||
new Transform( '', kIdentity, '(' ),
|
||||
new Transform( '', kUppercaseFirst, ', ' ),
|
||||
new Transform( '', kOmitLast8, '' ),
|
||||
new Transform( '', kIdentity, ' at ' ),
|
||||
new Transform( '', kIdentity, 'ly ' ),
|
||||
new Transform( ' the ', kIdentity, ' of ' ),
|
||||
new Transform( '', kOmitLast5, '' ),
|
||||
new Transform( '', kOmitLast9, '' ),
|
||||
new Transform( ' ', kUppercaseFirst, ', ' ),
|
||||
new Transform( '', kUppercaseFirst, '"' ),
|
||||
new Transform( '.', kIdentity, '(' ),
|
||||
new Transform( '', kUppercaseAll, ' ' ),
|
||||
new Transform( '', kUppercaseFirst, '">' ),
|
||||
new Transform( '', kIdentity, '="' ),
|
||||
new Transform( ' ', kIdentity, '.' ),
|
||||
new Transform( '.com/', kIdentity, '' ),
|
||||
new Transform( ' the ', kIdentity, ' of the ' ),
|
||||
new Transform( '', kUppercaseFirst, '\'' ),
|
||||
new Transform( '', kIdentity, '. This ' ),
|
||||
new Transform( '', kIdentity, ',' ),
|
||||
new Transform( '.', kIdentity, ' ' ),
|
||||
new Transform( '', kUppercaseFirst, '(' ),
|
||||
new Transform( '', kUppercaseFirst, '.' ),
|
||||
new Transform( '', kIdentity, ' not ' ),
|
||||
new Transform( ' ', kIdentity, '="' ),
|
||||
new Transform( '', kIdentity, 'er ' ),
|
||||
new Transform( ' ', kUppercaseAll, ' ' ),
|
||||
new Transform( '', kIdentity, 'al ' ),
|
||||
new Transform( ' ', kUppercaseAll, '' ),
|
||||
new Transform( '', kIdentity, '=\'' ),
|
||||
new Transform( '', kUppercaseAll, '"' ),
|
||||
new Transform( '', kUppercaseFirst, '. ' ),
|
||||
new Transform( ' ', kIdentity, '(' ),
|
||||
new Transform( '', kIdentity, 'ful ' ),
|
||||
new Transform( ' ', kUppercaseFirst, '. ' ),
|
||||
new Transform( '', kIdentity, 'ive ' ),
|
||||
new Transform( '', kIdentity, 'less ' ),
|
||||
new Transform( '', kUppercaseAll, '\'' ),
|
||||
new Transform( '', kIdentity, 'est ' ),
|
||||
new Transform( ' ', kUppercaseFirst, '.' ),
|
||||
new Transform( '', kUppercaseAll, '">' ),
|
||||
new Transform( ' ', kIdentity, '=\'' ),
|
||||
new Transform( '', kUppercaseFirst, ',' ),
|
||||
new Transform( '', kIdentity, 'ize ' ),
|
||||
new Transform( '', kUppercaseAll, '.' ),
|
||||
new Transform( '\xc2\xa0', kIdentity, '' ),
|
||||
new Transform( ' ', kIdentity, ',' ),
|
||||
new Transform( '', kUppercaseFirst, '="' ),
|
||||
new Transform( '', kUppercaseAll, '="' ),
|
||||
new Transform( '', kIdentity, 'ous ' ),
|
||||
new Transform( '', kUppercaseAll, ', ' ),
|
||||
new Transform( '', kUppercaseFirst, '=\'' ),
|
||||
new Transform( ' ', kUppercaseFirst, ',' ),
|
||||
new Transform( ' ', kUppercaseAll, '="' ),
|
||||
new Transform( ' ', kUppercaseAll, ', ' ),
|
||||
new Transform( '', kUppercaseAll, ',' ),
|
||||
new Transform( '', kUppercaseAll, '(' ),
|
||||
new Transform( '', kUppercaseAll, '. ' ),
|
||||
new Transform( ' ', kUppercaseAll, '.' ),
|
||||
new Transform( '', kUppercaseAll, '=\'' ),
|
||||
new Transform( ' ', kUppercaseAll, '. ' ),
|
||||
new Transform( ' ', kUppercaseFirst, '="' ),
|
||||
new Transform( ' ', kUppercaseAll, '=\'' ),
|
||||
new Transform( ' ', kUppercaseFirst, '=\'' ),
|
||||
]
|
||||
|
||||
export const kNumTransforms = kTransforms.length
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} p
|
||||
* @param {number} i
|
||||
* @returns {number}
|
||||
*/
|
||||
function ToUpperCase(p, i) {
|
||||
if (p[i] < 0xc0) {
|
||||
if (p[i] >= 97 && p[i] <= 122) {
|
||||
p[i] ^= 32
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
/* An overly simplified uppercasing model for utf-8. */
|
||||
if (p[i] < 0xe0) {
|
||||
p[i + 1] ^= 32
|
||||
return 2
|
||||
}
|
||||
|
||||
/* An arbitrary transform for three byte characters. */
|
||||
p[i + 2] ^= 5
|
||||
return 3
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint8Array} dst
|
||||
* @param {number} idx
|
||||
* @param {number} word
|
||||
* @param {number} len
|
||||
* @param {number} transform
|
||||
* @returns {number}
|
||||
*/
|
||||
export function transformDictionaryWord(dst, idx, word, len, transform) {
|
||||
const dictionary = getDictionary()
|
||||
const { prefix } = kTransforms[transform]
|
||||
const { suffix } = kTransforms[transform]
|
||||
const t = kTransforms[transform].transform
|
||||
let skip = t < kOmitFirst1 ? 0 : t - (kOmitFirst1 - 1)
|
||||
const start_idx = idx
|
||||
|
||||
if (skip > len) skip = len
|
||||
|
||||
let prefix_pos = 0
|
||||
while (prefix_pos < prefix.length) {
|
||||
dst[idx++] = prefix[prefix_pos++]
|
||||
}
|
||||
|
||||
word += skip
|
||||
len -= skip
|
||||
|
||||
if (t <= kOmitLast9) len -= t
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
dst[idx++] = dictionary[word + i]
|
||||
}
|
||||
|
||||
let uppercase = idx - len
|
||||
|
||||
if (t === kUppercaseFirst) {
|
||||
ToUpperCase(dst, uppercase)
|
||||
} else if (t === kUppercaseAll) {
|
||||
while (len > 0) {
|
||||
const step = ToUpperCase(dst, uppercase)
|
||||
uppercase += step
|
||||
len -= step
|
||||
}
|
||||
}
|
||||
|
||||
let suffix_pos = 0
|
||||
while (suffix_pos < suffix.length) {
|
||||
dst[idx++] = suffix[suffix_pos++]
|
||||
}
|
||||
|
||||
return idx - start_idx
|
||||
}
|
||||
4
src/index.d.ts
vendored
4
src/index.d.ts
vendored
@ -13,3 +13,7 @@ export type Compressors = {
|
||||
}
|
||||
|
||||
export const compressors: Compressors
|
||||
|
||||
declare module 'brotli/decompress' {
|
||||
export default function(input: Buffer, outputLength: number): Buffer
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import BROTLI from 'brotli/decompress.js'
|
||||
import { decompress as ZSTD } from 'fzstd'
|
||||
import { snappyUncompressor } from 'hysnappy'
|
||||
import { BROTLI } from './brotli.js'
|
||||
import { gunzip } from './gzip.js'
|
||||
import { LZ4, LZ4_RAW } from './lz4.js'
|
||||
|
||||
@ -14,7 +14,6 @@ export const compressors = {
|
||||
gunzip(input, out)
|
||||
return out
|
||||
},
|
||||
// @ts-expect-error brotli expects Buffer but Uint8Array works
|
||||
BROTLI,
|
||||
ZSTD: input => ZSTD(input),
|
||||
LZ4,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user