Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/yarnpkg-core/sources/Cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export class Cache {
? () => makeMockPackage()
: () => new ZipFS(cachePath, {baseFs, readOnly: true});

const lazyFs = new LazyFS<PortablePath>(() => miscUtils.prettifySyncErrors(() => {
const lazyFs = new LazyFS(() => miscUtils.prettifySyncErrors(() => {
return zipFs = zipFsBuilder();
}, message => {
return `Failed to open the cache entry for ${structUtils.prettyLocator(this.configuration, locator)}: ${message}`;
Expand Down
108 changes: 108 additions & 0 deletions packages/yarnpkg-core/sources/FastZip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {PortablePath} from '@yarnpkg/fslib';
import fs from 'fs';

import * as tgzUtils from './tgzUtils';

export const TarFileTypeMap = {
0: `File`,
5: `Directory`,
} as const;

export type TarFileType = (typeof TarFileTypeMap)[keyof typeof TarFileTypeMap];

export function parseTarEntries(source: Buffer) {
const entries: Array<{
type: TarFileType;
path: PortablePath;
offset: number;
size: number;
mode: number;
}> = [];

let offset = 0;

while (offset < source.byteLength) {
const path = source.toString(`utf8`, offset, offset + 100).replace(/\0.*$/, ``);
if (!path) {
offset += 512;
continue;
}

const mode = parseInt(source.toString(`utf8`, offset + 100, offset + 108), 8);
const size = parseInt(source.toString(`utf8`, offset + 124, offset + 136), 8);
const type = (source[offset + 156] || `0`);
const prefix = source.toString(`utf8`, offset + 345, offset + 500).toString().replace(/\0.*$/, ``);

entries.push({
path: `${prefix}${path}` as PortablePath,
type: (TarFileTypeMap as any)[type] ?? `Unknown`,
offset: offset + 512,
size,
mode,
});

offset += 512 + Math.ceil(size / 512) * 512;
}

return entries;
}

async function testConvertToZip3rdParty(tgz: Buffer) {
const start2 = Date.now();

await tgzUtils.convertToZip3rdParty(tgz, {
compressionLevel: 0,
});

console.log(`convertToZip3rdParty: ${Date.now() - start2}ms`);
}

async function testConvertToZipCustomJs(tgz: Buffer) {
const start2 = Date.now();

await tgzUtils.convertToZipCustomJs(tgz, {
compressionLevel: 0,
});

console.log(`testConvertToZipCustomJs: ${Date.now() - start2}ms`);
}

function testConvertToZipWasm(tgz: Buffer) {
const start2 = Date.now();

tgzUtils.convertToZipWasm(tgz, {
compressionLevel: 0,
});

console.log(`testConvertToZipWasm: ${Date.now() - start2}ms`);
}

async function test() {
console.log(`Let's test the performance of the various tgz -> zip converters!`);

const files = fs.readdirSync(`.`).filter(name => name.endsWith(`.tgz`));
for (const f of files) {
const tgz = fs.readFileSync(f);

console.log();
console.log(`=== ${f} ${tgz.byteLength / 1024} KiB ===`);
console.log();

for (let t = 0; t < 10; ++t)
await testConvertToZip3rdParty(tgz);

console.log();

for (let t = 0; t < 10; ++t)
await testConvertToZipCustomJs(tgz);

console.log();

for (let t = 0; t < 10; ++t) {
testConvertToZipWasm(tgz);
}
}
}

//if (require.main === module)
// test();
63 changes: 63 additions & 0 deletions packages/yarnpkg-core/sources/crc32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
var CRC_TABLE = new Int32Array([
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419,
0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4,
0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07,
0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856,
0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3,
0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a,
0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599,
0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190,
0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e,
0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed,
0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3,
0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5,
0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010,
0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17,
0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6,
0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344,
0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a,
0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1,
0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c,
0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe,
0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31,
0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c,
0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b,
0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1,
0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7,
0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66,
0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8,
0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b,
0x2d02ef8d,
]);

export function crc32(buf: Buffer, offset: number, length: number) {
let crc = ~0;

for (let n = offset, N = offset + length; n < N; ++n)
crc = CRC_TABLE[(crc ^ buf[n]) & 0xff] ^ (crc >>> 8);

return (crc ^ -1) >>> 0;
}
107 changes: 93 additions & 14 deletions packages/yarnpkg-core/sources/tgzUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import {FakeFS, PortablePath, NodeFS, ppath, xfs, npath, constants} from '@yarnpkg/fslib';
import {ZipCompression, ZipFS} from '@yarnpkg/libzip';
import {PassThrough, Readable} from 'stream';
import tar from 'tar';
import {FakeFS, PortablePath, NodeFS, ppath, xfs, npath, constants, statUtils} from '@yarnpkg/fslib';
import {ZipCompression, ZipFS} from '@yarnpkg/libzip';
import {PassThrough, Readable} from 'stream';
import tar from 'tar';
import {promisify} from 'util';
import {gunzip, gunzipSync} from 'zlib';

import {WorkerPool} from './WorkerPool';
import * as miscUtils from './miscUtils';
import {getContent as getZipWorkerSource, ConvertToZipPayload} from './worker-zip';
import {parseTarEntries} from './FastZip';
import {WorkerPool} from './WorkerPool';
import * as miscUtils from './miscUtils';
import {getContent as getZipWorkerSource, ConvertToZipPayload} from './worker-zip';

const gunzipP = promisify(gunzip);

interface MakeArchiveFromDirectoryOptions {
baseFs?: FakeFS<PortablePath>;
Expand All @@ -32,22 +37,92 @@ export async function makeArchiveFromDirectory(source: PortablePath, {baseFs = n
}

export interface ExtractBufferOptions {
algorithm?: `3rdParty` | `CustomJs` | `Wasm`;
compressionLevel?: ZipCompression;
destination?: PortablePath;
enableWorkerPool?: boolean;
prefixPath?: PortablePath;
stripComponents?: number;
}

let workerPool: WorkerPool<ConvertToZipPayload, PortablePath> | null;

const defaultEnableWorkers = process.env.CONVERT_TO_ZIP_WORKERS === `1`;
const defaultConvertAlgorithm = process.env.CONVERT_TO_ZIP_ALGORITHM ?? `3rdParty`;

const convertAlgorithms: Record<string, (tgz: Buffer, opts: ExtractBufferOptions) => Promise<ZipFS>> = {
[`3rdParty`]: convertToZip3rdParty,
[`CustomJs`]: convertToZipCustomJs,
[`Wasm`]: convertToZipWasm,
};

export async function convertToZip(tgz: Buffer, opts: ExtractBufferOptions) {
const tmpFolder = await xfs.mktempPromise();
const tmpFile = ppath.join(tmpFolder, `archive.zip`);
if (opts.enableWorkerPool ?? defaultEnableWorkers) {
return convertToZipWorker(tgz, opts);
} else {
return convertToZipNoWorker(tgz, opts);
}
}

export async function convertToZipWorker(tgz: Buffer, opts: ExtractBufferOptions) {
const destination = opts.destination ?? ppath.join(await xfs.mktempPromise(), `archive.zip`);

workerPool ||= new WorkerPool(getZipWorkerSource());

await workerPool.run({tmpFile, tgz, opts});
await workerPool.run({tgz, opts: {...opts, destination}});

return new ZipFS(destination, {level: opts.compressionLevel});
}

export async function convertToZipNoWorker(tgz: Buffer, opts: ExtractBufferOptions) {
const algorithm = opts.algorithm ?? defaultConvertAlgorithm;

return await convertAlgorithms[algorithm](tgz, opts);
}

export async function convertToZip3rdParty(tgz: Buffer, opts: ExtractBufferOptions) {
const destination = opts.destination ?? ppath.join(await xfs.mktempPromise(), `archive.zip`);

const zipFs = new ZipFS(destination, {create: true, level: opts.compressionLevel, stats: statUtils.makeDefaultStats()});

return new ZipFS(tmpFile, {level: opts.compressionLevel});
// Buffers sent through Node are turned into regular Uint8Arrays
const tgzBuffer = Buffer.from(tgz.buffer, tgz.byteOffset, tgz.byteLength);
await extractArchiveTo(0, tgzBuffer, zipFs, opts);

zipFs.saveAndClose();

return new ZipFS(destination, {level: opts.compressionLevel});
}

export async function convertToZipCustomJs(tgz: Buffer, opts: ExtractBufferOptions) {
const destination = opts.destination ?? ppath.join(await xfs.mktempPromise(), `archive.zip`);

const zipFs = new ZipFS(destination, {create: true, level: opts.compressionLevel, stats: statUtils.makeDefaultStats()});

// Buffers sent through Node are turned into regular Uint8Arrays
const tgzBuffer = Buffer.from(tgz.buffer, tgz.byteOffset, tgz.byteLength);
await extractArchiveTo(1, tgzBuffer, zipFs, opts);

zipFs.saveAndClose();

return new ZipFS(destination, {level: opts.compressionLevel});
}

export async function convertToZipWasm(tgz: Buffer, opts: ExtractBufferOptions) {
const zip = new ZipFS({
type: `tar`,
buffer: await gunzipP(tgz),
skipComponents: opts.stripComponents,
prefixPath: opts.prefixPath,
}, {
level: opts.compressionLevel,
stats: statUtils.makeDefaultStats(),
});

const destination = opts.destination ?? ppath.join(await xfs.mktempPromise(), `archive.zip`);
await xfs.writeFilePromise(destination, zip.getBufferAndClose());

return new ZipFS(destination, {level: opts.compressionLevel});
}

async function * parseTar(tgz: Buffer) {
Expand Down Expand Up @@ -79,8 +154,8 @@ async function * parseTar(tgz: Buffer) {
}
}

export async function extractArchiveTo<T extends FakeFS<PortablePath>>(tgz: Buffer, targetFs: T, {stripComponents = 0, prefixPath = PortablePath.dot}: ExtractBufferOptions = {}): Promise<T> {
function ignore(entry: tar.ReadEntry) {
export async function extractArchiveTo<T extends FakeFS<PortablePath>>(v: number, tgz: Buffer, targetFs: T, {stripComponents = 0, prefixPath = PortablePath.dot}: ExtractBufferOptions = {}): Promise<T> {
function ignore(entry: {path: string}) {
// Disallow absolute paths; might be malicious (ex: /etc/passwd)
if (entry.path[0] === `/`)
return true;
Expand All @@ -97,7 +172,11 @@ export async function extractArchiveTo<T extends FakeFS<PortablePath>>(tgz: Buff
return false;
}

for await (const entry of parseTar(tgz)) {
const entries = v === 0
? parseTar(tgz)
: parseTarEntries(gunzipSync(tgz));

for await (const entry of entries) {
if (ignore(entry))
continue;

Expand Down
23 changes: 9 additions & 14 deletions packages/yarnpkg-core/sources/worker-zip/Worker.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import {PortablePath, statUtils} from '@yarnpkg/fslib';
import {ZipFS} from '@yarnpkg/libzip';
import {parentPort} from 'worker_threads';
import {parentPort} from 'worker_threads';

import {extractArchiveTo, ExtractBufferOptions} from '../tgzUtils';
import * as tgzUtils from '../tgzUtils';

export type ConvertToZipPayload = {tmpFile: PortablePath, tgz: Buffer | Uint8Array, opts: ExtractBufferOptions};
export type ConvertToZipPayload = {
tgz: Buffer | Uint8Array;
opts: tgzUtils.ExtractBufferOptions;
};

if (!parentPort)
throw new Error(`Assertion failed: Expected parentPort to be set`);

parentPort.on(`message`, async (data: ConvertToZipPayload) => {
const {opts, tgz, tmpFile} = data;
const {compressionLevel, ...bufferOpts} = opts;
const {opts, tgz} = data;

const zipFs = new ZipFS(tmpFile, {create: true, level: compressionLevel, stats: statUtils.makeDefaultStats()});

// Buffers sent through Node are turned into regular Uint8Arrays
const tgzBuffer = Buffer.from(tgz.buffer, tgz.byteOffset, tgz.byteLength);
await extractArchiveTo(tgzBuffer, zipFs, bufferOpts);

zipFs.saveAndClose();
await tgzUtils.convertToZipNoWorker(tgzBuffer, opts);

parentPort!.postMessage(data.tmpFile);
parentPort!.postMessage(true);
});
2 changes: 1 addition & 1 deletion packages/yarnpkg-core/sources/worker-zip/index.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/yarnpkg-libzip/artifacts/exported.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@
"_zipstruct_stat_comp_size",
"_zipstruct_stat_comp_method",

"_zip_ext_count_symlinks"
"_zip_ext_count_symlinks",
"_zip_ext_import_tar"
]
Loading