Alex Rudenko | 8505e32 | 2021-05-25 06:01:17 | [diff] [blame] | 1 | // Copyright 2021 The Chromium Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | const fs = require('fs'); |
| 6 | const crypto = require('crypto'); |
| 7 | const zlib = require('zlib'); |
| 8 | const {pipeline, Readable} = require('stream'); |
| 9 | |
| 10 | const {promises: pfs} = fs; |
| 11 | |
| 12 | function sha1(data) { |
| 13 | return crypto.createHash('sha1').update(data, 'binary').digest('hex'); |
| 14 | } |
| 15 | |
| 16 | async function readTextFile(filename) { |
| 17 | return pfs.readFile(filename, 'utf8'); |
| 18 | } |
| 19 | |
| 20 | function fileExists(filename) { |
| 21 | return fs.existsSync(filename); |
| 22 | } |
| 23 | |
| 24 | async function writeTextFile(filename, data) { |
| 25 | return pfs.writeFile(filename, data, 'utf8'); |
| 26 | } |
| 27 | |
| 28 | async function readBinaryFile(filename) { |
| 29 | return pfs.readFile(filename); |
| 30 | } |
| 31 | |
| 32 | async function brotli(sourceData, compressedFilename) { |
| 33 | const sizeBytes = sourceData.length; |
| 34 | |
| 35 | // This replicates the following compression logic: |
| 36 | // https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/grit/node/base.py;l=649;drc=84ef659584d3beb83b44cc168d02244dbd6b8f87 |
| 37 | const array = new BigUint64Array(1); |
| 38 | // The length of the uncompressed data as 8 bytes little-endian. |
| 39 | new DataView(array.buffer).setBigUint64(0, BigInt(sizeBytes), true); |
| 40 | |
| 41 | // BROTLI_CONST is prepended to brotli compressed data in order to |
| 42 | // easily check if a resource has been brotli compressed. |
| 43 | // It should be kept in sync with https://source.chromium.org/chromium/chromium/src/+/main:tools/grit/grit/constants.py;l=25;drc=84ef659584d3beb83b44cc168d02244dbd6b8f87. |
| 44 | const brotliConst = new Uint8Array(2); |
| 45 | brotliConst[0] = 0x1E; |
| 46 | brotliConst[1] = 0x9B; |
| 47 | |
| 48 | // The length of the uncompressed data is also appended to the start, |
| 49 | // truncated to 6 bytes, little-endian. |
| 50 | const sizeHeader = new Uint8Array(array.buffer).slice(0, 6).buffer; |
| 51 | const output = fs.createWriteStream(compressedFilename); |
| 52 | output.write(Buffer.from(brotliConst)); |
| 53 | output.write(Buffer.from(sizeHeader)); |
| 54 | return new Promise((resolve, reject) => { |
| 55 | pipeline(Readable.from(sourceData), zlib.createBrotliCompress(), output, err => { |
| 56 | return err ? reject(err) : resolve(); |
| 57 | }); |
| 58 | }); |
| 59 | } |
| 60 | |
| 61 | async function compressFile(filename) { |
| 62 | const compressedFilename = filename + '.compressed'; |
| 63 | const hashFilename = filename + '.hash'; |
| 64 | const prevHash = fileExists(hashFilename) ? await readTextFile(hashFilename) : ''; |
| 65 | const sourceData = await readBinaryFile(filename); |
| 66 | const currHash = sha1(sourceData); |
| 67 | if (prevHash !== currHash) { |
| 68 | await writeTextFile(hashFilename, currHash); |
| 69 | await brotli(sourceData, compressedFilename); |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | async function main(argv) { |
| 74 | const fileListPosition = argv.indexOf('--file_list'); |
| 75 | const fileList = argv[fileListPosition + 1]; |
| 76 | const fileListContents = await readTextFile(fileList); |
| 77 | const files = fileListContents.split(' '); |
| 78 | await Promise.all(files.map(filename => filename.trim()).map(compressFile)); |
| 79 | } |
| 80 | |
| 81 | main(process.argv).catch(err => { |
| 82 | console.log('compress_files.js failure', err); |
| 83 | process.exit(1); |
| 84 | }); |