diff --git a/lib/tar/archive.js b/lib/tar/archive.js new file mode 100644 index 0000000..856b074 --- /dev/null +++ b/lib/tar/archive.js @@ -0,0 +1,281 @@ +'use strict'; + +const { createTar } = require('nanotar'); + +const TAR_BLOCK_SIZE = 512; +const TAR_END_BLOCK_SIZE = 1024; + +function normalizeData(data) { + if (data === undefined || data === null) return undefined; + if (Buffer.isBuffer(data)) return data; + if (data instanceof Uint8Array) return Buffer.from(data); + if (data instanceof ArrayBuffer) return Buffer.from(data); + if (typeof data === 'string') return Buffer.from(data); + return Buffer.from(data); +} + +function normalizeMode(mode, defaultMode) { + if (mode === undefined || mode === null || mode === '') return defaultMode; + if (typeof mode === 'number') return (mode & 0o7777).toString(8); + return String(mode).replace(/^0+/, '') || '0'; +} + +function isSymlinkEntry(entry) { + return entry.type === 'symlink' || entry.type === 'symbolicLink'; +} + +function normalizeEntry(entry) { + const data = normalizeData(entry.data); + const isDirectory = entry.type === 'directory'; + const mode = normalizeMode(entry.attrs && entry.attrs.mode, isDirectory ? '775' : '664'); + const mtime = entry.attrs && entry.attrs.mtime; + return { + name: isDirectory && !entry.name.endsWith('/') ? `${entry.name}/` : entry.name, + data, + linkname: entry.linkname, + type: isDirectory ? 'directory' : (isSymlinkEntry(entry) ? 'symlink' : 'file'), + attrs: { + mode, + mtime, + uid: entry.attrs && entry.attrs.uid, + gid: entry.attrs && entry.attrs.gid, + user: entry.attrs && entry.attrs.user, + group: entry.attrs && entry.attrs.group, + }, + }; +} + +function canUseNanotar(entries) { + return entries.every(entry => !isSymlinkEntry(entry)); +} + +function createTarBuffer(entries) { + const normalizedEntries = entries.map(normalizeEntry); + + if (canUseNanotar(normalizedEntries)) { + return Buffer.from(createTar(normalizedEntries.map(entry => ({ + name: entry.name, + data: entry.type === 'directory' ? undefined : entry.data, + attrs: entry.attrs, + })))); + } + + const chunks = []; + for (const entry of normalizedEntries) { + const data = entry.type === 'file' ? (entry.data || Buffer.alloc(0)) : Buffer.alloc(0); + chunks.push(createHeader(entry, data.length)); + if (data.length > 0) { + chunks.push(data); + const paddingSize = (TAR_BLOCK_SIZE - (data.length % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE; + if (paddingSize > 0) chunks.push(Buffer.alloc(paddingSize)); + } + } + chunks.push(Buffer.alloc(TAR_END_BLOCK_SIZE)); + return Buffer.concat(chunks); +} + +function createHeader(entry, size) { + const header = Buffer.alloc(TAR_BLOCK_SIZE); + const pathParts = splitTarPath(entry.name); + writeString(header, pathParts.name, 0, 100); + writeOctal(header, entry.attrs.mode, 100, 8); + writeOctal(header, entry.attrs.uid || 0, 108, 8); + writeOctal(header, entry.attrs.gid || 0, 116, 8); + writeOctal(header, size, 124, 12); + writeOctal(header, normalizeMtime(entry.attrs.mtime), 136, 12); + writeString(header, entry.type === 'directory' ? '5' : (entry.type === 'symlink' ? '2' : '0'), 156, 1); + writeString(header, entry.linkname || '', 157, 100); + writeString(header, 'ustar', 257, 6); + writeString(header, '00', 263, 2); + writeString(header, entry.attrs.user || '', 265, 32); + writeString(header, entry.attrs.group || '', 297, 32); + writeString(header, pathParts.prefix, 345, 155); + header.fill(0x20, 148, 156); + let checksum = 0; + for (const byte of header) checksum += byte; + writeChecksum(header, checksum, 148, 8); + return header; +} + +function splitTarPath(name) { + if (Buffer.byteLength(name) <= 100) { + return { name, prefix: '' }; + } + + const segments = name.split('/'); + for (let i = segments.length - 1; i > 0; i--) { + const prefix = segments.slice(0, i).join('/'); + const tail = segments.slice(i).join('/'); + if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(tail) <= 100) { + return { name: tail, prefix }; + } + } + + throw new Error(`TAR entry path is too long: ${name}`); +} + +function writeString(buffer, value, offset, length) { + const stringBuffer = Buffer.from(String(value)); + stringBuffer.copy(buffer, offset, 0, Math.min(stringBuffer.length, length)); +} + +function writeOctal(buffer, value, offset, length) { + const stringValue = typeof value === 'string' ? value : Number(value || 0).toString(8); + const padded = stringValue.padStart(length - 1, '0'); + writeString(buffer, padded, offset, length - 1); +} + +function writeChecksum(buffer, value, offset, length) { + const stringValue = Number(value || 0).toString(8).padStart(length - 2, '0'); + writeString(buffer, stringValue, offset, length - 2); + buffer[offset + length - 2] = 0; + buffer[offset + length - 1] = 0x20; +} + +function normalizeMtime(mtime) { + if (mtime === undefined || mtime === null) return Math.floor(Date.now() / 1000); + const numericMtime = Number(mtime); + if (!Number.isFinite(numericMtime)) return Math.floor(Date.now() / 1000); + return numericMtime > 1e12 ? Math.floor(numericMtime / 1000) : Math.floor(numericMtime); +} + +function parseTarBuffer(buffer) { + if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer); + + const entries = []; + let offset = 0; + let nextLongName; + let nextLongLinkname; + let nextExtendedHeader; + let globalExtendedHeader = {}; + + while (offset + TAR_BLOCK_SIZE <= buffer.length) { + const header = buffer.subarray(offset, offset + TAR_BLOCK_SIZE); + if (isZeroBlock(header)) break; + + const size = readOctal(header, 124, 12); + const dataOffset = offset + TAR_BLOCK_SIZE; + const dataEnd = dataOffset + size; + const blockSize = Math.ceil(size / TAR_BLOCK_SIZE) * TAR_BLOCK_SIZE; + const typeflag = readString(header, 156, 1) || '0'; + const prefix = readString(header, 345, 155); + let name = readString(header, 0, 100); + if (prefix) name = `${prefix}/${name}`; + let linkname = readString(header, 157, 100); + const data = buffer.subarray(dataOffset, dataEnd); + + if (typeflag === 'L') { + nextLongName = readBufferString(data); + offset = dataOffset + blockSize; + continue; + } + + if (typeflag === 'K') { + nextLongLinkname = readBufferString(data); + offset = dataOffset + blockSize; + continue; + } + + if (typeflag === 'x') { + nextExtendedHeader = parsePaxHeaders(data); + offset = dataOffset + blockSize; + continue; + } + + if (typeflag === 'g') { + globalExtendedHeader = Object.assign(globalExtendedHeader, parsePaxHeaders(data)); + offset = dataOffset + blockSize; + continue; + } + + const effectiveHeaders = Object.assign({}, globalExtendedHeader, nextExtendedHeader); + if (nextLongName) { + name = nextLongName; + nextLongName = undefined; + } else if (effectiveHeaders.path) { + name = effectiveHeaders.path; + } + + if (nextLongLinkname) { + linkname = nextLongLinkname; + nextLongLinkname = undefined; + } else if (effectiveHeaders.linkpath) { + linkname = effectiveHeaders.linkpath; + } + nextExtendedHeader = undefined; + + const type = mapType(typeflag); + if (type === 'directory' && name && !name.endsWith('/')) { + name += '/'; + } + + entries.push({ + header: { + name, + type, + mode: readOctal(header, 100, 8), + linkname, + }, + data: type === 'file' ? Buffer.from(data) : Buffer.alloc(0), + }); + + offset = dataOffset + blockSize; + } + + return entries; +} + +function isZeroBlock(block) { + for (const byte of block) { + if (byte !== 0) return false; + } + return true; +} + +function readString(buffer, offset, length) { + const slice = buffer.subarray(offset, offset + length); + const zeroIndex = slice.indexOf(0); + return slice.subarray(0, zeroIndex === -1 ? slice.length : zeroIndex).toString(); +} + +function readBufferString(buffer) { + const zeroIndex = buffer.indexOf(0); + return buffer.subarray(0, zeroIndex === -1 ? buffer.length : zeroIndex).toString(); +} + +function readOctal(buffer, offset, length) { + const value = readString(buffer, offset, length).trim(); + if (!value) return 0; + return parseInt(value, 8); +} + +function parsePaxHeaders(buffer) { + const headers = {}; + let offset = 0; + + while (offset < buffer.length) { + const spaceIndex = buffer.indexOf(0x20, offset); + if (spaceIndex === -1) break; + const recordLength = parseInt(buffer.subarray(offset, spaceIndex).toString(), 10); + if (!Number.isFinite(recordLength) || recordLength <= 0) break; + const record = buffer.subarray(spaceIndex + 1, offset + recordLength - 1).toString(); + const separatorIndex = record.indexOf('='); + if (separatorIndex !== -1) { + headers[record.slice(0, separatorIndex)] = record.slice(separatorIndex + 1); + } + offset += recordLength; + } + + return headers; +} + +function mapType(typeflag) { + if (typeflag === '2') return 'symlink'; + if (typeflag === '5') return 'directory'; + return 'file'; +} + +module.exports = { + createTarBuffer, + parseTarBuffer, +}; diff --git a/lib/tar/file_stream.js b/lib/tar/file_stream.js index 8f45a05..81c6a99 100644 --- a/lib/tar/file_stream.js +++ b/lib/tar/file_stream.js @@ -3,17 +3,14 @@ const fs = require('fs'); const path = require('path'); const stream = require('stream'); -const tar = require('tar-stream'); const utils = require('../utils'); -const ready = require('get-ready'); +const { createTarBuffer } = require('./archive'); class TarFileStream extends stream.Transform { constructor(opts) { super(opts); - - const pack = tar.pack(); - pack.on('data', chunk => this.push(chunk)); - pack.on('end', () => this.ready(true)); + this._opts = opts; + this._chunks = []; const sourceType = utils.sourceType(opts.source); @@ -21,43 +18,30 @@ class TarFileStream extends stream.Transform { // stat file to get file size fs.stat(opts.source, (err, stat) => { if (err) return this.emit('error', err); - this.entry = pack.entry({ name: opts.relativePath || path.basename(opts.source), size: stat.size, mode: stat.mode & 0o777 }, err => { - if (err) return this.emit('error', err); - pack.finalize(); - }); const stream = fs.createReadStream(opts.source, opts.fs); - stream.on('error', err => this.emit('error', err)); - stream.pipe(this); + utils.streamToBuffer(stream) + .then(buffer => this._pushArchive({ + name: opts.relativePath || path.basename(opts.source), + data: buffer, + attrs: { + mode: (stat.mode & 0o777).toString(8), + }, + })) + .catch(error => this.emit('error', error)); }); } else if (sourceType === 'buffer') { if (!opts.relativePath) return this.emit('error', 'opts.relativePath is required if opts.source is a buffer'); - - pack.entry({ name: opts.relativePath }, opts.source); - pack.finalize(); - this.end(); + this._pushArchive({ + name: opts.relativePath, + data: opts.source, + }); } else { // stream or undefined if (!opts.relativePath) return process.nextTick(() => this.emit('error', 'opts.relativePath is required')); - if (opts.size) { - this.entry = pack.entry({ name: opts.relativePath, size: opts.size }, err => { - if (err) return this.emit('error', err); - pack.finalize(); - }); - } else { + if (!opts.size) { if (!opts.suppressSizeWarning) { - console.warn('You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + console.warn('You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); } - const buf = []; - this.entry = new stream.Writable({ - write(chunk, _, callback) { - buf.push(chunk); - callback(); - }, - }); - this.entry.on('finish', () => { - pack.entry({ name: opts.relativePath }, Buffer.concat(buf)); - pack.finalize(); - }); } if (sourceType === 'stream') { @@ -68,19 +52,36 @@ class TarFileStream extends stream.Transform { } _transform(chunk, encoding, callback) { - if (this.entry) { - this.entry.write(chunk, encoding, callback); - } + this._chunks.push(Buffer.from(chunk)); + callback(); } _flush(callback) { - if (this.entry) { - this.entry.end(); + const sourceType = utils.sourceType(this._opts.source); + if (sourceType === 'stream' || sourceType === undefined) { + try { + this.push(createTarBuffer([{ + name: this._opts.relativePath, + data: Buffer.concat(this._chunks), + }])); + callback(); + } catch (err) { + callback(err); + } + return; } - this.ready(callback); + + callback(); } -} -ready.mixin(TarFileStream.prototype); + _pushArchive(entry) { + try { + this.push(createTarBuffer([ entry ])); + this.end(); + } catch (err) { + this.emit('error', err); + } + } +} module.exports = TarFileStream; diff --git a/lib/tar/stream.js b/lib/tar/stream.js index eb3dbc7..53708e4 100644 --- a/lib/tar/stream.js +++ b/lib/tar/stream.js @@ -2,26 +2,21 @@ const fs = require('fs'); const path = require('path'); -const stream = require('stream'); -const tar = require('tar-stream'); const utils = require('../utils'); const BaseStream = require('../base_stream'); +const { createTarBuffer } = require('./archive'); class TarStream extends BaseStream { constructor(opts) { super(opts); + this._archiveEntries = []; this._waitingEntries = []; this._processing = false; this._init(opts); } - _init() { - const pack = this._pack = tar.pack(); - pack.on('end', () => this.push(null)); - pack.on('data', chunk => this.push(chunk)); - pack.on('error', err => this.emit('error', err)); - } + _init() {} addEntry(entry, opts) { if (this._processing) { @@ -60,10 +55,19 @@ class TarStream extends BaseStream { // stat file to get file size fs.stat(entry, (err, stat) => { if (err) return this.emit('error', err); - const entryStream = this._pack.entry({ name: opts.relativePath || path.basename(entry), size: stat.size, mode: stat.mode & 0o777 }, this._onEntryFinish.bind(this)); const stream = fs.createReadStream(entry, opts.fs); - stream.on('error', err => this.emit('error', err)); - stream.pipe(entryStream); + utils.streamToBuffer(stream) + .then(buffer => { + this._archiveEntries.push({ + name: opts.relativePath || path.basename(entry), + data: buffer, + attrs: { + mode: (stat.mode & 0o777).toString(8), + }, + }); + this._onEntryFinish(); + }) + .catch(error => this.emit('error', error)); }); } @@ -88,7 +92,11 @@ class TarStream extends BaseStream { _addBufferEntry(entry, opts) { if (!opts.relativePath) return this.emit('error', 'opts.relativePath is required if entry is a buffer'); - this._pack.entry({ name: opts.relativePath }, entry, this._onEntryFinish.bind(this)); + this._archiveEntries.push({ + name: opts.relativePath, + data: entry, + }); + this._onEntryFinish(); } _addStreamEntry(entry, opts) { @@ -97,24 +105,28 @@ class TarStream extends BaseStream { if (!opts.relativePath) return this.emit('error', new Error('opts.relativePath is required')); if (opts.size) { - const entryStream = this._pack.entry({ name: opts.relativePath, size: opts.size }, this._onEntryFinish.bind(this)); - entry.pipe(entryStream); + utils.streamToBuffer(entry) + .then(buffer => { + this._archiveEntries.push({ + name: opts.relativePath, + data: buffer, + }); + this._onEntryFinish(); + }) + .catch(error => this.emit('error', error)); } else { if (!opts.suppressSizeWarning) { - console.warn('You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + console.warn('You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); } - const buf = []; - const collectStream = new stream.Writable({ - write(chunk, _, callback) { - buf.push(chunk); - callback(); - }, - }); - collectStream.on('error', err => this.emit('error', err)); - collectStream.on('finish', () => { - this._pack.entry({ name: opts.relativePath }, Buffer.concat(buf), this._onEntryFinish.bind(this)); - }); - entry.pipe(collectStream); + utils.streamToBuffer(entry) + .then(buffer => { + this._archiveEntries.push({ + name: opts.relativePath, + data: buffer, + }); + this._onEntryFinish(); + }) + .catch(error => this.emit('error', error)); } } @@ -133,7 +145,12 @@ class TarStream extends BaseStream { } _finalize() { - this._pack.finalize(); + try { + this.push(createTarBuffer(this._archiveEntries)); + this.push(null); + } catch (err) { + this.emit('error', err); + } } } diff --git a/lib/tar/uncompress_stream.js b/lib/tar/uncompress_stream.js index cece681..035b9a5 100644 --- a/lib/tar/uncompress_stream.js +++ b/lib/tar/uncompress_stream.js @@ -1,15 +1,16 @@ 'use strict'; const fs = require('fs'); -const tar = require('tar-stream'); +const stream = require('stream'); const utils = require('../utils'); const streamifier = require('streamifier'); +const { parseTarBuffer } = require('./archive'); -// stream.Writable -class TarUncompressStream extends tar.extract { +class TarUncompressStream extends stream.Writable { constructor(opts) { opts = opts || {}; super(opts); + this._chunks = []; const sourceType = utils.sourceType(opts.source); @@ -34,6 +35,53 @@ class TarUncompressStream extends tar.extract { // else: waiting to be piped } + + _write(chunk, encoding, callback) { + this._chunks.push(Buffer.from(chunk)); + callback(); + } + + _final(callback) { + let entries; + try { + entries = parseTarBuffer(Buffer.concat(this._chunks)); + } catch (err) { + callback(err); + return; + } + + let index = 0; + const nextEntry = err => { + if (err) { + callback(err); + return; + } + + if (index >= entries.length) { + callback(); + return; + } + + const entry = entries[index++]; + const entryStream = stream.Readable.from(entry.data.length ? [ entry.data ] : []); + let finished = false; + const next = nextError => { + if (finished) return; + finished = true; + nextEntry(nextError); + }; + + if (this.listenerCount('entry') === 0) { + entryStream.resume(); + entryStream.on('end', next); + return; + } + + this.emit('entry', entry.header, entryStream, next); + }; + + nextEntry(); + } } module.exports = TarUncompressStream; diff --git a/package.json b/package.json index b181204..a2e000b 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,8 @@ "flushwritable": "^1.0.0", "get-ready": "^1.0.0", "iconv-lite": "^0.5.0", + "nanotar": "^0.3.0", "streamifier": "^0.1.1", - "tar-stream": "^1.5.2", "yazl": "^2.4.2" }, "devDependencies": { diff --git a/test/tar/file_stream.test.js b/test/tar/file_stream.test.js index f4cb418..cba14c6 100644 --- a/test/tar/file_stream.test.js +++ b/test/tar/file_stream.test.js @@ -18,7 +18,7 @@ describe('test/tar/file_stream.test.js', () => { // console.log('dest', destFile); mm(console, 'warn', msg => { - assert(msg === 'You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + assert(msg === 'You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); }); const fileStream = fs.createWriteStream(destFile); diff --git a/test/tar/index.test.js b/test/tar/index.test.js index 1bd2249..baac8bb 100644 --- a/test/tar/index.test.js +++ b/test/tar/index.test.js @@ -68,7 +68,7 @@ describe('test/tar/index.test.js', () => { // console.log('dest', destFile); const fileStream = fs.createWriteStream(destFile); mm(console, 'warn', msg => { - assert(msg === 'You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + assert(msg === 'You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); }); await compressing.tar.compressFile(sourceStream, fileStream, { relativePath: 'xx.log' }); assert(fs.existsSync(destFile)); @@ -123,6 +123,12 @@ describe('test/tar/index.test.js', () => { // console.log('dest', destFile); await compressing.tar.compressDir(sourceDir, destFile); assert(fs.existsSync(destFile)); + + const destDir = path.join(os.tmpdir(), uuid.v4()); + await compressing.tar.uncompress(destFile, destDir); + const res = dircompare.compareSync(sourceDir, path.join(destDir, 'fixtures')); + assert.equal(res.distinct, 0); + assert(res.equal > 0); }); it('tar.compressDir(dir, destStream)', async () => { diff --git a/test/tar/stream.test.js b/test/tar/stream.test.js index 4d9fe63..fbd0d00 100644 --- a/test/tar/stream.test.js +++ b/test/tar/stream.test.js @@ -130,7 +130,7 @@ describe('test/tar/stream.test.js', () => { // console.log('dest', destFile); mm(console, 'warn', msg => { - assert(msg === 'You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + assert(msg === 'You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); }); const tarStream = new TarStream(); diff --git a/test/tgz/stream.test.js b/test/tgz/stream.test.js index 88cb3b3..025f1ef 100644 --- a/test/tgz/stream.test.js +++ b/test/tgz/stream.test.js @@ -130,7 +130,7 @@ describe('test/tgz/stream.test.js', () => { // console.log('dest', destFile); mm(console, 'warn', msg => { - assert(msg === 'You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + assert(msg === 'You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); }); const tgzStream = new TgzStream(); diff --git a/test/util.js b/test/util.js index 3951255..6659528 100644 --- a/test/util.js +++ b/test/util.js @@ -1,5 +1,5 @@ const stream = require('stream'); -const tar = require('tar-stream'); +const { createTarBuffer } = require('../lib/tar/archive'); const pipelinePromise = stream.promises.pipeline; @@ -8,27 +8,13 @@ const pipelinePromise = stream.promises.pipeline; * @param {Array<{name: string, type?: string, linkname?: string, content?: string}>} entries * @returns {Promise} */ -function createTarBuffer(entries) { - return new Promise((resolve, reject) => { - const pack = tar.pack(); - const chunks = []; - - pack.on('data', chunk => chunks.push(chunk)); - pack.on('end', () => resolve(Buffer.concat(chunks))); - pack.on('error', reject); - - for (const entry of entries) { - if (entry.type === 'symlink') { - pack.entry({ name: entry.name, type: 'symlink', linkname: entry.linkname }); - } else if (entry.type === 'directory') { - pack.entry({ name: entry.name, type: 'directory' }); - } else { - pack.entry({ name: entry.name, type: 'file' }, entry.content || ''); - } - } - - pack.finalize(); - }); +function createTarBufferForTests(entries) { + return Promise.resolve(createTarBuffer(entries.map(entry => ({ + name: entry.name, + type: entry.type, + linkname: entry.linkname, + data: entry.content || '', + })))); } -module.exports = { pipelinePromise, createTarBuffer }; +module.exports = { pipelinePromise, createTarBuffer: createTarBufferForTests }; diff --git a/test/zip/stream.test.js b/test/zip/stream.test.js index fbce10c..116e3b4 100644 --- a/test/zip/stream.test.js +++ b/test/zip/stream.test.js @@ -129,7 +129,7 @@ describe('test/zip/stream.test.js', () => { // console.log('dest', destFile); mm(console, 'warn', msg => { - assert(msg === 'You should specify the size of streamming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); + assert(msg === 'You should specify the size of streaming data by opts.size to prevent all streaming data from loading into memory. If you are sure about memory cost, pass opts.suppressSizeWarning: true to suppress this warning'); }); const zipStream = new ZipStream();