tor-browser

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

fileUtil.ts (4756B)


      1 /**
      2 * @license
      3 * Copyright 2023 Google Inc.
      4 * SPDX-License-Identifier: Apache-2.0
      5 */
      6 
      7 import type {ChildProcessByStdio} from 'node:child_process';
      8 import {spawnSync, spawn} from 'node:child_process';
      9 import {createReadStream} from 'node:fs';
     10 import {mkdir, readdir} from 'node:fs/promises';
     11 import * as path from 'node:path';
     12 import type {Readable, Transform, Writable} from 'node:stream';
     13 import {Stream} from 'node:stream';
     14 
     15 import debug from 'debug';
     16 
     17 const debugFileUtil = debug('puppeteer:browsers:fileUtil');
     18 
     19 /**
     20 * @internal
     21 */
     22 export async function unpackArchive(
     23  archivePath: string,
     24  folderPath: string,
     25 ): Promise<void> {
     26  if (!path.isAbsolute(folderPath)) {
     27    folderPath = path.resolve(process.cwd(), folderPath);
     28  }
     29  if (archivePath.endsWith('.zip')) {
     30    const extractZip = await import('extract-zip');
     31    await extractZip.default(archivePath, {dir: folderPath});
     32  } else if (archivePath.endsWith('.tar.bz2')) {
     33    await extractTar(archivePath, folderPath, 'bzip2');
     34  } else if (archivePath.endsWith('.dmg')) {
     35    await mkdir(folderPath);
     36    await installDMG(archivePath, folderPath);
     37  } else if (archivePath.endsWith('.exe')) {
     38    // Firefox on Windows.
     39    const result = spawnSync(archivePath, [`/ExtractDir=${folderPath}`], {
     40      env: {
     41        __compat_layer: 'RunAsInvoker',
     42      },
     43    });
     44    if (result.status !== 0) {
     45      throw new Error(
     46        `Failed to extract ${archivePath} to ${folderPath}: ${result.output}`,
     47      );
     48    }
     49  } else if (archivePath.endsWith('.tar.xz')) {
     50    await extractTar(archivePath, folderPath, 'xz');
     51  } else {
     52    throw new Error(`Unsupported archive format: ${archivePath}`);
     53  }
     54 }
     55 
     56 function createTransformStream(
     57  child: ChildProcessByStdio<Writable, Readable, null>,
     58 ): Transform {
     59  const stream = new Stream.Transform({
     60    transform(chunk, encoding, callback) {
     61      if (!child.stdin.write(chunk, encoding)) {
     62        child.stdin.once('drain', callback);
     63      } else {
     64        callback();
     65      }
     66    },
     67 
     68    flush(callback) {
     69      if (child.stdout.destroyed) {
     70        callback();
     71      } else {
     72        child.stdin.end();
     73        child.stdout.on('close', callback);
     74      }
     75    },
     76  });
     77 
     78  child.stdin.on('error', e => {
     79    if ('code' in e && e.code === 'EPIPE') {
     80      // finished before reading the file finished (i.e. head)
     81      stream.emit('end');
     82    } else {
     83      stream.destroy(e);
     84    }
     85  });
     86 
     87  child.stdout
     88    .on('data', data => {
     89      return stream.push(data);
     90    })
     91    .on('error', e => {
     92      return stream.destroy(e);
     93    });
     94 
     95  child.once('close', () => {
     96    return stream.end();
     97  });
     98 
     99  return stream;
    100 }
    101 
    102 /**
    103 * @internal
    104 */
    105 export const internalConstantsForTesting = {
    106  xz: 'xz',
    107  bzip2: 'bzip2',
    108 };
    109 
    110 /**
    111 * @internal
    112 */
    113 async function extractTar(
    114  tarPath: string,
    115  folderPath: string,
    116  decompressUtilityName: keyof typeof internalConstantsForTesting,
    117 ): Promise<void> {
    118  const tarFs = await import('tar-fs');
    119  return await new Promise<void>((fulfill, reject) => {
    120    function handleError(utilityName: string) {
    121      return (error: Error) => {
    122        if ('code' in error && error.code === 'ENOENT') {
    123          error = new Error(
    124            `\`${utilityName}\` utility is required to unpack this archive`,
    125            {
    126              cause: error,
    127            },
    128          );
    129        }
    130        reject(error);
    131      };
    132    }
    133    const unpack = spawn(
    134      internalConstantsForTesting[decompressUtilityName],
    135      ['-d'],
    136      {
    137        stdio: ['pipe', 'pipe', 'inherit'],
    138      },
    139    )
    140      .once('error', handleError(decompressUtilityName))
    141      .once('exit', code => {
    142        debugFileUtil(`${decompressUtilityName} exited, code=${code}`);
    143      });
    144 
    145    const tar = tarFs.extract(folderPath);
    146    tar.once('error', handleError('tar'));
    147    tar.once('finish', fulfill);
    148    createReadStream(tarPath).pipe(createTransformStream(unpack)).pipe(tar);
    149  });
    150 }
    151 
    152 /**
    153 * @internal
    154 */
    155 async function installDMG(dmgPath: string, folderPath: string): Promise<void> {
    156  const {stdout} = spawnSync(`hdiutil`, [
    157    'attach',
    158    '-nobrowse',
    159    '-noautoopen',
    160    dmgPath,
    161  ]);
    162 
    163  const volumes = stdout.toString('utf8').match(/\/Volumes\/(.*)/m);
    164  if (!volumes) {
    165    throw new Error(`Could not find volume path in ${stdout}`);
    166  }
    167  const mountPath = volumes[0]!;
    168 
    169  try {
    170    const fileNames = await readdir(mountPath);
    171    const appName = fileNames.find(item => {
    172      return typeof item === 'string' && item.endsWith('.app');
    173    });
    174    if (!appName) {
    175      throw new Error(`Cannot find app in ${mountPath}`);
    176    }
    177    const mountedPath = path.join(mountPath!, appName);
    178 
    179    spawnSync('cp', ['-R', mountedPath, folderPath]);
    180  } finally {
    181    spawnSync('hdiutil', ['detach', mountPath, '-quiet']);
    182  }
    183 }