tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

gen_cache.ts (8481B)


      1 import * as fs from 'fs';
      2 import * as path from 'path';
      3 import * as process from 'process';
      4 
      5 import { Cacheable, dataCache, setIsBuildingDataCache } from '../framework/data_cache.js';
      6 import { crc32, toHexString } from '../util/crc32.js';
      7 import { parseImports } from '../util/parse_imports.js';
      8 
      9 function usage(rc: number): void {
     10  console.error(`Usage: tools/gen_cache [options] [SUITE_DIRS...]
     11 
     12 For each suite in SUITE_DIRS, pre-compute data that is expensive to generate
     13 at runtime and store it under 'src/resources/cache'. If the data file is found
     14 then the DataCache will load this instead of building the expensive data at CTS
     15 runtime.
     16 Note: Due to differences in gzip compression, different versions of node can
     17 produce radically different binary cache files. gen_cache uses the hashes of the
     18 source files to determine whether a cache file is 'up to date'. This is faster
     19 and does not depend on the compressed output.
     20 
     21 Options:
     22  --help          Print this message and exit.
     23  --list          Print the list of output files without writing them.
     24  --force         Rebuild cache even if they're up to date
     25  --validate      Check the cache is up to date
     26  --verbose       Print each action taken.
     27 `);
     28  process.exit(rc);
     29 }
     30 
     31 // Where the cache is generated
     32 const outDir = 'src/resources/cache';
     33 
     34 let forceRebuild = false;
     35 let mode: 'emit' | 'list' | 'validate' = 'emit';
     36 let verbose = false;
     37 
     38 const nonFlagsArgs: string[] = [];
     39 
     40 for (const arg of process.argv) {
     41  if (arg.startsWith('-')) {
     42    switch (arg) {
     43      case '--list': {
     44        mode = 'list';
     45        break;
     46      }
     47      case '--help': {
     48        usage(0);
     49        break;
     50      }
     51      case '--force': {
     52        forceRebuild = true;
     53        break;
     54      }
     55      case '--verbose': {
     56        verbose = true;
     57        break;
     58      }
     59      case '--validate': {
     60        mode = 'validate';
     61        break;
     62      }
     63      default: {
     64        console.log('unrecognized flag: ', arg);
     65        usage(1);
     66      }
     67    }
     68  } else {
     69    nonFlagsArgs.push(arg);
     70  }
     71 }
     72 
     73 if (nonFlagsArgs.length < 3) {
     74  usage(0);
     75 }
     76 
     77 dataCache.setStore({
     78  load: (path: string) => {
     79    return new Promise<Uint8Array>((resolve, reject) => {
     80      fs.readFile(`data/${path}`, (err, data) => {
     81        if (err !== null) {
     82          reject(err.message);
     83        } else {
     84          resolve(data);
     85        }
     86      });
     87    });
     88  },
     89 });
     90 setIsBuildingDataCache();
     91 
     92 const cacheFileSuffix = __filename.endsWith('.ts') ? '.cache.ts' : '.cache.js';
     93 
     94 /**
     95 * @returns a list of all the files under 'dir' that has the given extension
     96 * @param dir the directory to search
     97 * @param ext the extension of the files to find
     98 */
     99 function glob(dir: string, ext: string) {
    100  const files: string[] = [];
    101  for (const file of fs.readdirSync(dir)) {
    102    const path = `${dir}/${file}`;
    103    if (fs.statSync(path).isDirectory()) {
    104      for (const child of glob(path, ext)) {
    105        files.push(`${file}/${child}`);
    106      }
    107    }
    108 
    109    if (path.endsWith(ext) && fs.statSync(path).isFile()) {
    110      files.push(file);
    111    }
    112  }
    113  return files;
    114 }
    115 
    116 /**
    117 * Exception type thrown by SourceHasher.hashFile() when a file annotated with
    118 * MUST_NOT_BE_IMPORTED_BY_DATA_CACHE is transitively imported by a .cache.ts file.
    119 */
    120 class InvalidImportException {
    121  constructor(path: string) {
    122    this.stack = [path];
    123  }
    124  toString(): string {
    125    return `invalid transitive import for cache:\n  ${this.stack.join('\n  ')}`;
    126  }
    127  readonly stack: string[];
    128 }
    129 /**
    130 * SourceHasher is a utility for producing a hash of a source .ts file and its imported source files.
    131 */
    132 class SourceHasher {
    133  /**
    134   * @param path the source file path
    135   * @returns a hash of the source file and all of its imported dependencies.
    136   */
    137  public hashOf(path: string) {
    138    this.u32Array[0] = this.hashFile(path);
    139    return this.u32Array[0].toString(16);
    140  }
    141 
    142  hashFile(path: string): number {
    143    if (!fs.existsSync(path) && path.endsWith('.js')) {
    144      path = path.substring(0, path.length - 2) + 'ts';
    145    }
    146 
    147    const cached = this.hashes.get(path);
    148    if (cached !== undefined) {
    149      return cached;
    150    }
    151 
    152    this.hashes.set(path, 0); // Store a zero hash to handle cyclic imports
    153 
    154    const content = fs.readFileSync(path, { encoding: 'utf-8' });
    155    const normalized = content.replace('\r\n', '\n');
    156    let hash = crc32(normalized);
    157    for (const importPath of parseImports(path, normalized)) {
    158      try {
    159        const importHash = this.hashFile(importPath);
    160        hash = this.hashCombine(hash, importHash);
    161      } catch (ex) {
    162        if (ex instanceof InvalidImportException) {
    163          ex.stack.push(path);
    164          throw ex;
    165        }
    166      }
    167    }
    168 
    169    if (content.includes('MUST_NOT_BE_IMPORTED_BY_DATA_CACHE')) {
    170      throw new InvalidImportException(path);
    171    }
    172 
    173    this.hashes.set(path, hash);
    174    return hash;
    175  }
    176 
    177  /** Simple non-cryptographic hash combiner */
    178  hashCombine(a: number, b: number): number {
    179    return crc32(`${toHexString(a)} ${toHexString(b)}`);
    180  }
    181 
    182  private hashes = new Map<string, number>();
    183  private u32Array = new Uint32Array(1);
    184 }
    185 
    186 void (async () => {
    187  const suiteDirs = nonFlagsArgs.slice(2); // skip <exe> <js>
    188  for (const suiteDir of suiteDirs) {
    189    await build(suiteDir);
    190  }
    191 })();
    192 
    193 async function build(suiteDir: string) {
    194  if (!fs.existsSync(suiteDir)) {
    195    console.error(`Could not find ${suiteDir}`);
    196    process.exit(1);
    197  }
    198 
    199  // Load  hashes.json
    200  const fileHashJsonPath = `${outDir}/hashes.json`;
    201  let fileHashes: Record<string, string> = {};
    202  if (fs.existsSync(fileHashJsonPath)) {
    203    const json = fs.readFileSync(fileHashJsonPath, { encoding: 'utf8' });
    204    fileHashes = JSON.parse(json);
    205  }
    206 
    207  // Crawl files and convert paths to be POSIX-style, relative to suiteDir.
    208  const filesToEnumerate = glob(suiteDir, cacheFileSuffix)
    209    .map(p => `${suiteDir}/${p}`)
    210    .sort();
    211 
    212  const fileHasher = new SourceHasher();
    213  const cacheablePathToTS = new Map<string, string>();
    214  const errors: Array<string> = [];
    215 
    216  for (const file of filesToEnumerate) {
    217    const pathWithoutExtension = file.substring(0, file.length - 3);
    218    const mod = await import(`../../../${pathWithoutExtension}.js`);
    219    if (mod.d?.serialize !== undefined) {
    220      const cacheable = mod.d as Cacheable<unknown>;
    221 
    222      {
    223        // Check for collisions
    224        const existing = cacheablePathToTS.get(cacheable.path);
    225        if (existing !== undefined) {
    226          errors.push(
    227            `'${cacheable.path}' is emitted by both:
    228    '${existing}'
    229 and
    230    '${file}'`
    231          );
    232        }
    233        cacheablePathToTS.set(cacheable.path, file);
    234      }
    235 
    236      const outPath = `${outDir}/${cacheable.path}`;
    237      const fileHash = fileHasher.hashOf(file);
    238 
    239      switch (mode) {
    240        case 'emit': {
    241          if (!forceRebuild && fileHashes[cacheable.path] === fileHash) {
    242            if (verbose) {
    243              console.log(`'${outPath}' is up to date`);
    244            }
    245            continue;
    246          }
    247          console.log(`building '${outPath}'`);
    248          const data = await cacheable.build();
    249          const serialized = cacheable.serialize(data);
    250          fs.mkdirSync(path.dirname(outPath), { recursive: true });
    251          fs.writeFileSync(outPath, serialized, 'binary');
    252          fileHashes[cacheable.path] = fileHash;
    253          break;
    254        }
    255        case 'list': {
    256          console.log(outPath);
    257          break;
    258        }
    259        case 'validate': {
    260          if (fileHashes[cacheable.path] !== fileHash) {
    261            errors.push(
    262              `'${outPath}' needs rebuilding. Generate with 'npx grunt run:generate-cache'`
    263            );
    264          } else if (verbose) {
    265            console.log(`'${outPath}' is up to date`);
    266          }
    267        }
    268      }
    269    }
    270  }
    271 
    272  // Check that there aren't stale files in the cache directory
    273  for (const file of glob(outDir, '.bin')) {
    274    if (cacheablePathToTS.get(file) === undefined) {
    275      switch (mode) {
    276        case 'emit':
    277          fs.rmSync(file);
    278          break;
    279        case 'validate':
    280          errors.push(
    281            `cache file '${outDir}/${file}' is no longer generated. Remove with 'npx grunt run:generate-cache'`
    282          );
    283          break;
    284      }
    285    }
    286  }
    287 
    288  // Update hashes.json
    289  if (mode === 'emit') {
    290    const json = JSON.stringify(fileHashes, undefined, '  ');
    291    fs.writeFileSync(fileHashJsonPath, json, { encoding: 'utf8' });
    292  }
    293 
    294  if (errors.length > 0) {
    295    for (const error of errors) {
    296      console.error(error);
    297    }
    298    process.exit(1);
    299  }
    300 }