├── test ├── testdata │ ├── x │ ├── empty │ ├── xyzzy │ ├── empty.compressed │ ├── empty.compressed.00 │ ├── empty.compressed.08 │ ├── empty.compressed.09 │ ├── empty.compressed.10 │ ├── empty.compressed.11 │ ├── empty.compressed.12 │ ├── empty.compressed.13 │ ├── empty.compressed.14 │ ├── empty.compressed.15 │ ├── x.compressed.00 │ ├── x.compressed.01 │ ├── x.compressed.02 │ ├── 10x10y │ ├── quickfox │ ├── 64x │ ├── bb.binast │ ├── mapsdatazrh │ ├── x.compressed │ ├── 64x.compressed │ ├── random_chunks │ ├── 10x10y.compressed │ ├── compressed_file │ ├── monkey.compressed │ ├── x.compressed.03 │ ├── xyzzy.compressed │ ├── zeros.compressed │ ├── compressed_repeated │ ├── empty.compressed.01 │ ├── empty.compressed.02 │ ├── empty.compressed.03 │ ├── empty.compressed.04 │ ├── empty.compressed.05 │ ├── empty.compressed.06 │ ├── empty.compressed.07 │ ├── empty.compressed.16 │ ├── quickfox.compressed │ ├── random_org_10k.bin │ ├── ukkonooa.compressed │ ├── alice29.txt.compressed │ ├── lcet10.txt.compressed │ ├── mapsdatazrh.compressed │ ├── asyoulik.txt.compressed │ ├── backward65536.compressed │ ├── plrabn12.txt.compressed │ ├── ukkonooa │ ├── compressed_file.compressed │ ├── quickfox_repeated.compressed │ ├── random_org_10k.bin.compressed │ ├── compressed_repeated.compressed │ ├── monkey │ ├── empty.compressed.17 │ └── backward65536 └── test.js ├── .gitignore ├── decompress.js ├── .gitmodules ├── index.js ├── .npmignore ├── enc ├── pre.js └── encode.c ├── dec ├── dictionary-browser.js ├── streams.js ├── dictionary.js ├── prefix.js ├── bit_reader.js ├── huffman.js ├── transform.js ├── context.js └── decode.js ├── Makefile ├── package.json ├── compress.js └── readme.md /test/testdata/x: -------------------------------------------------------------------------------- 1 | X -------------------------------------------------------------------------------- /test/testdata/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/testdata/xyzzy: -------------------------------------------------------------------------------- 1 | Xyzzy -------------------------------------------------------------------------------- /test/testdata/empty.compressed: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /test/testdata/empty.compressed.00: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /test/testdata/empty.compressed.08: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.09: -------------------------------------------------------------------------------- 1 | 5 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.10: -------------------------------------------------------------------------------- 1 | 7 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.11: -------------------------------------------------------------------------------- 1 | 9 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.12: -------------------------------------------------------------------------------- 1 | ; -------------------------------------------------------------------------------- /test/testdata/empty.compressed.13: -------------------------------------------------------------------------------- 1 | = -------------------------------------------------------------------------------- /test/testdata/empty.compressed.14: -------------------------------------------------------------------------------- 1 | ? -------------------------------------------------------------------------------- /test/testdata/empty.compressed.15: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /test/testdata/x.compressed.00: -------------------------------------------------------------------------------- 1 | X -------------------------------------------------------------------------------- /test/testdata/x.compressed.01: -------------------------------------------------------------------------------- 1 | ,XX -------------------------------------------------------------------------------- /test/testdata/x.compressed.02: -------------------------------------------------------------------------------- 1 | X -------------------------------------------------------------------------------- /test/testdata/10x10y: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXYYYYYYYYYY -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | .DS_Store 4 | *.o 5 | -------------------------------------------------------------------------------- /test/testdata/quickfox: -------------------------------------------------------------------------------- 1 | The quick brown fox jumps over the lazy dog -------------------------------------------------------------------------------- /decompress.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dec/decode').BrotliDecompressBuffer; 2 | -------------------------------------------------------------------------------- /test/testdata/64x: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/brotli"] 2 | path = vendor/brotli 3 | url = git@github.com:google/brotli.git 4 | -------------------------------------------------------------------------------- /test/testdata/bb.binast: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/bb.binast -------------------------------------------------------------------------------- /test/testdata/mapsdatazrh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/mapsdatazrh -------------------------------------------------------------------------------- /test/testdata/x.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/x.compressed -------------------------------------------------------------------------------- /test/testdata/64x.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/64x.compressed -------------------------------------------------------------------------------- /test/testdata/random_chunks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/random_chunks -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | exports.compress = require('./compress'); 2 | exports.decompress = require('./dec/decode').BrotliDecompressBuffer; 3 | -------------------------------------------------------------------------------- /test/testdata/10x10y.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/10x10y.compressed -------------------------------------------------------------------------------- /test/testdata/compressed_file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/compressed_file -------------------------------------------------------------------------------- /test/testdata/monkey.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/monkey.compressed -------------------------------------------------------------------------------- /test/testdata/x.compressed.03: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/x.compressed.03 -------------------------------------------------------------------------------- /test/testdata/xyzzy.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/xyzzy.compressed -------------------------------------------------------------------------------- /test/testdata/zeros.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/zeros.compressed -------------------------------------------------------------------------------- /test/testdata/compressed_repeated: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/compressed_repeated -------------------------------------------------------------------------------- /test/testdata/empty.compressed.01: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.01 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.02: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.02 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.03: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.03 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.04: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.04 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.05: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.05 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.06: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.06 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.07: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.07 -------------------------------------------------------------------------------- /test/testdata/empty.compressed.16: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/empty.compressed.16 -------------------------------------------------------------------------------- /test/testdata/quickfox.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/quickfox.compressed -------------------------------------------------------------------------------- /test/testdata/random_org_10k.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/random_org_10k.bin -------------------------------------------------------------------------------- /test/testdata/ukkonooa.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/ukkonooa.compressed -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | test/ 3 | *.o 4 | *.c 5 | Makefile 6 | .DS_Store 7 | build/all* 8 | build/decode* 9 | .gitmodules 10 | -------------------------------------------------------------------------------- /test/testdata/alice29.txt.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/alice29.txt.compressed -------------------------------------------------------------------------------- /test/testdata/lcet10.txt.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/lcet10.txt.compressed -------------------------------------------------------------------------------- /test/testdata/mapsdatazrh.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/mapsdatazrh.compressed -------------------------------------------------------------------------------- /test/testdata/asyoulik.txt.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/asyoulik.txt.compressed -------------------------------------------------------------------------------- /test/testdata/backward65536.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/backward65536.compressed -------------------------------------------------------------------------------- /test/testdata/plrabn12.txt.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/plrabn12.txt.compressed -------------------------------------------------------------------------------- /test/testdata/ukkonooa: -------------------------------------------------------------------------------- 1 | ukko nooa, ukko nooa oli kunnon mies, kun han meni saunaan, pisti laukun naulaan, ukko nooa, ukko nooa oli kunnon mies. -------------------------------------------------------------------------------- /test/testdata/compressed_file.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/compressed_file.compressed -------------------------------------------------------------------------------- /test/testdata/quickfox_repeated.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/quickfox_repeated.compressed -------------------------------------------------------------------------------- /test/testdata/random_org_10k.bin.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/random_org_10k.bin.compressed -------------------------------------------------------------------------------- /test/testdata/compressed_repeated.compressed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Technologies/brotli.js/master/test/testdata/compressed_repeated.compressed -------------------------------------------------------------------------------- /enc/pre.js: -------------------------------------------------------------------------------- 1 | var Module = {}; 2 | var decode = require('../decompress'); 3 | var base64 = require('base64-js'); 4 | Module['readBinary'] = function() { 5 | var src = base64['toByteArray'](require('../build/mem.js')); 6 | return decode(src); 7 | }; 8 | -------------------------------------------------------------------------------- /enc/encode.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int encode(int quality, int lgwin, int mode, 4 | size_t input_size, const uint8_t* input_buffer, 5 | size_t encoded_size, uint8_t* encoded_buffer) { 6 | if (BrotliEncoderCompress(quality, lgwin, (BrotliEncoderMode) mode, 7 | input_size, input_buffer, &encoded_size, encoded_buffer)) { 8 | return encoded_size; 9 | } 10 | 11 | return -1; 12 | } 13 | -------------------------------------------------------------------------------- /dec/dictionary-browser.js: -------------------------------------------------------------------------------- 1 | var base64 = require('base64-js'); 2 | var fs = require('fs'); 3 | 4 | /** 5 | * The normal dictionary-data.js is quite large, which makes it 6 | * unsuitable for browser usage. In order to make it smaller, 7 | * we read dictionary.bin, which is a compressed version of 8 | * the dictionary, and on initial load, Brotli decompresses 9 | * it's own dictionary. 😜 10 | */ 11 | exports.init = function() { 12 | var BrotliDecompressBuffer = require('./decode').BrotliDecompressBuffer; 13 | var compressed = base64.toByteArray(require('./dictionary.bin.js')); 14 | return BrotliDecompressBuffer(compressed); 15 | }; 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CPP = emcc 2 | CPPFLAGS = -O3 -s TOTAL_MEMORY=318767104 -s NO_FILESYSTEM=1 -s NO_BROWSER=1 --closure 1 --llvm-lto 1 --pre-js enc/pre.js 3 | 4 | COMMONDIR = vendor/brotli/common 5 | ENCDIR = vendor/brotli/enc 6 | ENCSRC = enc/encode.c $(wildcard $(COMMONDIR)/*.c) $(wildcard $(ENCDIR)/*.c) 7 | ENCOBJ = $(ENCSRC:.c=.o) 8 | 9 | all: build/encode.js 10 | 11 | .c.o .cc.o: 12 | $(CPP) -I vendor/brotli/include -c $< -o $@ 13 | 14 | build/encode.js: enc/pre.js $(ENCOBJ) 15 | mkdir -p build/ 16 | $(CPP) $(CPPFLAGS) -s EXPORTED_FUNCTIONS="['_encode']" -s EXPORTED_RUNTIME_METHODS="['malloc', 'free']" $(ENCOBJ) -o build/encode.js 17 | bro --input build/encode.js.mem | base64 | awk '{print "module.exports=\""$$1"\";"}' > build/mem.js 18 | rm build/encode.js.mem 19 | 20 | clean: 21 | rm -rf $(ENCOBJ) build/ 22 | -------------------------------------------------------------------------------- /test/testdata/monkey: -------------------------------------------------------------------------------- 1 | znxcvnmz,xvnm.,zxcnv.,xcn.z,vn.zvn.zxcvn.,zxcn.vn.v,znm.,vnzx.,vnzxc.vn.z,vnz.,nv.z,nvmzxc,nvzxcvcnm.,vczxvnzxcnvmxc.zmcnvzm.,nvmc,nzxmc,vn.mnnmzxc,vnxcnmv,znvzxcnmv,.xcnvm,zxcnzxv.zx,qweryweurqioweupropqwutioweupqrioweutiopweuriopweuriopqwurioputiopqwuriowuqerioupqweropuweropqwurweuqriopuropqwuriopuqwriopuqweopruioqweurqweuriouqweopruioupqiytioqtyiowtyqptypryoqweutioioqtweqruowqeytiowquiourowetyoqwupiotweuqiorweuqroipituqwiorqwtioweuriouytuioerytuioweryuitoweytuiweyuityeruirtyuqriqweuropqweiruioqweurioqwuerioqwyuituierwotueryuiotweyrtuiwertyioweryrueioqptyioruyiopqwtjkasdfhlafhlasdhfjklashjkfhasjklfhklasjdfhklasdhfjkalsdhfklasdhjkflahsjdkfhklasfhjkasdfhasfjkasdhfklsdhalghhaf;hdklasfhjklashjklfasdhfasdjklfhsdjklafsd;hkldadfjjklasdhfjasddfjklfhakjklasdjfkl;asdjfasfljasdfhjklasdfhjkaghjkashf;djfklasdjfkljasdklfjklasdjfkljasdfkljaklfj -------------------------------------------------------------------------------- /dec/streams.js: -------------------------------------------------------------------------------- 1 | function BrotliInput(buffer) { 2 | this.buffer = buffer; 3 | this.pos = 0; 4 | } 5 | 6 | BrotliInput.prototype.read = function(buf, i, count) { 7 | if (this.pos + count > this.buffer.length) { 8 | count = this.buffer.length - this.pos; 9 | } 10 | 11 | for (var p = 0; p < count; p++) 12 | buf[i + p] = this.buffer[this.pos + p]; 13 | 14 | this.pos += count; 15 | return count; 16 | } 17 | 18 | exports.BrotliInput = BrotliInput; 19 | 20 | function BrotliOutput(buf) { 21 | this.buffer = buf; 22 | this.pos = 0; 23 | } 24 | 25 | BrotliOutput.prototype.write = function(buf, count) { 26 | if (this.pos + count > this.buffer.length) 27 | throw new Error('Output buffer is not large enough'); 28 | 29 | this.buffer.set(buf.subarray(0, count), this.pos); 30 | this.pos += count; 31 | return count; 32 | }; 33 | 34 | exports.BrotliOutput = BrotliOutput; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brotli", 3 | "version": "1.3.1", 4 | "description": "A port of the Brotli compression algorithm as used in WOFF2", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/devongovett/brotli.js.git" 9 | }, 10 | "keywords": [ 11 | "compress", 12 | "decompress", 13 | "encode", 14 | "decode" 15 | ], 16 | "author": "Devon Govett ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/devongovett/brotli.js/issues" 20 | }, 21 | "homepage": "https://github.com/devongovett/brotli.js", 22 | "dependencies": { 23 | "base64-js": "^1.1.2" 24 | }, 25 | "devDependencies": { 26 | "mocha": "^2.2.1" 27 | }, 28 | "browser": { 29 | "./dec/dictionary-data.js": "./dec/dictionary-browser.js" 30 | }, 31 | "scripts": { 32 | "test": "mocha", 33 | "prepublish": "make" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /dec/dictionary.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Collection of static dictionary words. 16 | */ 17 | 18 | var data = require('./dictionary-data'); 19 | exports.init = function() { 20 | exports.dictionary = data.init(); 21 | }; 22 | 23 | exports.offsetsByLength = new Uint32Array([ 24 | 0, 0, 0, 0, 0, 4096, 9216, 21504, 35840, 44032, 25 | 53248, 63488, 74752, 87040, 93696, 100864, 104704, 106752, 108928, 113536, 26 | 115968, 118528, 119872, 121280, 122016, 27 | ]); 28 | 29 | exports.sizeBitsByLength = new Uint8Array([ 30 | 0, 0, 0, 0, 10, 10, 11, 11, 10, 10, 31 | 10, 10, 10, 9, 9, 8, 7, 7, 8, 7, 32 | 7, 6, 6, 5, 5, 33 | ]); 34 | 35 | exports.minDictionaryWordLength = 4; 36 | exports.maxDictionaryWordLength = 24; 37 | -------------------------------------------------------------------------------- /compress.js: -------------------------------------------------------------------------------- 1 | var brotli = require('./build/encode'); 2 | 3 | /** 4 | * Compresses the given buffer 5 | * The second parameter is optional and specifies whether the buffer is 6 | * text or binary data (the default is binary). 7 | * Returns null on error 8 | */ 9 | module.exports = function(buffer, opts) { 10 | // default to binary data 11 | var quality = 11; 12 | var mode = 0; 13 | var lgwin = 22; 14 | 15 | if (typeof opts === 'boolean') { 16 | mode = opts ? 0 : 1; 17 | } else if (typeof opts === 'object') { 18 | quality = opts.quality || 11; 19 | mode = opts.mode || 0; 20 | lgwin = opts.lgwin || 22; 21 | } 22 | 23 | // allocate input buffer and copy data to it 24 | var buf = brotli._malloc(buffer.length); 25 | brotli.HEAPU8.set(buffer, buf); 26 | 27 | // allocate output buffer (same size + some padding to be sure it fits), and encode 28 | var outBuf = brotli._malloc(buffer.length + 1024); 29 | var encodedSize = brotli._encode(quality, lgwin, mode, buffer.length, buf, buffer.length, outBuf); 30 | 31 | var outBuffer = null; 32 | if (encodedSize !== -1) { 33 | // allocate and copy data to an output buffer 34 | outBuffer = new Uint8Array(encodedSize); 35 | outBuffer.set(brotli.HEAPU8.subarray(outBuf, outBuf + encodedSize)); 36 | } 37 | 38 | // free malloc'd buffers 39 | brotli._free(buf); 40 | brotli._free(outBuf); 41 | 42 | return outBuffer; 43 | }; 44 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Brotli.js 2 | 3 | Brotli.js is port of the [Brotli](http://tools.ietf.org/html/draft-alakuijala-brotli-01) compression algorithm (as used in the [WOFF2](http://www.w3.org/TR/WOFF2/) font format) to JavaScript. The decompressor is hand ported, and the compressor is ported 4 | with Emscripten. The original C++ source code can be found [here](http://github.com/google/brotli). 5 | 6 | ## Installation and usage 7 | 8 | Install using npm. 9 | 10 | npm install brotli 11 | 12 | If you want to use brotli in the browser, you should use [Browserify](http://browserify.org/) to build it. 13 | 14 | In node, or in browserify, you can load brotli in the standard way: 15 | 16 | ```javascript 17 | var brotli = require('brotli'); 18 | ``` 19 | 20 | You can also require just the `decompress` function or just the `compress` function, which is useful for browserify builds. 21 | For example, here's how you'd require just the `decompress` function. 22 | 23 | ```javascript 24 | var decompress = require('brotli/decompress'); 25 | ``` 26 | 27 | ## API 28 | 29 | ### brotli.decompress(buffer, [outSize]) 30 | 31 | Decompresses the given buffer to produce the original input to the compressor. 32 | The `outSize` parameter is optional, and will be computed by the decompressor 33 | if not provided. Inside a WOFF2 file, this can be computed from the WOFF2 directory. 34 | 35 | ```javascript 36 | // decode a buffer where the output size is known 37 | brotli.decompress(compressedData, uncompressedLength); 38 | 39 | // decode a buffer where the output size is not known 40 | brotli.decompress(fs.readFileSync('compressed.bin')); 41 | ``` 42 | 43 | ### brotli.compress(buffer, isText = false) 44 | 45 | Compresses the given buffer. Pass optional parameters as the second argument. 46 | 47 | ```javascript 48 | // encode a buffer of binary data 49 | brotli.compress(fs.readFileSync('myfile.bin')); 50 | 51 | // encode some data with options (default options shown) 52 | brotli.compress(fs.readFileSync('myfile.bin'), { 53 | mode: 0, // 0 = generic, 1 = text, 2 = font (WOFF2) 54 | quality: 11, // 0 - 11 55 | lgwin: 22 // window size 56 | }); 57 | ``` 58 | 59 | ## License 60 | 61 | MIT 62 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var assert = require('assert'); 3 | var brotli = require('../'); 4 | var decompress = require('../decompress'); 5 | var compress = require('../compress'); 6 | 7 | describe('brotli', function() { 8 | describe('compress', function() { 9 | it('should compress some binary data', function() { 10 | var data = fs.readFileSync('build/encode.js').slice(0, 1024 * 4); 11 | var res = brotli.compress(data); 12 | assert(res.length < data.length); 13 | }); 14 | 15 | it('should compress some binary data using standalone version', function() { 16 | var data = fs.readFileSync('build/encode.js').slice(0, 1024 * 4); 17 | var res = compress(data); 18 | assert(res.length < data.length); 19 | }); 20 | 21 | it('should compress some text data', function() { 22 | this.timeout(100000); // not sure why the first time text data is compressed it is slow... 23 | var data = fs.readFileSync('build/encode.js', 'utf8').slice(0, 1024 * 4); 24 | var res = brotli.compress(data, true); 25 | assert(res.length < data.length); 26 | }); 27 | 28 | it('should compress some text data using standalone version', function() { 29 | var data = fs.readFileSync('build/encode.js', 'utf8').slice(0, 1024 * 4); 30 | var res = compress(data, true); 31 | assert(res.length < data.length); 32 | }); 33 | }); 34 | 35 | describe('decompress', function() { 36 | fs.readdirSync(__dirname + '/testdata').forEach(function(file) { 37 | if (!/\.compressed/.test(file)) return; 38 | 39 | it(file, function() { 40 | var compressed = fs.readFileSync(__dirname + '/testdata/' + file); 41 | var expected = fs.readFileSync(__dirname + '/testdata/' + file.replace(/\.compressed.*/, '')); 42 | var result = decompress(compressed); 43 | assert.deepEqual(new Buffer(result), expected); 44 | }); 45 | }); 46 | }); 47 | 48 | describe('roundtrip', function() { 49 | var files = ['alice29.txt', 'asyoulik.txt', 'lcet10.txt', 'plrabn12.txt']; 50 | files.forEach(function(file) { 51 | it(file, function() { 52 | this.timeout(10000); 53 | var input = fs.readFileSync(__dirname + '/testdata/' + file); 54 | var compressed = compress(input); 55 | var decompressed = decompress(compressed); 56 | assert.deepEqual(new Buffer(decompressed), input); 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /dec/prefix.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Lookup tables to map prefix codes to value ranges. This is used during 16 | decoding of the block lengths, literal insertion lengths and copy lengths. 17 | */ 18 | 19 | /* Represents the range of values belonging to a prefix code: */ 20 | /* [offset, offset + 2^nbits) */ 21 | function PrefixCodeRange(offset, nbits) { 22 | this.offset = offset; 23 | this.nbits = nbits; 24 | } 25 | 26 | exports.kBlockLengthPrefixCode = [ 27 | new PrefixCodeRange(1, 2), new PrefixCodeRange(5, 2), new PrefixCodeRange(9, 2), new PrefixCodeRange(13, 2), 28 | new PrefixCodeRange(17, 3), new PrefixCodeRange(25, 3), new PrefixCodeRange(33, 3), new PrefixCodeRange(41, 3), 29 | new PrefixCodeRange(49, 4), new PrefixCodeRange(65, 4), new PrefixCodeRange(81, 4), new PrefixCodeRange(97, 4), 30 | new PrefixCodeRange(113, 5), new PrefixCodeRange(145, 5), new PrefixCodeRange(177, 5), new PrefixCodeRange(209, 5), 31 | new PrefixCodeRange(241, 6), new PrefixCodeRange(305, 6), new PrefixCodeRange(369, 7), new PrefixCodeRange(497, 8), 32 | new PrefixCodeRange(753, 9), new PrefixCodeRange(1265, 10), new PrefixCodeRange(2289, 11), new PrefixCodeRange(4337, 12), 33 | new PrefixCodeRange(8433, 13), new PrefixCodeRange(16625, 24) 34 | ]; 35 | 36 | exports.kInsertLengthPrefixCode = [ 37 | new PrefixCodeRange(0, 0), new PrefixCodeRange(1, 0), new PrefixCodeRange(2, 0), new PrefixCodeRange(3, 0), 38 | new PrefixCodeRange(4, 0), new PrefixCodeRange(5, 0), new PrefixCodeRange(6, 1), new PrefixCodeRange(8, 1), 39 | new PrefixCodeRange(10, 2), new PrefixCodeRange(14, 2), new PrefixCodeRange(18, 3), new PrefixCodeRange(26, 3), 40 | new PrefixCodeRange(34, 4), new PrefixCodeRange(50, 4), new PrefixCodeRange(66, 5), new PrefixCodeRange(98, 5), 41 | new PrefixCodeRange(130, 6), new PrefixCodeRange(194, 7), new PrefixCodeRange(322, 8), new PrefixCodeRange(578, 9), 42 | new PrefixCodeRange(1090, 10), new PrefixCodeRange(2114, 12), new PrefixCodeRange(6210, 14), new PrefixCodeRange(22594, 24), 43 | ]; 44 | 45 | exports.kCopyLengthPrefixCode = [ 46 | new PrefixCodeRange(2, 0), new PrefixCodeRange(3, 0), new PrefixCodeRange(4, 0), new PrefixCodeRange(5, 0), 47 | new PrefixCodeRange(6, 0), new PrefixCodeRange(7, 0), new PrefixCodeRange(8, 0), new PrefixCodeRange(9, 0), 48 | new PrefixCodeRange(10, 1), new PrefixCodeRange(12, 1), new PrefixCodeRange(14, 2), new PrefixCodeRange(18, 2), 49 | new PrefixCodeRange(22, 3), new PrefixCodeRange(30, 3), new PrefixCodeRange(38, 4), new PrefixCodeRange(54, 4), 50 | new PrefixCodeRange(70, 5), new PrefixCodeRange(102, 5), new PrefixCodeRange(134, 6), new PrefixCodeRange(198, 7), 51 | new PrefixCodeRange(326, 8), new PrefixCodeRange(582, 9), new PrefixCodeRange(1094, 10), new PrefixCodeRange(2118, 24), 52 | ]; 53 | 54 | exports.kInsertRangeLut = [ 55 | 0, 0, 8, 8, 0, 16, 8, 16, 16, 56 | ]; 57 | 58 | exports.kCopyRangeLut = [ 59 | 0, 8, 0, 8, 16, 0, 16, 8, 16, 60 | ]; 61 | -------------------------------------------------------------------------------- /dec/bit_reader.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Bit reading helpers 16 | */ 17 | 18 | const BROTLI_READ_SIZE = 4096; 19 | const BROTLI_IBUF_SIZE = (2 * BROTLI_READ_SIZE + 32); 20 | const BROTLI_IBUF_MASK = (2 * BROTLI_READ_SIZE - 1); 21 | 22 | const kBitMask = new Uint32Array([ 23 | 0, 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023, 2047, 4095, 8191, 16383, 32767, 24 | 65535, 131071, 262143, 524287, 1048575, 2097151, 4194303, 8388607, 16777215 25 | ]); 26 | 27 | /* Input byte buffer, consist of a ringbuffer and a "slack" region where */ 28 | /* bytes from the start of the ringbuffer are copied. */ 29 | function BrotliBitReader(input) { 30 | this.buf_ = new Uint8Array(BROTLI_IBUF_SIZE); 31 | this.input_ = input; /* input callback */ 32 | 33 | this.reset(); 34 | } 35 | 36 | BrotliBitReader.READ_SIZE = BROTLI_READ_SIZE; 37 | BrotliBitReader.IBUF_MASK = BROTLI_IBUF_MASK; 38 | 39 | BrotliBitReader.prototype.reset = function() { 40 | this.buf_ptr_ = 0; /* next input will write here */ 41 | this.val_ = 0; /* pre-fetched bits */ 42 | this.pos_ = 0; /* byte position in stream */ 43 | this.bit_pos_ = 0; /* current bit-reading position in val_ */ 44 | this.bit_end_pos_ = 0; /* bit-reading end position from LSB of val_ */ 45 | this.eos_ = 0; /* input stream is finished */ 46 | 47 | this.readMoreInput(); 48 | for (var i = 0; i < 4; i++) { 49 | this.val_ |= this.buf_[this.pos_] << (8 * i); 50 | ++this.pos_; 51 | } 52 | 53 | return this.bit_end_pos_ > 0; 54 | }; 55 | 56 | /* Fills up the input ringbuffer by calling the input callback. 57 | 58 | Does nothing if there are at least 32 bytes present after current position. 59 | 60 | Returns 0 if either: 61 | - the input callback returned an error, or 62 | - there is no more input and the position is past the end of the stream. 63 | 64 | After encountering the end of the input stream, 32 additional zero bytes are 65 | copied to the ringbuffer, therefore it is safe to call this function after 66 | every 32 bytes of input is read. 67 | */ 68 | BrotliBitReader.prototype.readMoreInput = function() { 69 | if (this.bit_end_pos_ > 256) { 70 | return; 71 | } else if (this.eos_) { 72 | if (this.bit_pos_ > this.bit_end_pos_) 73 | throw new Error('Unexpected end of input ' + this.bit_pos_ + ' ' + this.bit_end_pos_); 74 | } else { 75 | var dst = this.buf_ptr_; 76 | var bytes_read = this.input_.read(this.buf_, dst, BROTLI_READ_SIZE); 77 | if (bytes_read < 0) { 78 | throw new Error('Unexpected end of input'); 79 | } 80 | 81 | if (bytes_read < BROTLI_READ_SIZE) { 82 | this.eos_ = 1; 83 | /* Store 32 bytes of zero after the stream end. */ 84 | for (var p = 0; p < 32; p++) 85 | this.buf_[dst + bytes_read + p] = 0; 86 | } 87 | 88 | if (dst === 0) { 89 | /* Copy the head of the ringbuffer to the slack region. */ 90 | for (var p = 0; p < 32; p++) 91 | this.buf_[(BROTLI_READ_SIZE << 1) + p] = this.buf_[p]; 92 | 93 | this.buf_ptr_ = BROTLI_READ_SIZE; 94 | } else { 95 | this.buf_ptr_ = 0; 96 | } 97 | 98 | this.bit_end_pos_ += bytes_read << 3; 99 | } 100 | }; 101 | 102 | /* Guarantees that there are at least 24 bits in the buffer. */ 103 | BrotliBitReader.prototype.fillBitWindow = function() { 104 | while (this.bit_pos_ >= 8) { 105 | this.val_ >>>= 8; 106 | this.val_ |= this.buf_[this.pos_ & BROTLI_IBUF_MASK] << 24; 107 | ++this.pos_; 108 | this.bit_pos_ = this.bit_pos_ - 8 >>> 0; 109 | this.bit_end_pos_ = this.bit_end_pos_ - 8 >>> 0; 110 | } 111 | }; 112 | 113 | /* Reads the specified number of bits from Read Buffer. */ 114 | BrotliBitReader.prototype.readBits = function(n_bits) { 115 | if (32 - this.bit_pos_ < n_bits) { 116 | this.fillBitWindow(); 117 | } 118 | 119 | var val = ((this.val_ >>> this.bit_pos_) & kBitMask[n_bits]); 120 | this.bit_pos_ += n_bits; 121 | return val; 122 | }; 123 | 124 | module.exports = BrotliBitReader; 125 | -------------------------------------------------------------------------------- /dec/huffman.js: -------------------------------------------------------------------------------- 1 | function HuffmanCode(bits, value) { 2 | this.bits = bits; /* number of bits used for this symbol */ 3 | this.value = value; /* symbol value or table offset */ 4 | } 5 | 6 | exports.HuffmanCode = HuffmanCode; 7 | 8 | const MAX_LENGTH = 15; 9 | 10 | /* Returns reverse(reverse(key, len) + 1, len), where reverse(key, len) is the 11 | bit-wise reversal of the len least significant bits of key. */ 12 | function GetNextKey(key, len) { 13 | var step = 1 << (len - 1); 14 | while (key & step) { 15 | step >>= 1; 16 | } 17 | return (key & (step - 1)) + step; 18 | } 19 | 20 | /* Stores code in table[0], table[step], table[2*step], ..., table[end] */ 21 | /* Assumes that end is an integer multiple of step */ 22 | function ReplicateValue(table, i, step, end, code) { 23 | do { 24 | end -= step; 25 | table[i + end] = new HuffmanCode(code.bits, code.value); 26 | } while (end > 0); 27 | } 28 | 29 | /* Returns the table width of the next 2nd level table. count is the histogram 30 | of bit lengths for the remaining symbols, len is the code length of the next 31 | processed symbol */ 32 | function NextTableBitSize(count, len, root_bits) { 33 | var left = 1 << (len - root_bits); 34 | while (len < MAX_LENGTH) { 35 | left -= count[len]; 36 | if (left <= 0) break; 37 | ++len; 38 | left <<= 1; 39 | } 40 | return len - root_bits; 41 | } 42 | 43 | exports.BrotliBuildHuffmanTable = function(root_table, table, root_bits, code_lengths, code_lengths_size) { 44 | var start_table = table; 45 | var code; /* current table entry */ 46 | var len; /* current code length */ 47 | var symbol; /* symbol index in original or sorted table */ 48 | var key; /* reversed prefix code */ 49 | var step; /* step size to replicate values in current table */ 50 | var low; /* low bits for current root entry */ 51 | var mask; /* mask for low bits */ 52 | var table_bits; /* key length of current table */ 53 | var table_size; /* size of current table */ 54 | var total_size; /* sum of root table size and 2nd level table sizes */ 55 | var sorted; /* symbols sorted by code length */ 56 | var count = new Int32Array(MAX_LENGTH + 1); /* number of codes of each length */ 57 | var offset = new Int32Array(MAX_LENGTH + 1); /* offsets in sorted table for each length */ 58 | 59 | sorted = new Int32Array(code_lengths_size); 60 | 61 | /* build histogram of code lengths */ 62 | for (symbol = 0; symbol < code_lengths_size; symbol++) { 63 | count[code_lengths[symbol]]++; 64 | } 65 | 66 | /* generate offsets into sorted symbol table by code length */ 67 | offset[1] = 0; 68 | for (len = 1; len < MAX_LENGTH; len++) { 69 | offset[len + 1] = offset[len] + count[len]; 70 | } 71 | 72 | /* sort symbols by length, by symbol order within each length */ 73 | for (symbol = 0; symbol < code_lengths_size; symbol++) { 74 | if (code_lengths[symbol] !== 0) { 75 | sorted[offset[code_lengths[symbol]]++] = symbol; 76 | } 77 | } 78 | 79 | table_bits = root_bits; 80 | table_size = 1 << table_bits; 81 | total_size = table_size; 82 | 83 | /* special case code with only one value */ 84 | if (offset[MAX_LENGTH] === 1) { 85 | for (key = 0; key < total_size; ++key) { 86 | root_table[table + key] = new HuffmanCode(0, sorted[0] & 0xffff); 87 | } 88 | 89 | return total_size; 90 | } 91 | 92 | /* fill in root table */ 93 | key = 0; 94 | symbol = 0; 95 | for (len = 1, step = 2; len <= root_bits; ++len, step <<= 1) { 96 | for (; count[len] > 0; --count[len]) { 97 | code = new HuffmanCode(len & 0xff, sorted[symbol++] & 0xffff); 98 | ReplicateValue(root_table, table + key, step, table_size, code); 99 | key = GetNextKey(key, len); 100 | } 101 | } 102 | 103 | /* fill in 2nd level tables and add pointers to root table */ 104 | mask = total_size - 1; 105 | low = -1; 106 | for (len = root_bits + 1, step = 2; len <= MAX_LENGTH; ++len, step <<= 1) { 107 | for (; count[len] > 0; --count[len]) { 108 | if ((key & mask) !== low) { 109 | table += table_size; 110 | table_bits = NextTableBitSize(count, len, root_bits); 111 | table_size = 1 << table_bits; 112 | total_size += table_size; 113 | low = key & mask; 114 | root_table[start_table + low] = new HuffmanCode((table_bits + root_bits) & 0xff, ((table - start_table) - low) & 0xffff); 115 | } 116 | code = new HuffmanCode((len - root_bits) & 0xff, sorted[symbol++] & 0xffff); 117 | ReplicateValue(root_table, table + (key >> root_bits), step, table_size, code); 118 | key = GetNextKey(key, len); 119 | } 120 | } 121 | 122 | return total_size; 123 | } 124 | -------------------------------------------------------------------------------- /dec/transform.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Transformations on dictionary words. 16 | */ 17 | 18 | var BrotliDictionary = require('./dictionary'); 19 | 20 | const kIdentity = 0; 21 | const kOmitLast1 = 1; 22 | const kOmitLast2 = 2; 23 | const kOmitLast3 = 3; 24 | const kOmitLast4 = 4; 25 | const kOmitLast5 = 5; 26 | const kOmitLast6 = 6; 27 | const kOmitLast7 = 7; 28 | const kOmitLast8 = 8; 29 | const kOmitLast9 = 9; 30 | const kUppercaseFirst = 10; 31 | const kUppercaseAll = 11; 32 | const kOmitFirst1 = 12; 33 | const kOmitFirst2 = 13; 34 | const kOmitFirst3 = 14; 35 | const kOmitFirst4 = 15; 36 | const kOmitFirst5 = 16; 37 | const kOmitFirst6 = 17; 38 | const kOmitFirst7 = 18; 39 | const kOmitFirst8 = 19; 40 | const kOmitFirst9 = 20; 41 | 42 | function Transform(prefix, transform, suffix) { 43 | this.prefix = new Uint8Array(prefix.length); 44 | this.transform = transform; 45 | this.suffix = new Uint8Array(suffix.length); 46 | 47 | for (var i = 0; i < prefix.length; i++) 48 | this.prefix[i] = prefix.charCodeAt(i); 49 | 50 | for (var i = 0; i < suffix.length; i++) 51 | this.suffix[i] = suffix.charCodeAt(i); 52 | } 53 | 54 | var kTransforms = [ 55 | new Transform( "", kIdentity, "" ), 56 | new Transform( "", kIdentity, " " ), 57 | new Transform( " ", kIdentity, " " ), 58 | new Transform( "", kOmitFirst1, "" ), 59 | new Transform( "", kUppercaseFirst, " " ), 60 | new Transform( "", kIdentity, " the " ), 61 | new Transform( " ", kIdentity, "" ), 62 | new Transform( "s ", kIdentity, " " ), 63 | new Transform( "", kIdentity, " of " ), 64 | new Transform( "", kUppercaseFirst, "" ), 65 | new Transform( "", kIdentity, " and " ), 66 | new Transform( "", kOmitFirst2, "" ), 67 | new Transform( "", kOmitLast1, "" ), 68 | new Transform( ", ", kIdentity, " " ), 69 | new Transform( "", kIdentity, ", " ), 70 | new Transform( " ", kUppercaseFirst, " " ), 71 | new Transform( "", kIdentity, " in " ), 72 | new Transform( "", kIdentity, " to " ), 73 | new Transform( "e ", kIdentity, " " ), 74 | new Transform( "", kIdentity, "\"" ), 75 | new Transform( "", kIdentity, "." ), 76 | new Transform( "", kIdentity, "\">" ), 77 | new Transform( "", kIdentity, "\n" ), 78 | new Transform( "", kOmitLast3, "" ), 79 | new Transform( "", kIdentity, "]" ), 80 | new Transform( "", kIdentity, " for " ), 81 | new Transform( "", kOmitFirst3, "" ), 82 | new Transform( "", kOmitLast2, "" ), 83 | new Transform( "", kIdentity, " a " ), 84 | new Transform( "", kIdentity, " that " ), 85 | new Transform( " ", kUppercaseFirst, "" ), 86 | new Transform( "", kIdentity, ". " ), 87 | new Transform( ".", kIdentity, "" ), 88 | new Transform( " ", kIdentity, ", " ), 89 | new Transform( "", kOmitFirst4, "" ), 90 | new Transform( "", kIdentity, " with " ), 91 | new Transform( "", kIdentity, "'" ), 92 | new Transform( "", kIdentity, " from " ), 93 | new Transform( "", kIdentity, " by " ), 94 | new Transform( "", kOmitFirst5, "" ), 95 | new Transform( "", kOmitFirst6, "" ), 96 | new Transform( " the ", kIdentity, "" ), 97 | new Transform( "", kOmitLast4, "" ), 98 | new Transform( "", kIdentity, ". The " ), 99 | new Transform( "", kUppercaseAll, "" ), 100 | new Transform( "", kIdentity, " on " ), 101 | new Transform( "", kIdentity, " as " ), 102 | new Transform( "", kIdentity, " is " ), 103 | new Transform( "", kOmitLast7, "" ), 104 | new Transform( "", kOmitLast1, "ing " ), 105 | new Transform( "", kIdentity, "\n\t" ), 106 | new Transform( "", kIdentity, ":" ), 107 | new Transform( " ", kIdentity, ". " ), 108 | new Transform( "", kIdentity, "ed " ), 109 | new Transform( "", kOmitFirst9, "" ), 110 | new Transform( "", kOmitFirst7, "" ), 111 | new Transform( "", kOmitLast6, "" ), 112 | new Transform( "", kIdentity, "(" ), 113 | new Transform( "", kUppercaseFirst, ", " ), 114 | new Transform( "", kOmitLast8, "" ), 115 | new Transform( "", kIdentity, " at " ), 116 | new Transform( "", kIdentity, "ly " ), 117 | new Transform( " the ", kIdentity, " of " ), 118 | new Transform( "", kOmitLast5, "" ), 119 | new Transform( "", kOmitLast9, "" ), 120 | new Transform( " ", kUppercaseFirst, ", " ), 121 | new Transform( "", kUppercaseFirst, "\"" ), 122 | new Transform( ".", kIdentity, "(" ), 123 | new Transform( "", kUppercaseAll, " " ), 124 | new Transform( "", kUppercaseFirst, "\">" ), 125 | new Transform( "", kIdentity, "=\"" ), 126 | new Transform( " ", kIdentity, "." ), 127 | new Transform( ".com/", kIdentity, "" ), 128 | new Transform( " the ", kIdentity, " of the " ), 129 | new Transform( "", kUppercaseFirst, "'" ), 130 | new Transform( "", kIdentity, ". This " ), 131 | new Transform( "", kIdentity, "," ), 132 | new Transform( ".", kIdentity, " " ), 133 | new Transform( "", kUppercaseFirst, "(" ), 134 | new Transform( "", kUppercaseFirst, "." ), 135 | new Transform( "", kIdentity, " not " ), 136 | new Transform( " ", kIdentity, "=\"" ), 137 | new Transform( "", kIdentity, "er " ), 138 | new Transform( " ", kUppercaseAll, " " ), 139 | new Transform( "", kIdentity, "al " ), 140 | new Transform( " ", kUppercaseAll, "" ), 141 | new Transform( "", kIdentity, "='" ), 142 | new Transform( "", kUppercaseAll, "\"" ), 143 | new Transform( "", kUppercaseFirst, ". " ), 144 | new Transform( " ", kIdentity, "(" ), 145 | new Transform( "", kIdentity, "ful " ), 146 | new Transform( " ", kUppercaseFirst, ". " ), 147 | new Transform( "", kIdentity, "ive " ), 148 | new Transform( "", kIdentity, "less " ), 149 | new Transform( "", kUppercaseAll, "'" ), 150 | new Transform( "", kIdentity, "est " ), 151 | new Transform( " ", kUppercaseFirst, "." ), 152 | new Transform( "", kUppercaseAll, "\">" ), 153 | new Transform( " ", kIdentity, "='" ), 154 | new Transform( "", kUppercaseFirst, "," ), 155 | new Transform( "", kIdentity, "ize " ), 156 | new Transform( "", kUppercaseAll, "." ), 157 | new Transform( "\xc2\xa0", kIdentity, "" ), 158 | new Transform( " ", kIdentity, "," ), 159 | new Transform( "", kUppercaseFirst, "=\"" ), 160 | new Transform( "", kUppercaseAll, "=\"" ), 161 | new Transform( "", kIdentity, "ous " ), 162 | new Transform( "", kUppercaseAll, ", " ), 163 | new Transform( "", kUppercaseFirst, "='" ), 164 | new Transform( " ", kUppercaseFirst, "," ), 165 | new Transform( " ", kUppercaseAll, "=\"" ), 166 | new Transform( " ", kUppercaseAll, ", " ), 167 | new Transform( "", kUppercaseAll, "," ), 168 | new Transform( "", kUppercaseAll, "(" ), 169 | new Transform( "", kUppercaseAll, ". " ), 170 | new Transform( " ", kUppercaseAll, "." ), 171 | new Transform( "", kUppercaseAll, "='" ), 172 | new Transform( " ", kUppercaseAll, ". " ), 173 | new Transform( " ", kUppercaseFirst, "=\"" ), 174 | new Transform( " ", kUppercaseAll, "='" ), 175 | new Transform( " ", kUppercaseFirst, "='" ) 176 | ]; 177 | 178 | exports.kTransforms = kTransforms; 179 | exports.kNumTransforms = kTransforms.length; 180 | 181 | function ToUpperCase(p, i) { 182 | if (p[i] < 0xc0) { 183 | if (p[i] >= 97 && p[i] <= 122) { 184 | p[i] ^= 32; 185 | } 186 | return 1; 187 | } 188 | 189 | /* An overly simplified uppercasing model for utf-8. */ 190 | if (p[i] < 0xe0) { 191 | p[i + 1] ^= 32; 192 | return 2; 193 | } 194 | 195 | /* An arbitrary transform for three byte characters. */ 196 | p[i + 2] ^= 5; 197 | return 3; 198 | } 199 | 200 | exports.transformDictionaryWord = function(dst, idx, word, len, transform) { 201 | var prefix = kTransforms[transform].prefix; 202 | var suffix = kTransforms[transform].suffix; 203 | var t = kTransforms[transform].transform; 204 | var skip = t < kOmitFirst1 ? 0 : t - (kOmitFirst1 - 1); 205 | var i = 0; 206 | var start_idx = idx; 207 | var uppercase; 208 | 209 | if (skip > len) { 210 | skip = len; 211 | } 212 | 213 | var prefix_pos = 0; 214 | while (prefix_pos < prefix.length) { 215 | dst[idx++] = prefix[prefix_pos++]; 216 | } 217 | 218 | word += skip; 219 | len -= skip; 220 | 221 | if (t <= kOmitLast9) { 222 | len -= t; 223 | } 224 | 225 | for (i = 0; i < len; i++) { 226 | dst[idx++] = BrotliDictionary.dictionary[word + i]; 227 | } 228 | 229 | uppercase = idx - len; 230 | 231 | if (t === kUppercaseFirst) { 232 | ToUpperCase(dst, uppercase); 233 | } else if (t === kUppercaseAll) { 234 | while (len > 0) { 235 | var step = ToUpperCase(dst, uppercase); 236 | uppercase += step; 237 | len -= step; 238 | } 239 | } 240 | 241 | var suffix_pos = 0; 242 | while (suffix_pos < suffix.length) { 243 | dst[idx++] = suffix[suffix_pos++]; 244 | } 245 | 246 | return idx - start_idx; 247 | } 248 | -------------------------------------------------------------------------------- /dec/context.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | Lookup table to map the previous two bytes to a context id. 16 | 17 | There are four different context modeling modes defined here: 18 | CONTEXT_LSB6: context id is the least significant 6 bits of the last byte, 19 | CONTEXT_MSB6: context id is the most significant 6 bits of the last byte, 20 | CONTEXT_UTF8: second-order context model tuned for UTF8-encoded text, 21 | CONTEXT_SIGNED: second-order context model tuned for signed integers. 22 | 23 | The context id for the UTF8 context model is calculated as follows. If p1 24 | and p2 are the previous two bytes, we calcualte the context as 25 | 26 | context = kContextLookup[p1] | kContextLookup[p2 + 256]. 27 | 28 | If the previous two bytes are ASCII characters (i.e. < 128), this will be 29 | equivalent to 30 | 31 | context = 4 * context1(p1) + context2(p2), 32 | 33 | where context1 is based on the previous byte in the following way: 34 | 35 | 0 : non-ASCII control 36 | 1 : \t, \n, \r 37 | 2 : space 38 | 3 : other punctuation 39 | 4 : " ' 40 | 5 : % 41 | 6 : ( < [ { 42 | 7 : ) > ] } 43 | 8 : , ; : 44 | 9 : . 45 | 10 : = 46 | 11 : number 47 | 12 : upper-case vowel 48 | 13 : upper-case consonant 49 | 14 : lower-case vowel 50 | 15 : lower-case consonant 51 | 52 | and context2 is based on the second last byte: 53 | 54 | 0 : control, space 55 | 1 : punctuation 56 | 2 : upper-case letter, number 57 | 3 : lower-case letter 58 | 59 | If the last byte is ASCII, and the second last byte is not (in a valid UTF8 60 | stream it will be a continuation byte, value between 128 and 191), the 61 | context is the same as if the second last byte was an ASCII control or space. 62 | 63 | If the last byte is a UTF8 lead byte (value >= 192), then the next byte will 64 | be a continuation byte and the context id is 2 or 3 depending on the LSB of 65 | the last byte and to a lesser extent on the second last byte if it is ASCII. 66 | 67 | If the last byte is a UTF8 continuation byte, the second last byte can be: 68 | - continuation byte: the next byte is probably ASCII or lead byte (assuming 69 | 4-byte UTF8 characters are rare) and the context id is 0 or 1. 70 | - lead byte (192 - 207): next byte is ASCII or lead byte, context is 0 or 1 71 | - lead byte (208 - 255): next byte is continuation byte, context is 2 or 3 72 | 73 | The possible value combinations of the previous two bytes, the range of 74 | context ids and the type of the next byte is summarized in the table below: 75 | 76 | |--------\-----------------------------------------------------------------| 77 | | \ Last byte | 78 | | Second \---------------------------------------------------------------| 79 | | last byte \ ASCII | cont. byte | lead byte | 80 | | \ (0-127) | (128-191) | (192-) | 81 | |=============|===================|=====================|==================| 82 | | ASCII | next: ASCII/lead | not valid | next: cont. | 83 | | (0-127) | context: 4 - 63 | | context: 2 - 3 | 84 | |-------------|-------------------|---------------------|------------------| 85 | | cont. byte | next: ASCII/lead | next: ASCII/lead | next: cont. | 86 | | (128-191) | context: 4 - 63 | context: 0 - 1 | context: 2 - 3 | 87 | |-------------|-------------------|---------------------|------------------| 88 | | lead byte | not valid | next: ASCII/lead | not valid | 89 | | (192-207) | | context: 0 - 1 | | 90 | |-------------|-------------------|---------------------|------------------| 91 | | lead byte | not valid | next: cont. | not valid | 92 | | (208-) | | context: 2 - 3 | | 93 | |-------------|-------------------|---------------------|------------------| 94 | 95 | The context id for the signed context mode is calculated as: 96 | 97 | context = (kContextLookup[512 + p1] << 3) | kContextLookup[512 + p2]. 98 | 99 | For any context modeling modes, the context ids can be calculated by |-ing 100 | together two lookups from one table using context model dependent offsets: 101 | 102 | context = kContextLookup[offset1 + p1] | kContextLookup[offset2 + p2]. 103 | 104 | where offset1 and offset2 are dependent on the context mode. 105 | */ 106 | 107 | const CONTEXT_LSB6 = 0; 108 | const CONTEXT_MSB6 = 1; 109 | const CONTEXT_UTF8 = 2; 110 | const CONTEXT_SIGNED = 3; 111 | 112 | /* Common context lookup table for all context modes. */ 113 | exports.lookup = new Uint8Array([ 114 | /* CONTEXT_UTF8, last byte. */ 115 | /* ASCII range. */ 116 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 4, 0, 0, 117 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 118 | 8, 12, 16, 12, 12, 20, 12, 16, 24, 28, 12, 12, 32, 12, 36, 12, 119 | 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 32, 32, 24, 40, 28, 12, 120 | 12, 48, 52, 52, 52, 48, 52, 52, 52, 48, 52, 52, 52, 52, 52, 48, 121 | 52, 52, 52, 52, 52, 48, 52, 52, 52, 52, 52, 24, 12, 28, 12, 12, 122 | 12, 56, 60, 60, 60, 56, 60, 60, 60, 56, 60, 60, 60, 60, 60, 56, 123 | 60, 60, 60, 60, 60, 56, 60, 60, 60, 60, 60, 24, 12, 28, 12, 0, 124 | /* UTF8 continuation byte range. */ 125 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 126 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 127 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 128 | 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 129 | /* UTF8 lead byte range. */ 130 | 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 131 | 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 132 | 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 133 | 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 2, 3, 134 | /* CONTEXT_UTF8 second last byte. */ 135 | /* ASCII range. */ 136 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 137 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 138 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 139 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 140 | 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 141 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 142 | 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 143 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 0, 144 | /* UTF8 continuation byte range. */ 145 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 146 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 147 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 148 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 149 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150 | /* UTF8 lead byte range. */ 151 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 152 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 153 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 154 | /* CONTEXT_SIGNED, second last byte. */ 155 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 156 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 157 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 158 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 159 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 160 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 161 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 162 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 163 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 164 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 165 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 166 | 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 167 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 168 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 169 | 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 170 | 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 171 | /* CONTEXT_SIGNED, last byte, same as the above values shifted by 3 bits. */ 172 | 0, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 173 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 174 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 175 | 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 176 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 177 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 178 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 179 | 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 180 | 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 181 | 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 182 | 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 183 | 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 184 | 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 185 | 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 186 | 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 187 | 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 56, 188 | /* CONTEXT_LSB6, last byte. */ 189 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 190 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 191 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 192 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 193 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 194 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 195 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 196 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 197 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 198 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 199 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 200 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 201 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 202 | 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 203 | 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 204 | 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 205 | /* CONTEXT_MSB6, last byte. */ 206 | 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 207 | 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 208 | 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11, 209 | 12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15, 210 | 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 211 | 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 212 | 24, 24, 24, 24, 25, 25, 25, 25, 26, 26, 26, 26, 27, 27, 27, 27, 213 | 28, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 214 | 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 215 | 36, 36, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 39, 39, 39, 216 | 40, 40, 40, 40, 41, 41, 41, 41, 42, 42, 42, 42, 43, 43, 43, 43, 217 | 44, 44, 44, 44, 45, 45, 45, 45, 46, 46, 46, 46, 47, 47, 47, 47, 218 | 48, 48, 48, 48, 49, 49, 49, 49, 50, 50, 50, 50, 51, 51, 51, 51, 219 | 52, 52, 52, 52, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 55, 55, 220 | 56, 56, 56, 56, 57, 57, 57, 57, 58, 58, 58, 58, 59, 59, 59, 59, 221 | 60, 60, 60, 60, 61, 61, 61, 61, 62, 62, 62, 62, 63, 63, 63, 63, 222 | /* CONTEXT_{M,L}SB6, second last byte, */ 223 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 225 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 226 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 227 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 228 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 229 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 231 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 232 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 233 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 234 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 235 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 236 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 237 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 238 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 239 | ]); 240 | 241 | exports.lookupOffsets = new Uint16Array([ 242 | /* CONTEXT_LSB6 */ 243 | 1024, 1536, 244 | /* CONTEXT_MSB6 */ 245 | 1280, 1536, 246 | /* CONTEXT_UTF8 */ 247 | 0, 256, 248 | /* CONTEXT_SIGNED */ 249 | 768, 512, 250 | ]); 251 | -------------------------------------------------------------------------------- /dec/decode.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2013 Google Inc. All Rights Reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | */ 15 | 16 | var BrotliInput = require('./streams').BrotliInput; 17 | var BrotliOutput = require('./streams').BrotliOutput; 18 | var BrotliBitReader = require('./bit_reader'); 19 | var BrotliDictionary = require('./dictionary'); 20 | var HuffmanCode = require('./huffman').HuffmanCode; 21 | var BrotliBuildHuffmanTable = require('./huffman').BrotliBuildHuffmanTable; 22 | var Context = require('./context'); 23 | var Prefix = require('./prefix'); 24 | var Transform = require('./transform'); 25 | 26 | const kDefaultCodeLength = 8; 27 | const kCodeLengthRepeatCode = 16; 28 | const kNumLiteralCodes = 256; 29 | const kNumInsertAndCopyCodes = 704; 30 | const kNumBlockLengthCodes = 26; 31 | const kLiteralContextBits = 6; 32 | const kDistanceContextBits = 2; 33 | 34 | const HUFFMAN_TABLE_BITS = 8; 35 | const HUFFMAN_TABLE_MASK = 0xff; 36 | /* Maximum possible Huffman table size for an alphabet size of 704, max code 37 | * length 15 and root table bits 8. */ 38 | const HUFFMAN_MAX_TABLE_SIZE = 1080; 39 | 40 | const CODE_LENGTH_CODES = 18; 41 | const kCodeLengthCodeOrder = new Uint8Array([ 42 | 1, 2, 3, 4, 0, 5, 17, 6, 16, 7, 8, 9, 10, 11, 12, 13, 14, 15, 43 | ]); 44 | 45 | const NUM_DISTANCE_SHORT_CODES = 16; 46 | const kDistanceShortCodeIndexOffset = new Uint8Array([ 47 | 3, 2, 1, 0, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2 48 | ]); 49 | 50 | const kDistanceShortCodeValueOffset = new Int8Array([ 51 | 0, 0, 0, 0, -1, 1, -2, 2, -3, 3, -1, 1, -2, 2, -3, 3 52 | ]); 53 | 54 | const kMaxHuffmanTableSize = new Uint16Array([ 55 | 256, 402, 436, 468, 500, 534, 566, 598, 630, 662, 694, 726, 758, 790, 822, 56 | 854, 886, 920, 952, 984, 1016, 1048, 1080 57 | ]); 58 | 59 | function DecodeWindowBits(br) { 60 | var n; 61 | if (br.readBits(1) === 0) { 62 | return 16; 63 | } 64 | 65 | n = br.readBits(3); 66 | if (n > 0) { 67 | return 17 + n; 68 | } 69 | 70 | n = br.readBits(3); 71 | if (n > 0) { 72 | return 8 + n; 73 | } 74 | 75 | return 17; 76 | } 77 | 78 | /* Decodes a number in the range [0..255], by reading 1 - 11 bits. */ 79 | function DecodeVarLenUint8(br) { 80 | if (br.readBits(1)) { 81 | var nbits = br.readBits(3); 82 | if (nbits === 0) { 83 | return 1; 84 | } else { 85 | return br.readBits(nbits) + (1 << nbits); 86 | } 87 | } 88 | return 0; 89 | } 90 | 91 | function MetaBlockLength() { 92 | this.meta_block_length = 0; 93 | this.input_end = 0; 94 | this.is_uncompressed = 0; 95 | this.is_metadata = false; 96 | } 97 | 98 | function DecodeMetaBlockLength(br) { 99 | var out = new MetaBlockLength; 100 | var size_nibbles; 101 | var size_bytes; 102 | var i; 103 | 104 | out.input_end = br.readBits(1); 105 | if (out.input_end && br.readBits(1)) { 106 | return out; 107 | } 108 | 109 | size_nibbles = br.readBits(2) + 4; 110 | if (size_nibbles === 7) { 111 | out.is_metadata = true; 112 | 113 | if (br.readBits(1) !== 0) 114 | throw new Error('Invalid reserved bit'); 115 | 116 | size_bytes = br.readBits(2); 117 | if (size_bytes === 0) 118 | return out; 119 | 120 | for (i = 0; i < size_bytes; i++) { 121 | var next_byte = br.readBits(8); 122 | if (i + 1 === size_bytes && size_bytes > 1 && next_byte === 0) 123 | throw new Error('Invalid size byte'); 124 | 125 | out.meta_block_length |= next_byte << (i * 8); 126 | } 127 | } else { 128 | for (i = 0; i < size_nibbles; ++i) { 129 | var next_nibble = br.readBits(4); 130 | if (i + 1 === size_nibbles && size_nibbles > 4 && next_nibble === 0) 131 | throw new Error('Invalid size nibble'); 132 | 133 | out.meta_block_length |= next_nibble << (i * 4); 134 | } 135 | } 136 | 137 | ++out.meta_block_length; 138 | 139 | if (!out.input_end && !out.is_metadata) { 140 | out.is_uncompressed = br.readBits(1); 141 | } 142 | 143 | return out; 144 | } 145 | 146 | /* Decodes the next Huffman code from bit-stream. */ 147 | function ReadSymbol(table, index, br) { 148 | var start_index = index; 149 | 150 | var nbits; 151 | br.fillBitWindow(); 152 | index += (br.val_ >>> br.bit_pos_) & HUFFMAN_TABLE_MASK; 153 | nbits = table[index].bits - HUFFMAN_TABLE_BITS; 154 | if (nbits > 0) { 155 | br.bit_pos_ += HUFFMAN_TABLE_BITS; 156 | index += table[index].value; 157 | index += (br.val_ >>> br.bit_pos_) & ((1 << nbits) - 1); 158 | } 159 | br.bit_pos_ += table[index].bits; 160 | return table[index].value; 161 | } 162 | 163 | function ReadHuffmanCodeLengths(code_length_code_lengths, num_symbols, code_lengths, br) { 164 | var symbol = 0; 165 | var prev_code_len = kDefaultCodeLength; 166 | var repeat = 0; 167 | var repeat_code_len = 0; 168 | var space = 32768; 169 | 170 | var table = []; 171 | for (var i = 0; i < 32; i++) 172 | table.push(new HuffmanCode(0, 0)); 173 | 174 | BrotliBuildHuffmanTable(table, 0, 5, code_length_code_lengths, CODE_LENGTH_CODES); 175 | 176 | while (symbol < num_symbols && space > 0) { 177 | var p = 0; 178 | var code_len; 179 | 180 | br.readMoreInput(); 181 | br.fillBitWindow(); 182 | p += (br.val_ >>> br.bit_pos_) & 31; 183 | br.bit_pos_ += table[p].bits; 184 | code_len = table[p].value & 0xff; 185 | if (code_len < kCodeLengthRepeatCode) { 186 | repeat = 0; 187 | code_lengths[symbol++] = code_len; 188 | if (code_len !== 0) { 189 | prev_code_len = code_len; 190 | space -= 32768 >> code_len; 191 | } 192 | } else { 193 | var extra_bits = code_len - 14; 194 | var old_repeat; 195 | var repeat_delta; 196 | var new_len = 0; 197 | if (code_len === kCodeLengthRepeatCode) { 198 | new_len = prev_code_len; 199 | } 200 | if (repeat_code_len !== new_len) { 201 | repeat = 0; 202 | repeat_code_len = new_len; 203 | } 204 | old_repeat = repeat; 205 | if (repeat > 0) { 206 | repeat -= 2; 207 | repeat <<= extra_bits; 208 | } 209 | repeat += br.readBits(extra_bits) + 3; 210 | repeat_delta = repeat - old_repeat; 211 | if (symbol + repeat_delta > num_symbols) { 212 | throw new Error('[ReadHuffmanCodeLengths] symbol + repeat_delta > num_symbols'); 213 | } 214 | 215 | for (var x = 0; x < repeat_delta; x++) 216 | code_lengths[symbol + x] = repeat_code_len; 217 | 218 | symbol += repeat_delta; 219 | 220 | if (repeat_code_len !== 0) { 221 | space -= repeat_delta << (15 - repeat_code_len); 222 | } 223 | } 224 | } 225 | if (space !== 0) { 226 | throw new Error("[ReadHuffmanCodeLengths] space = " + space); 227 | } 228 | 229 | for (; symbol < num_symbols; symbol++) 230 | code_lengths[symbol] = 0; 231 | } 232 | 233 | function ReadHuffmanCode(alphabet_size, tables, table, br) { 234 | var table_size = 0; 235 | var simple_code_or_skip; 236 | var code_lengths = new Uint8Array(alphabet_size); 237 | 238 | br.readMoreInput(); 239 | 240 | /* simple_code_or_skip is used as follows: 241 | 1 for simple code; 242 | 0 for no skipping, 2 skips 2 code lengths, 3 skips 3 code lengths */ 243 | simple_code_or_skip = br.readBits(2); 244 | if (simple_code_or_skip === 1) { 245 | /* Read symbols, codes & code lengths directly. */ 246 | var i; 247 | var max_bits_counter = alphabet_size - 1; 248 | var max_bits = 0; 249 | var symbols = new Int32Array(4); 250 | var num_symbols = br.readBits(2) + 1; 251 | while (max_bits_counter) { 252 | max_bits_counter >>= 1; 253 | ++max_bits; 254 | } 255 | 256 | for (i = 0; i < num_symbols; ++i) { 257 | symbols[i] = br.readBits(max_bits) % alphabet_size; 258 | code_lengths[symbols[i]] = 2; 259 | } 260 | code_lengths[symbols[0]] = 1; 261 | switch (num_symbols) { 262 | case 1: 263 | break; 264 | case 3: 265 | if ((symbols[0] === symbols[1]) || 266 | (symbols[0] === symbols[2]) || 267 | (symbols[1] === symbols[2])) { 268 | throw new Error('[ReadHuffmanCode] invalid symbols'); 269 | } 270 | break; 271 | case 2: 272 | if (symbols[0] === symbols[1]) { 273 | throw new Error('[ReadHuffmanCode] invalid symbols'); 274 | } 275 | 276 | code_lengths[symbols[1]] = 1; 277 | break; 278 | case 4: 279 | if ((symbols[0] === symbols[1]) || 280 | (symbols[0] === symbols[2]) || 281 | (symbols[0] === symbols[3]) || 282 | (symbols[1] === symbols[2]) || 283 | (symbols[1] === symbols[3]) || 284 | (symbols[2] === symbols[3])) { 285 | throw new Error('[ReadHuffmanCode] invalid symbols'); 286 | } 287 | 288 | if (br.readBits(1)) { 289 | code_lengths[symbols[2]] = 3; 290 | code_lengths[symbols[3]] = 3; 291 | } else { 292 | code_lengths[symbols[0]] = 2; 293 | } 294 | break; 295 | } 296 | } else { /* Decode Huffman-coded code lengths. */ 297 | var i; 298 | var code_length_code_lengths = new Uint8Array(CODE_LENGTH_CODES); 299 | var space = 32; 300 | var num_codes = 0; 301 | /* Static Huffman code for the code length code lengths */ 302 | var huff = [ 303 | new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(3, 2), 304 | new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(4, 1), 305 | new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(3, 2), 306 | new HuffmanCode(2, 0), new HuffmanCode(2, 4), new HuffmanCode(2, 3), new HuffmanCode(4, 5) 307 | ]; 308 | for (i = simple_code_or_skip; i < CODE_LENGTH_CODES && space > 0; ++i) { 309 | var code_len_idx = kCodeLengthCodeOrder[i]; 310 | var p = 0; 311 | var v; 312 | br.fillBitWindow(); 313 | p += (br.val_ >>> br.bit_pos_) & 15; 314 | br.bit_pos_ += huff[p].bits; 315 | v = huff[p].value; 316 | code_length_code_lengths[code_len_idx] = v; 317 | if (v !== 0) { 318 | space -= (32 >> v); 319 | ++num_codes; 320 | } 321 | } 322 | 323 | if (!(num_codes === 1 || space === 0)) 324 | throw new Error('[ReadHuffmanCode] invalid num_codes or space'); 325 | 326 | ReadHuffmanCodeLengths(code_length_code_lengths, alphabet_size, code_lengths, br); 327 | } 328 | 329 | table_size = BrotliBuildHuffmanTable(tables, table, HUFFMAN_TABLE_BITS, code_lengths, alphabet_size); 330 | 331 | if (table_size === 0) { 332 | throw new Error("[ReadHuffmanCode] BuildHuffmanTable failed: "); 333 | } 334 | 335 | return table_size; 336 | } 337 | 338 | function ReadBlockLength(table, index, br) { 339 | var code; 340 | var nbits; 341 | code = ReadSymbol(table, index, br); 342 | nbits = Prefix.kBlockLengthPrefixCode[code].nbits; 343 | return Prefix.kBlockLengthPrefixCode[code].offset + br.readBits(nbits); 344 | } 345 | 346 | function TranslateShortCodes(code, ringbuffer, index) { 347 | var val; 348 | if (code < NUM_DISTANCE_SHORT_CODES) { 349 | index += kDistanceShortCodeIndexOffset[code]; 350 | index &= 3; 351 | val = ringbuffer[index] + kDistanceShortCodeValueOffset[code]; 352 | } else { 353 | val = code - NUM_DISTANCE_SHORT_CODES + 1; 354 | } 355 | return val; 356 | } 357 | 358 | function MoveToFront(v, index) { 359 | var value = v[index]; 360 | var i = index; 361 | for (; i; --i) v[i] = v[i - 1]; 362 | v[0] = value; 363 | } 364 | 365 | function InverseMoveToFrontTransform(v, v_len) { 366 | var mtf = new Uint8Array(256); 367 | var i; 368 | for (i = 0; i < 256; ++i) { 369 | mtf[i] = i; 370 | } 371 | for (i = 0; i < v_len; ++i) { 372 | var index = v[i]; 373 | v[i] = mtf[index]; 374 | if (index) MoveToFront(mtf, index); 375 | } 376 | } 377 | 378 | /* Contains a collection of huffman trees with the same alphabet size. */ 379 | function HuffmanTreeGroup(alphabet_size, num_htrees) { 380 | this.alphabet_size = alphabet_size; 381 | this.num_htrees = num_htrees; 382 | this.codes = new Array(num_htrees + num_htrees * kMaxHuffmanTableSize[(alphabet_size + 31) >>> 5]); 383 | this.htrees = new Uint32Array(num_htrees); 384 | } 385 | 386 | HuffmanTreeGroup.prototype.decode = function(br) { 387 | var i; 388 | var table_size; 389 | var next = 0; 390 | for (i = 0; i < this.num_htrees; ++i) { 391 | this.htrees[i] = next; 392 | table_size = ReadHuffmanCode(this.alphabet_size, this.codes, next, br); 393 | next += table_size; 394 | } 395 | }; 396 | 397 | function DecodeContextMap(context_map_size, br) { 398 | var out = { num_htrees: null, context_map: null }; 399 | var use_rle_for_zeros; 400 | var max_run_length_prefix = 0; 401 | var table; 402 | var i; 403 | 404 | br.readMoreInput(); 405 | var num_htrees = out.num_htrees = DecodeVarLenUint8(br) + 1; 406 | 407 | var context_map = out.context_map = new Uint8Array(context_map_size); 408 | if (num_htrees <= 1) { 409 | return out; 410 | } 411 | 412 | use_rle_for_zeros = br.readBits(1); 413 | if (use_rle_for_zeros) { 414 | max_run_length_prefix = br.readBits(4) + 1; 415 | } 416 | 417 | table = []; 418 | for (i = 0; i < HUFFMAN_MAX_TABLE_SIZE; i++) { 419 | table[i] = new HuffmanCode(0, 0); 420 | } 421 | 422 | ReadHuffmanCode(num_htrees + max_run_length_prefix, table, 0, br); 423 | 424 | for (i = 0; i < context_map_size;) { 425 | var code; 426 | 427 | br.readMoreInput(); 428 | code = ReadSymbol(table, 0, br); 429 | if (code === 0) { 430 | context_map[i] = 0; 431 | ++i; 432 | } else if (code <= max_run_length_prefix) { 433 | var reps = 1 + (1 << code) + br.readBits(code); 434 | while (--reps) { 435 | if (i >= context_map_size) { 436 | throw new Error("[DecodeContextMap] i >= context_map_size"); 437 | } 438 | context_map[i] = 0; 439 | ++i; 440 | } 441 | } else { 442 | context_map[i] = code - max_run_length_prefix; 443 | ++i; 444 | } 445 | } 446 | if (br.readBits(1)) { 447 | InverseMoveToFrontTransform(context_map, context_map_size); 448 | } 449 | 450 | return out; 451 | } 452 | 453 | function DecodeBlockType(max_block_type, trees, tree_type, block_types, ringbuffers, indexes, br) { 454 | var ringbuffer = tree_type * 2; 455 | var index = tree_type; 456 | var type_code = ReadSymbol(trees, tree_type * HUFFMAN_MAX_TABLE_SIZE, br); 457 | var block_type; 458 | if (type_code === 0) { 459 | block_type = ringbuffers[ringbuffer + (indexes[index] & 1)]; 460 | } else if (type_code === 1) { 461 | block_type = ringbuffers[ringbuffer + ((indexes[index] - 1) & 1)] + 1; 462 | } else { 463 | block_type = type_code - 2; 464 | } 465 | if (block_type >= max_block_type) { 466 | block_type -= max_block_type; 467 | } 468 | block_types[tree_type] = block_type; 469 | ringbuffers[ringbuffer + (indexes[index] & 1)] = block_type; 470 | ++indexes[index]; 471 | } 472 | 473 | function CopyUncompressedBlockToOutput(output, len, pos, ringbuffer, ringbuffer_mask, br) { 474 | var rb_size = ringbuffer_mask + 1; 475 | var rb_pos = pos & ringbuffer_mask; 476 | var br_pos = br.pos_ & BrotliBitReader.IBUF_MASK; 477 | var nbytes; 478 | 479 | /* For short lengths copy byte-by-byte */ 480 | if (len < 8 || br.bit_pos_ + (len << 3) < br.bit_end_pos_) { 481 | while (len-- > 0) { 482 | br.readMoreInput(); 483 | ringbuffer[rb_pos++] = br.readBits(8); 484 | if (rb_pos === rb_size) { 485 | output.write(ringbuffer, rb_size); 486 | rb_pos = 0; 487 | } 488 | } 489 | return; 490 | } 491 | 492 | if (br.bit_end_pos_ < 32) { 493 | throw new Error('[CopyUncompressedBlockToOutput] br.bit_end_pos_ < 32'); 494 | } 495 | 496 | /* Copy remaining 0-4 bytes from br.val_ to ringbuffer. */ 497 | while (br.bit_pos_ < 32) { 498 | ringbuffer[rb_pos] = (br.val_ >>> br.bit_pos_); 499 | br.bit_pos_ += 8; 500 | ++rb_pos; 501 | --len; 502 | } 503 | 504 | /* Copy remaining bytes from br.buf_ to ringbuffer. */ 505 | nbytes = (br.bit_end_pos_ - br.bit_pos_) >> 3; 506 | if (br_pos + nbytes > BrotliBitReader.IBUF_MASK) { 507 | var tail = BrotliBitReader.IBUF_MASK + 1 - br_pos; 508 | for (var x = 0; x < tail; x++) 509 | ringbuffer[rb_pos + x] = br.buf_[br_pos + x]; 510 | 511 | nbytes -= tail; 512 | rb_pos += tail; 513 | len -= tail; 514 | br_pos = 0; 515 | } 516 | 517 | for (var x = 0; x < nbytes; x++) 518 | ringbuffer[rb_pos + x] = br.buf_[br_pos + x]; 519 | 520 | rb_pos += nbytes; 521 | len -= nbytes; 522 | 523 | /* If we wrote past the logical end of the ringbuffer, copy the tail of the 524 | ringbuffer to its beginning and flush the ringbuffer to the output. */ 525 | if (rb_pos >= rb_size) { 526 | output.write(ringbuffer, rb_size); 527 | rb_pos -= rb_size; 528 | for (var x = 0; x < rb_pos; x++) 529 | ringbuffer[x] = ringbuffer[rb_size + x]; 530 | } 531 | 532 | /* If we have more to copy than the remaining size of the ringbuffer, then we 533 | first fill the ringbuffer from the input and then flush the ringbuffer to 534 | the output */ 535 | while (rb_pos + len >= rb_size) { 536 | nbytes = rb_size - rb_pos; 537 | if (br.input_.read(ringbuffer, rb_pos, nbytes) < nbytes) { 538 | throw new Error('[CopyUncompressedBlockToOutput] not enough bytes'); 539 | } 540 | output.write(ringbuffer, rb_size); 541 | len -= nbytes; 542 | rb_pos = 0; 543 | } 544 | 545 | /* Copy straight from the input onto the ringbuffer. The ringbuffer will be 546 | flushed to the output at a later time. */ 547 | if (br.input_.read(ringbuffer, rb_pos, len) < len) { 548 | throw new Error('[CopyUncompressedBlockToOutput] not enough bytes'); 549 | } 550 | 551 | /* Restore the state of the bit reader. */ 552 | br.reset(); 553 | } 554 | 555 | /* Advances the bit reader position to the next byte boundary and verifies 556 | that any skipped bits are set to zero. */ 557 | function JumpToByteBoundary(br) { 558 | var new_bit_pos = (br.bit_pos_ + 7) & ~7; 559 | var pad_bits = br.readBits(new_bit_pos - br.bit_pos_); 560 | return pad_bits == 0; 561 | } 562 | 563 | function BrotliDecompressedSize(buffer) { 564 | var input = new BrotliInput(buffer); 565 | var br = new BrotliBitReader(input); 566 | DecodeWindowBits(br); 567 | var out = DecodeMetaBlockLength(br); 568 | return out.meta_block_length; 569 | } 570 | 571 | exports.BrotliDecompressedSize = BrotliDecompressedSize; 572 | 573 | function BrotliDecompressBuffer(buffer, output_size) { 574 | var input = new BrotliInput(buffer); 575 | 576 | if (output_size == null) { 577 | output_size = BrotliDecompressedSize(buffer); 578 | } 579 | 580 | var output_buffer = new Uint8Array(output_size); 581 | var output = new BrotliOutput(output_buffer); 582 | 583 | BrotliDecompress(input, output); 584 | 585 | if (output.pos < output.buffer.length) { 586 | output.buffer = output.buffer.subarray(0, output.pos); 587 | } 588 | 589 | return output.buffer; 590 | } 591 | 592 | exports.BrotliDecompressBuffer = BrotliDecompressBuffer; 593 | 594 | function BrotliDecompress(input, output) { 595 | var i; 596 | var pos = 0; 597 | var input_end = 0; 598 | var window_bits = 0; 599 | var max_backward_distance; 600 | var max_distance = 0; 601 | var ringbuffer_size; 602 | var ringbuffer_mask; 603 | var ringbuffer; 604 | var ringbuffer_end; 605 | /* This ring buffer holds a few past copy distances that will be used by */ 606 | /* some special distance codes. */ 607 | var dist_rb = [ 16, 15, 11, 4 ]; 608 | var dist_rb_idx = 0; 609 | /* The previous 2 bytes used for context. */ 610 | var prev_byte1 = 0; 611 | var prev_byte2 = 0; 612 | var hgroup = [new HuffmanTreeGroup(0, 0), new HuffmanTreeGroup(0, 0), new HuffmanTreeGroup(0, 0)]; 613 | var block_type_trees; 614 | var block_len_trees; 615 | var br; 616 | 617 | /* We need the slack region for the following reasons: 618 | - always doing two 8-byte copies for fast backward copying 619 | - transforms 620 | - flushing the input ringbuffer when decoding uncompressed blocks */ 621 | const kRingBufferWriteAheadSlack = 128 + BrotliBitReader.READ_SIZE; 622 | 623 | br = new BrotliBitReader(input); 624 | 625 | /* Decode window size. */ 626 | window_bits = DecodeWindowBits(br); 627 | max_backward_distance = (1 << window_bits) - 16; 628 | 629 | ringbuffer_size = 1 << window_bits; 630 | ringbuffer_mask = ringbuffer_size - 1; 631 | ringbuffer = new Uint8Array(ringbuffer_size + kRingBufferWriteAheadSlack + BrotliDictionary.maxDictionaryWordLength); 632 | ringbuffer_end = ringbuffer_size; 633 | 634 | block_type_trees = []; 635 | block_len_trees = []; 636 | for (var x = 0; x < 3 * HUFFMAN_MAX_TABLE_SIZE; x++) { 637 | block_type_trees[x] = new HuffmanCode(0, 0); 638 | block_len_trees[x] = new HuffmanCode(0, 0); 639 | } 640 | 641 | while (!input_end) { 642 | var meta_block_remaining_len = 0; 643 | var is_uncompressed; 644 | var block_length = [ 1 << 28, 1 << 28, 1 << 28 ]; 645 | var block_type = [ 0 ]; 646 | var num_block_types = [ 1, 1, 1 ]; 647 | var block_type_rb = [ 0, 1, 0, 1, 0, 1 ]; 648 | var block_type_rb_index = [ 0 ]; 649 | var distance_postfix_bits; 650 | var num_direct_distance_codes; 651 | var distance_postfix_mask; 652 | var num_distance_codes; 653 | var context_map = null; 654 | var context_modes = null; 655 | var num_literal_htrees; 656 | var dist_context_map = null; 657 | var num_dist_htrees; 658 | var context_offset = 0; 659 | var context_map_slice = null; 660 | var literal_htree_index = 0; 661 | var dist_context_offset = 0; 662 | var dist_context_map_slice = null; 663 | var dist_htree_index = 0; 664 | var context_lookup_offset1 = 0; 665 | var context_lookup_offset2 = 0; 666 | var context_mode; 667 | var htree_command; 668 | 669 | for (i = 0; i < 3; ++i) { 670 | hgroup[i].codes = null; 671 | hgroup[i].htrees = null; 672 | } 673 | 674 | br.readMoreInput(); 675 | 676 | var _out = DecodeMetaBlockLength(br); 677 | meta_block_remaining_len = _out.meta_block_length; 678 | if (pos + meta_block_remaining_len > output.buffer.length) { 679 | /* We need to grow the output buffer to fit the additional data. */ 680 | var tmp = new Uint8Array( pos + meta_block_remaining_len ); 681 | tmp.set( output.buffer ); 682 | output.buffer = tmp; 683 | } 684 | input_end = _out.input_end; 685 | is_uncompressed = _out.is_uncompressed; 686 | 687 | if (_out.is_metadata) { 688 | JumpToByteBoundary(br); 689 | 690 | for (; meta_block_remaining_len > 0; --meta_block_remaining_len) { 691 | br.readMoreInput(); 692 | /* Read one byte and ignore it. */ 693 | br.readBits(8); 694 | } 695 | 696 | continue; 697 | } 698 | 699 | if (meta_block_remaining_len === 0) { 700 | continue; 701 | } 702 | 703 | if (is_uncompressed) { 704 | br.bit_pos_ = (br.bit_pos_ + 7) & ~7; 705 | CopyUncompressedBlockToOutput(output, meta_block_remaining_len, pos, 706 | ringbuffer, ringbuffer_mask, br); 707 | pos += meta_block_remaining_len; 708 | continue; 709 | } 710 | 711 | for (i = 0; i < 3; ++i) { 712 | num_block_types[i] = DecodeVarLenUint8(br) + 1; 713 | if (num_block_types[i] >= 2) { 714 | ReadHuffmanCode(num_block_types[i] + 2, block_type_trees, i * HUFFMAN_MAX_TABLE_SIZE, br); 715 | ReadHuffmanCode(kNumBlockLengthCodes, block_len_trees, i * HUFFMAN_MAX_TABLE_SIZE, br); 716 | block_length[i] = ReadBlockLength(block_len_trees, i * HUFFMAN_MAX_TABLE_SIZE, br); 717 | block_type_rb_index[i] = 1; 718 | } 719 | } 720 | 721 | br.readMoreInput(); 722 | 723 | distance_postfix_bits = br.readBits(2); 724 | num_direct_distance_codes = NUM_DISTANCE_SHORT_CODES + (br.readBits(4) << distance_postfix_bits); 725 | distance_postfix_mask = (1 << distance_postfix_bits) - 1; 726 | num_distance_codes = (num_direct_distance_codes + (48 << distance_postfix_bits)); 727 | context_modes = new Uint8Array(num_block_types[0]); 728 | 729 | for (i = 0; i < num_block_types[0]; ++i) { 730 | br.readMoreInput(); 731 | context_modes[i] = (br.readBits(2) << 1); 732 | } 733 | 734 | var _o1 = DecodeContextMap(num_block_types[0] << kLiteralContextBits, br); 735 | num_literal_htrees = _o1.num_htrees; 736 | context_map = _o1.context_map; 737 | 738 | var _o2 = DecodeContextMap(num_block_types[2] << kDistanceContextBits, br); 739 | num_dist_htrees = _o2.num_htrees; 740 | dist_context_map = _o2.context_map; 741 | 742 | hgroup[0] = new HuffmanTreeGroup(kNumLiteralCodes, num_literal_htrees); 743 | hgroup[1] = new HuffmanTreeGroup(kNumInsertAndCopyCodes, num_block_types[1]); 744 | hgroup[2] = new HuffmanTreeGroup(num_distance_codes, num_dist_htrees); 745 | 746 | for (i = 0; i < 3; ++i) { 747 | hgroup[i].decode(br); 748 | } 749 | 750 | context_map_slice = 0; 751 | dist_context_map_slice = 0; 752 | context_mode = context_modes[block_type[0]]; 753 | context_lookup_offset1 = Context.lookupOffsets[context_mode]; 754 | context_lookup_offset2 = Context.lookupOffsets[context_mode + 1]; 755 | htree_command = hgroup[1].htrees[0]; 756 | 757 | while (meta_block_remaining_len > 0) { 758 | var cmd_code; 759 | var range_idx; 760 | var insert_code; 761 | var copy_code; 762 | var insert_length; 763 | var copy_length; 764 | var distance_code; 765 | var distance; 766 | var context; 767 | var j; 768 | var copy_dst; 769 | 770 | br.readMoreInput(); 771 | 772 | if (block_length[1] === 0) { 773 | DecodeBlockType(num_block_types[1], 774 | block_type_trees, 1, block_type, block_type_rb, 775 | block_type_rb_index, br); 776 | block_length[1] = ReadBlockLength(block_len_trees, HUFFMAN_MAX_TABLE_SIZE, br); 777 | htree_command = hgroup[1].htrees[block_type[1]]; 778 | } 779 | --block_length[1]; 780 | cmd_code = ReadSymbol(hgroup[1].codes, htree_command, br); 781 | range_idx = cmd_code >> 6; 782 | if (range_idx >= 2) { 783 | range_idx -= 2; 784 | distance_code = -1; 785 | } else { 786 | distance_code = 0; 787 | } 788 | insert_code = Prefix.kInsertRangeLut[range_idx] + ((cmd_code >> 3) & 7); 789 | copy_code = Prefix.kCopyRangeLut[range_idx] + (cmd_code & 7); 790 | insert_length = Prefix.kInsertLengthPrefixCode[insert_code].offset + 791 | br.readBits(Prefix.kInsertLengthPrefixCode[insert_code].nbits); 792 | copy_length = Prefix.kCopyLengthPrefixCode[copy_code].offset + 793 | br.readBits(Prefix.kCopyLengthPrefixCode[copy_code].nbits); 794 | prev_byte1 = ringbuffer[pos-1 & ringbuffer_mask]; 795 | prev_byte2 = ringbuffer[pos-2 & ringbuffer_mask]; 796 | for (j = 0; j < insert_length; ++j) { 797 | br.readMoreInput(); 798 | 799 | if (block_length[0] === 0) { 800 | DecodeBlockType(num_block_types[0], 801 | block_type_trees, 0, block_type, block_type_rb, 802 | block_type_rb_index, br); 803 | block_length[0] = ReadBlockLength(block_len_trees, 0, br); 804 | context_offset = block_type[0] << kLiteralContextBits; 805 | context_map_slice = context_offset; 806 | context_mode = context_modes[block_type[0]]; 807 | context_lookup_offset1 = Context.lookupOffsets[context_mode]; 808 | context_lookup_offset2 = Context.lookupOffsets[context_mode + 1]; 809 | } 810 | context = (Context.lookup[context_lookup_offset1 + prev_byte1] | 811 | Context.lookup[context_lookup_offset2 + prev_byte2]); 812 | literal_htree_index = context_map[context_map_slice + context]; 813 | --block_length[0]; 814 | prev_byte2 = prev_byte1; 815 | prev_byte1 = ReadSymbol(hgroup[0].codes, hgroup[0].htrees[literal_htree_index], br); 816 | ringbuffer[pos & ringbuffer_mask] = prev_byte1; 817 | if ((pos & ringbuffer_mask) === ringbuffer_mask) { 818 | output.write(ringbuffer, ringbuffer_size); 819 | } 820 | ++pos; 821 | } 822 | meta_block_remaining_len -= insert_length; 823 | if (meta_block_remaining_len <= 0) break; 824 | 825 | if (distance_code < 0) { 826 | var context; 827 | 828 | br.readMoreInput(); 829 | if (block_length[2] === 0) { 830 | DecodeBlockType(num_block_types[2], 831 | block_type_trees, 2, block_type, block_type_rb, 832 | block_type_rb_index, br); 833 | block_length[2] = ReadBlockLength(block_len_trees, 2 * HUFFMAN_MAX_TABLE_SIZE, br); 834 | dist_context_offset = block_type[2] << kDistanceContextBits; 835 | dist_context_map_slice = dist_context_offset; 836 | } 837 | --block_length[2]; 838 | context = (copy_length > 4 ? 3 : copy_length - 2) & 0xff; 839 | dist_htree_index = dist_context_map[dist_context_map_slice + context]; 840 | distance_code = ReadSymbol(hgroup[2].codes, hgroup[2].htrees[dist_htree_index], br); 841 | if (distance_code >= num_direct_distance_codes) { 842 | var nbits; 843 | var postfix; 844 | var offset; 845 | distance_code -= num_direct_distance_codes; 846 | postfix = distance_code & distance_postfix_mask; 847 | distance_code >>= distance_postfix_bits; 848 | nbits = (distance_code >> 1) + 1; 849 | offset = ((2 + (distance_code & 1)) << nbits) - 4; 850 | distance_code = num_direct_distance_codes + 851 | ((offset + br.readBits(nbits)) << 852 | distance_postfix_bits) + postfix; 853 | } 854 | } 855 | 856 | /* Convert the distance code to the actual distance by possibly looking */ 857 | /* up past distnaces from the ringbuffer. */ 858 | distance = TranslateShortCodes(distance_code, dist_rb, dist_rb_idx); 859 | if (distance < 0) { 860 | throw new Error('[BrotliDecompress] invalid distance'); 861 | } 862 | 863 | if (pos < max_backward_distance && 864 | max_distance !== max_backward_distance) { 865 | max_distance = pos; 866 | } else { 867 | max_distance = max_backward_distance; 868 | } 869 | 870 | copy_dst = pos & ringbuffer_mask; 871 | 872 | if (distance > max_distance) { 873 | if (copy_length >= BrotliDictionary.minDictionaryWordLength && 874 | copy_length <= BrotliDictionary.maxDictionaryWordLength) { 875 | var offset = BrotliDictionary.offsetsByLength[copy_length]; 876 | var word_id = distance - max_distance - 1; 877 | var shift = BrotliDictionary.sizeBitsByLength[copy_length]; 878 | var mask = (1 << shift) - 1; 879 | var word_idx = word_id & mask; 880 | var transform_idx = word_id >> shift; 881 | offset += word_idx * copy_length; 882 | if (transform_idx < Transform.kNumTransforms) { 883 | var len = Transform.transformDictionaryWord(ringbuffer, copy_dst, offset, copy_length, transform_idx); 884 | copy_dst += len; 885 | pos += len; 886 | meta_block_remaining_len -= len; 887 | if (copy_dst >= ringbuffer_end) { 888 | output.write(ringbuffer, ringbuffer_size); 889 | 890 | for (var _x = 0; _x < (copy_dst - ringbuffer_end); _x++) 891 | ringbuffer[_x] = ringbuffer[ringbuffer_end + _x]; 892 | } 893 | } else { 894 | throw new Error("Invalid backward reference. pos: " + pos + " distance: " + distance + 895 | " len: " + copy_length + " bytes left: " + meta_block_remaining_len); 896 | } 897 | } else { 898 | throw new Error("Invalid backward reference. pos: " + pos + " distance: " + distance + 899 | " len: " + copy_length + " bytes left: " + meta_block_remaining_len); 900 | } 901 | } else { 902 | if (distance_code > 0) { 903 | dist_rb[dist_rb_idx & 3] = distance; 904 | ++dist_rb_idx; 905 | } 906 | 907 | if (copy_length > meta_block_remaining_len) { 908 | throw new Error("Invalid backward reference. pos: " + pos + " distance: " + distance + 909 | " len: " + copy_length + " bytes left: " + meta_block_remaining_len); 910 | } 911 | 912 | for (j = 0; j < copy_length; ++j) { 913 | ringbuffer[pos & ringbuffer_mask] = ringbuffer[(pos - distance) & ringbuffer_mask]; 914 | if ((pos & ringbuffer_mask) === ringbuffer_mask) { 915 | output.write(ringbuffer, ringbuffer_size); 916 | } 917 | ++pos; 918 | --meta_block_remaining_len; 919 | } 920 | } 921 | 922 | /* When we get here, we must have inserted at least one literal and */ 923 | /* made a copy of at least length two, therefore accessing the last 2 */ 924 | /* bytes is valid. */ 925 | prev_byte1 = ringbuffer[(pos - 1) & ringbuffer_mask]; 926 | prev_byte2 = ringbuffer[(pos - 2) & ringbuffer_mask]; 927 | } 928 | 929 | /* Protect pos from overflow, wrap it around at every GB of input data */ 930 | pos &= 0x3fffffff; 931 | } 932 | 933 | output.write(ringbuffer, pos & ringbuffer_mask); 934 | } 935 | 936 | exports.BrotliDecompress = BrotliDecompress; 937 | 938 | BrotliDictionary.init(); 939 | -------------------------------------------------------------------------------- /test/testdata/empty.compressed.17: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /test/testdata/backward65536: -------------------------------------------------------------------------------- 1 | XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX --------------------------------------------------------------------------------