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.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filesrc/vs/base/node/pfs.ts · 866 LOCnode/pfs.tsextensionDownloader.ts ×10 · 52 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…extensionDownloader.ts ×1 · 2 introduced LOCextensionDownloader.ts ×…abstractExtensionManagementService.ts ×32 · 395 introduced LOCabstractExtensionManagem…zip.ts ×9 · 51 introduced LOCzip.ts ×9zip.ts ×1 · 3 introduced LOCzip.ts ×1zip.ts ×2 · 3 introduced LOCzip.ts ×2zip.ts ×1 · 5 introduced LOCzip.ts ×1zip.ts ×11 · 77 introduced LOCzip.ts ×11zip.ts ×2 · 12 introduced LOCzip.ts ×2zip.ts ×11 · 66 introduced LOCzip.ts ×11zip.test|title=Zip extract should handle directories|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/node/zip/zip.test|title=Zip extract should handle directories|occurrence=1zip.test|title=Zip extra…zip.test|title=Zip zip should clamp a prefix larger than the current file size|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/node/zip/zip.test|title=Zip zip should clamp a prefix larger than the current file size|occurrence=1zip.test|title=Zip zip s…zip.test|title=Zip zip should skip a vanished streamed source without failing the archive|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/node/zip/zip.test|title=Zip zip should skip a vanished streamed source without failing the archive|occurrence=1zip.test|title=Zip zip s…zip.test|title=Zip zip should stream a fixed prefix of a local file|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/node/zip/zip.test|title=Zip zip should stream a fixed prefix of a local file|occurrence=1zip.test|title=Zip zip s…zip.test|title=Zip zip should write an empty entry for a zero-length prefix|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/base/test/node/zip/zip.test|title=Zip zip should write an empty entry for a zero-length prefix|occurrence=1zip.test|title=Zip zip s…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for an unsigned extension even when signature verification throws error|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully for unsigned extension|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails to execute|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification fails|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled because the module is not loaded|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification is disabled by options|occurrence=1extensionDownloader.test…extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/extensionManagement/test/node/extensionDownloader.test|title=ExtensionDownloader Tests download completes successfully if verification succeeds|occurrence=1extensionDownloader.test…Focused file · src/vs/base/node/zip.ts · 299 LOCnode/zip.ts

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.

1 > /*--------------------------------------------------------------------------------------------- zip.ts ×11
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { createWriteStream, WriteStream, promises } from 'fs';
7 > import type { FileHandle } from 'fs/promises';
8 > import { Readable } from 'stream';
9 > import { createCancelablePromise, Sequencer } from '../common/async.js';
10 > import { CancellationToken } from '../common/cancellation.js';
11 > import * as path from '../common/path.js';
12 > import { assertReturnsDefined } from '../common/types.js';
13 > import { Promises } from './pfs.js';
14 > import * as nls from '../../nls.js';
15 > import type { Entry, ZipFile } from 'yauzl';
16 >
17 > export const CorruptZipMessage: string = 'end of central directory record signature not found';
18 > const CORRUPT_ZIP_PATTERN = new RegExp(CorruptZipMessage);
19 >
20 > export interface IExtractOptions {
21 > overwrite?: boolean;
22 >
23 > /**
24 > * Source path within the ZIP archive. Only the files contained in this
25 > * path will be extracted.
26 > */
27 > sourcePath?: string;
28 > }
29 >
30 > interface IOptions {
31 > sourcePathRegex: RegExp;
32 > }
33 >
34 > export type ExtractErrorType = 'CorruptZip' | 'Incomplete';
35 >
36 > export class ExtractError extends Error {
37 >
38 > readonly type?: ExtractErrorType;
39 >
40 > constructor(type: ExtractErrorType | undefined, cause: Error) {
41 let message = cause.message;
42
43 switch (type) {
44 case 'CorruptZip': message = `Corrupt ZIP: ${message}`; break;
45 }
46
47 super(message);
48 this.type = type;
49 this.cause = cause;
50 }
51 > } zip.ts ×11
52 >
53 function modeFromEntry(entry: Entry) {
54 const attr = entry.externalFileAttributes >> 16 || 33188;
55
56 return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
57 .map(mask => attr & mask)
58 .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
59 }
61 function toExtractError(err: Error): ExtractError {
62 if (err instanceof ExtractError) {
63 return err;
64 }
65
66 let type: ExtractErrorType | undefined = undefined;
67
68 if (CORRUPT_ZIP_PATTERN.test(err.message)) {
69 type = 'CorruptZip';
70 }
71
72 return new ExtractError(type, err);
73 }
75 function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
76 const dirName = path.dirname(fileName);
77 const targetDirName = path.join(targetPath, dirName);
78 if (!targetDirName.startsWith(targetPath)) {
79 return Promise.reject(new Error(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName)));
80 }
81 const targetFileName = path.join(targetPath, fileName);
82
83 let istream: WriteStream;
84
85 token.onCancellationRequested(() => {
86 istream?.destroy();
87 });
88
89 return Promise.resolve(promises.mkdir(targetDirName, { recursive: true })).then(() => new Promise<void>((c, e) => {
90 if (token.isCancellationRequested) {
91 return;
92 }
93
94 try {
95 istream = createWriteStream(targetFileName, { mode });
96 istream.once('close', () => c());
97 istream.once('error', e);
98 stream.once('error', e);
99 stream.pipe(istream);
100 } catch (error) {
101 e(error);
102 }
103 }));
104 }
105 > zip.ts ×11
106 > function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> { zip.ts ×9
107 > let last = createCancelablePromise<void>(() => Promise.resolve());
108 > let extractedEntriesCount = 0;
109 >
110 > const listener = token.onCancellationRequested(() => {
111 last.cancel();
112 zipfile.close();
113 > }); zip.ts ×9
114 >
115 > return new Promise<void>((c, e) => {
116 > const throttler = new Sequencer();
117 >
118 > const readNextEntry = (token: CancellationToken) => {
119 > if (token.isCancellationRequested) {
120 return;
121 }
122 > zip.ts ×9
123 > extractedEntriesCount++;
124 > zipfile.readEntry();
125 > };
126 >
127 > zipfile.once('error', e);
128 > zipfile.once('close', () => last.then(() => {
129 > if (token.isCancellationRequested || zipfile.entryCount === extractedEntriesCount) {
130 > c();
131 > } else {
132 e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount))));
133 }
134 > }, e)); zip.ts ×9
135 > zipfile.readEntry();
136 > zipfile.on('entry', (entry: Entry) => {
137 >
138 > if (token.isCancellationRequested) {
139 return;
140 }
141 > zip.ts ×9
142 > if (!options.sourcePathRegex.test(entry.fileName)) {
143 readNextEntry(token);
144 return;
145 }
146 > zip.ts ×9
147 > const fileName = entry.fileName.replace(options.sourcePathRegex, '');
148 >
149 > // directory file names end with '/'
150 > if (/\/$/.test(fileName)) {
151 > const targetFileName = path.join(targetPath, fileName);
152 > last = createCancelablePromise(token => promises.mkdir(targetFileName, { recursive: true }).then(() => readNextEntry(token)).then(undefined, e));
153 > return;
154 > }
155
156 const stream = openZipStream(zipfile, entry);
157 const mode = modeFromEntry(entry);
158
159 last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null, e));
160 > }); zip.ts ×9
161 > }).finally(() => listener.dispose());
162 > }
163 > zip.ts ×11
164 > async function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> { zip.ts ×2
165 > const { open } = await import('yauzl');
166 >
167 > return new Promise<ZipFile>((resolve, reject) => {
168 > open(zipFile, lazy ? { lazyEntries: true } : undefined!, (error: Error | null, zipfile?: ZipFile) => {
169 > if (error) {
170 reject(toExtractError(error));
171 > } else { zip.ts ×2
172 > resolve(assertReturnsDefined(zipfile));
173 > }
174 > });
175 > });
176 > }
177 > zip.ts ×11
178 > function openZipStream(zipFile: ZipFile, entry: Entry): Promise<Readable> { zip.ts ×11
179 > return new Promise<Readable>((resolve, reject) => {
180 > zipFile.openReadStream(entry, (error: Error | null, stream?: Readable) => {
181 > if (error) {
182 reject(toExtractError(error));
183 > } else { zip.ts ×11
184 > resolve(assertReturnsDefined(stream));
185 > }
186 > });
187 > });
188 > }
189 > zip.ts ×11
190 > export interface IFile {
191 > path: string;
192 > contents?: Buffer | string;
193 > localPath?: string;
194 > /**
195 > * When set (and `contents` is not provided), stream at most this many bytes
196 > * from the start of {@link localPath} instead of adding the whole file. The
197 > * prefix is clamped to the file's current size so a rotated or truncated file
198 > * still produces a valid entry.
199 > */
200 > localPathSize?: number;
201 > }
202 >
203 > export async function zip(zipPath: string, files: IFile[]): Promise<string> { zip.ts ×11
204 > const { ZipFile } = await import('yazl');
205 >
206 > const zip = new ZipFile();
207 > const zipStream = createWriteStream(zipPath);
208 >
209 > // Attach error/finish handling before adding entries so a read stream that
210 > // errors while a later entry is still awaiting I/O has a listener.
211 > const result = new Promise<string>((c, e) => {
212 > zip.outputStream.once('error', e);
213 > zipStream.once('error', e);
214 > zipStream.once('finish', () => c(zipPath));
215 > });
216 > zip.outputStream.pipe(zipStream);
217 >
218 > for (const f of files) {
219 > if (f.contents !== undefined) {
220 zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path);
221 > } else if (f.localPath) { zip.ts ×11
222 > if (f.localPathSize === undefined) {
223 zip.addFile(f.localPath, f.path);
224 > } else { zip.ts ×11
225 > // yazl aborts the archive unless the streamed byte count matches the
226 > // declared size. Derive both from a single handle so the counts match
227 > // even if the path is rotated or truncated concurrently; skip a
228 > // vanished file rather than failing the whole archive.
229 > let handle: FileHandle;
230 > try {
231 > handle = await promises.open(f.localPath, 'r');
232 > } catch (error) {
233 > if ((error as NodeJS.ErrnoException).code === 'ENOENT') { zip.ts ×1
234 > continue;
235 > }
236 throw error;
237 }
238 > let streamOwnsHandle = false; zip.ts ×11
239 > try {
240 > const size = Math.min(f.localPathSize, (await handle.stat()).size);
241 > if (size === 0) {
242 > zip.addBuffer(Buffer.alloc(0), f.path); zip.ts ×2
243 > } else { zip.ts ×11
244 > const readStream = handle.createReadStream({ start: 0, end: size - 1 }); zip.ts ×1
245 > readStream.once('error', error => zip.outputStream.emit('error', error));
246 > zip.addReadStream(readStream, f.path, { size });
247 > streamOwnsHandle = true;
248 > }
249 > } finally { zip.ts ×11
250 > // The read stream closes the handle when it finishes.
251 > if (!streamOwnsHandle) {
252 > await handle.close(); zip.ts ×2
253 > }
254 > } zip.ts ×11
255 > }
256 > }
257 > }
258 > zip.end();
259 >
260 > return result;
261 > }
262 > zip.ts ×11
263 > export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
264 > const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : ''); zip.ts ×9
265 >
266 > let promise = openZip(zipPath, true);
267 >
268 > if (options.overwrite) {
269 promise = promise.then(zipfile => Promises.rm(targetPath).then(() => zipfile));
270 }
271 > zip.ts ×9
272 > return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token));
273 > }
274 > zip.ts ×11
275 > function read(zipPath: string, filePath: string): Promise<Readable> { zip.ts ×11
276 > return openZip(zipPath).then(zipfile => {
277 > return new Promise<Readable>((c, e) => {
278 > zipfile.once('error', err => e(toExtractError(err)));
279 > zipfile.on('entry', (entry: Entry) => {
280 > if (entry.fileName === filePath) {
281 > openZipStream(zipfile, entry).then(stream => c(stream), err => e(err));
282 > }
283 > });
284 >
285 > zipfile.once('close', () => e(new Error(nls.localize('notFound', "{0} not found inside zip.", filePath))));
286 > });
287 > });
288 > }
289 > zip.ts ×11
290 > export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
291 > return read(zipPath, filePath).then(stream => { zip.ts ×11
292 > return new Promise<Buffer>((c, e) => {
293 > const buffers: Buffer[] = [];
294 > stream.once('error', e);
295 > stream.on('data', (b: Buffer) => buffers.push(b));
296 > stream.on('end', () => c(Buffer.concat(buffers)));
297 > });
298 > });
299 > }