src/vs/base/node/zip.ts
299 LOC · 215 covered · 84 uncovered · 37 ranges · 12 concepts · 7 introducers · 12 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
zip.ts ×11
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createWriteStream, WriteStream, promises } from 'fs';
import type { FileHandle } from 'fs/promises';
import { Readable } from 'stream';
import { createCancelablePromise, Sequencer } from '../common/async.js';
import { CancellationToken } from '../common/cancellation.js';
import * as path from '../common/path.js';
import { assertReturnsDefined } from '../common/types.js';
import { Promises } from './pfs.js';
import * as nls from '../../nls.js';
import type { Entry, ZipFile } from 'yauzl';
export const CorruptZipMessage: string = 'end of central directory record signature not found';
const CORRUPT_ZIP_PATTERN = new RegExp(CorruptZipMessage);
export interface IExtractOptions {
overwrite?: boolean;
/**
* Source path within the ZIP archive. Only the files contained in this
* path will be extracted.
*/
sourcePath?: string;
}
interface IOptions {
sourcePathRegex: RegExp;
}
export type ExtractErrorType = 'CorruptZip' | 'Incomplete';
export class ExtractError extends Error {
readonly type?: ExtractErrorType;
constructor(type: ExtractErrorType | undefined, cause: Error) {
let message = cause.message;
switch (type) {
case 'CorruptZip': message = `Corrupt ZIP: ${message}`; break;
}
super(message);
this.type = type;
this.cause = cause;
}
function modeFromEntry(entry: Entry) {
const attr = entry.externalFileAttributes >> 16 || 33188;
return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
.map(mask => attr & mask)
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
}
function toExtractError(err: Error): ExtractError {
if (err instanceof ExtractError) {
return err;
}
let type: ExtractErrorType | undefined = undefined;
if (CORRUPT_ZIP_PATTERN.test(err.message)) {
type = 'CorruptZip';
}
return new ExtractError(type, err);
}
function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
const dirName = path.dirname(fileName);
const targetDirName = path.join(targetPath, dirName);
if (!targetDirName.startsWith(targetPath)) {
return Promise.reject(new Error(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName)));
}
const targetFileName = path.join(targetPath, fileName);
let istream: WriteStream;
token.onCancellationRequested(() => {
istream?.destroy();
});
return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise<void>((c, e) => {
if (token.isCancellationRequested) {
return;
}
try {
istream = createWriteStream(targetFileName, { mode });
istream.once('close', () => c());
istream.once('error', e);
stream.once('error', e);
stream.pipe(istream);
} catch (error) {
e(error);
}
}));
}
function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
zip.ts ×9
let last = createCancelablePromise<void>(() => Promise.resolve());
let extractedEntriesCount = 0;
const listener = token.onCancellationRequested(() => {
last.cancel();
zipfile.close();
return new Promise<void>((c, e) => {
const throttler = new Sequencer();
const readNextEntry = (token: CancellationToken) => {
if (token.isCancellationRequested) {
return;
}
extractedEntriesCount++;
zipfile.readEntry();
};
zipfile.once('error', e);
zipfile.once('close', () => last.then(() => {
if (token.isCancellationRequested || zipfile.entryCount === extractedEntriesCount) {
c();
} else {
e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount))));
}
zipfile.readEntry();
zipfile.on('entry', (entry: Entry) => {
if (token.isCancellationRequested) {
return;
}
if (!options.sourcePathRegex.test(entry.fileName)) {
readNextEntry(token);
return;
}
const fileName = entry.fileName.replace(options.sourcePathRegex, '');
// directory file names end with '/'
if (/\/$/.test(fileName)) {
const targetFileName = path.join(targetPath, fileName);
last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e));
return;
}
const stream = openZipStream(zipfile, entry);
const mode = modeFromEntry(entry);
last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null, e));
}).finally(() => listener.dispose());
}
const { open } = await import('yauzl');
return new Promise<ZipFile>((resolve, reject) => {
open(zipFile, lazy ? { lazyEntries: true } : undefined!, (error: Error | null, zipfile?: ZipFile) => {
if (error) {
reject(toExtractError(error));
resolve(assertReturnsDefined(zipfile));
}
});
});
}
return new Promise<Readable>((resolve, reject) => {
zipFile.openReadStream(entry, (error: Error | null, stream?: Readable) => {
if (error) {
reject(toExtractError(error));
resolve(assertReturnsDefined(stream));
}
});
});
}
export interface IFile {
path: string;
contents?: Buffer | string;
localPath?: string;
/**
* When set (and `contents` is not provided), stream at most this many bytes
* from the start of {@link localPath} instead of adding the whole file. The
* prefix is clamped to the file's current size so a rotated or truncated file
* still produces a valid entry.
*/
localPathSize?: number;
}
const { ZipFile } = await import('yazl');
const zip = new ZipFile();
const zipStream = createWriteStream(zipPath);
// Attach error/finish handling before adding entries so a read stream that
// errors while a later entry is still awaiting I/O has a listener.
const result = new Promise<string>((c, e) => {
zip.outputStream.once('error', e);
zipStream.once('error', e);
zipStream.once('finish', () => c(zipPath));
});
zip.outputStream.pipe(zipStream);
for (const f of files) {
if (f.contents !== undefined) {
zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path);
if (f.localPathSize === undefined) {
zip.addFile(f.localPath, f.path);
// yazl aborts the archive unless the streamed byte count matches the
// declared size. Derive both from a single handle so the counts match
// even if the path is rotated or truncated concurrently; skip a
// vanished file rather than failing the whole archive.
let handle: FileHandle;
try {
handle = await promises.open(f.localPath, 'r');
} catch (error) {
continue;
}
throw error;
}
try {
const size = Math.min(f.localPathSize, (await handle.stat()).size);
if (size === 0) {
readStream.once('error', error => zip.outputStream.emit('error', error));
zip.addReadStream(readStream, f.path, { size });
streamOwnsHandle = true;
}
// The read stream closes the handle when it finishes.
if (!streamOwnsHandle) {
}
}
}
}
zip.end();
return result;
}
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
zip.ts ×9
let promise = openZip(zipPath, true);
if (options.overwrite) {
promise = promise.then(zipfile => Promises.rm(targetPath).then(() => zipfile));
}
return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token));
}
return openZip(zipPath).then(zipfile => {
return new Promise<Readable>((c, e) => {
zipfile.once('error', err => e(toExtractError(err)));
zipfile.on('entry', (entry: Entry) => {
if (entry.fileName === filePath) {
openZipStream(zipfile, entry).then(stream => c(stream), err => e(err));
}
});
zipfile.once('close', () => e(new Error(nls.localize('notFound', "{0} not found inside zip.", filePath))));
});
});
}
export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
return new Promise<Buffer>((c, e) => {
const buffers: Buffer[] = [];
stream.once('error', e);
stream.on('data', (b: Buffer) => buffers.push(b));
stream.on('end', () => c(Buffer.concat(buffers)));
});
});
}