blob: 615a26e62c74fb5cb08b7d4f9f9eecd0876fe1b6 [file] [log] [blame]
Alex Rudenko8505e322021-05-25 06:01:171// 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
5const fs = require('fs');
6const crypto = require('crypto');
7const zlib = require('zlib');
8const {pipeline, Readable} = require('stream');
9
10const {promises: pfs} = fs;
11
12function sha1(data) {
13 return crypto.createHash('sha1').update(data, 'binary').digest('hex');
14}
15
16async function readTextFile(filename) {
17 return pfs.readFile(filename, 'utf8');
18}
19
20function fileExists(filename) {
21 return fs.existsSync(filename);
22}
23
24async function writeTextFile(filename, data) {
25 return pfs.writeFile(filename, data, 'utf8');
26}
27
28async function readBinaryFile(filename) {
29 return pfs.readFile(filename);
30}
31
32async 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
61async 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
73async 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
81main(process.argv).catch(err => {
82 console.log('compress_files.js failure', err);
83 process.exit(1);
84});