├── test ├── fixtures │ ├── empty.txt │ ├── directory │ │ ├── level0.txt │ │ └── subdir │ │ │ ├── level1.txt │ │ │ └── subsub │ │ │ └── level2.txt │ ├── test.txt │ └── image.png ├── commons.js ├── archive-output-stream.js ├── helpers │ └── index.js ├── general-purpose-bit.js ├── zip-archive-output-stream.js └── zip-archive-entry.js ├── .gitignore ├── .prettierignore ├── lib ├── archivers │ ├── archive-entry.js │ ├── zip │ │ ├── unix-stat.js │ │ ├── util.js │ │ ├── general-purpose-bit.js │ │ ├── constants.js │ │ ├── zip-archive-entry.js │ │ └── zip-archive-output-stream.js │ └── archive-output-stream.js ├── compress-commons.js └── util │ └── index.js ├── .github ├── workflows │ ├── release-drafter.yml │ ├── labels.yml │ ├── npmpublish.yml │ └── nodejs.yml ├── release-drafter.yml └── labels.yml ├── CONTRIBUTING.md ├── renovate.json ├── README.md ├── LICENSE ├── package.json └── CHANGELOG.md /test/fixtures/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/directory/level0.txt: -------------------------------------------------------------------------------- 1 | level0 -------------------------------------------------------------------------------- /test/fixtures/test.txt: -------------------------------------------------------------------------------- 1 | this is a text file -------------------------------------------------------------------------------- /test/fixtures/directory/subdir/level1.txt: -------------------------------------------------------------------------------- 1 | level1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | tmp/ -------------------------------------------------------------------------------- /test/fixtures/directory/subdir/subsub/level2.txt: -------------------------------------------------------------------------------- 1 | level2 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | -------------------------------------------------------------------------------- /test/fixtures/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archiverjs/node-compress-commons/HEAD/test/fixtures/image.png -------------------------------------------------------------------------------- /lib/archivers/archive-entry.js: -------------------------------------------------------------------------------- 1 | export default class ArchiveEntry { 2 | getName() {} 3 | getSize() {} 4 | getLastModifiedDate() {} 5 | isDirectory() {} 6 | } 7 | -------------------------------------------------------------------------------- /test/commons.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | /*global before,describe,it */ 3 | 4 | describe("commons", function () { 5 | // it('will do something some day', function(){ 6 | // }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/archive-output-stream.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { assert } from "chai"; 3 | import { binaryBuffer } from "./helpers/index.js"; 4 | 5 | var testBuffer = binaryBuffer(1024 * 16); 6 | describe("ArchiveOutputStream", function () { 7 | describe("#entry", function () {}); 8 | }); 9 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | update_release_draft: 10 | name: Update Release Draft 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /lib/archivers/zip/unix-stat.js: -------------------------------------------------------------------------------- 1 | export const PERM_MASK = 4095; 2 | export const FILE_TYPE_FLAG = 61440; 3 | export const LINK_FLAG = 40960; 4 | export const FILE_FLAG = 32768; 5 | export const DIR_FLAG = 16384; 6 | export const DEFAULT_LINK_PERM = 511; 7 | export const DEFAULT_DIR_PERM = 493; 8 | export const DEFAULT_FILE_PERM = 420; // 0644 9 | 10 | export default { 11 | PERM_MASK, 12 | FILE_TYPE_FLAG, 13 | LINK_FLAG, 14 | FILE_FLAG, 15 | DIR_FLAG, 16 | DEFAULT_LINK_PERM, 17 | DEFAULT_DIR_PERM, 18 | DEFAULT_FILE_PERM, 19 | }; 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | #### Code Style Guide 4 | 5 | - code should be indented with 2 spaces 6 | - single quotes should be used where feasible 7 | - commas should be followed by a single space (function params, etc) 8 | - variable declaration should include `var`, [no multiple declarations](http://benalman.com/news/2012/05/multiple-var-statements-javascript/) 9 | 10 | #### Tests 11 | 12 | - tests should be added to the nodeunit configs in `tests/` 13 | - tests can be run with `npm test` 14 | - see existing tests for guidance 15 | -------------------------------------------------------------------------------- /lib/compress-commons.js: -------------------------------------------------------------------------------- 1 | import ArchiveEntry from "./archivers/archive-entry.js"; 2 | import ZipArchiveEntry from "./archivers/zip/zip-archive-entry.js"; 3 | import ArchiveOutputStream from "./archivers/archive-output-stream.js"; 4 | import ZipArchiveOutputStream from "./archivers/zip/zip-archive-output-stream.js"; 5 | 6 | export { 7 | ArchiveEntry, 8 | ZipArchiveEntry, 9 | ArchiveOutputStream, 10 | ZipArchiveOutputStream, 11 | }; 12 | 13 | export default { 14 | ArchiveEntry, 15 | ZipArchiveEntry, 16 | ArchiveOutputStream, 17 | ZipArchiveOutputStream, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sync labels 3 | 4 | # yamllint disable-line rule:truthy 5 | on: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - .github/labels.yml 11 | workflow_dispatch: 12 | 13 | jobs: 14 | labels: 15 | name: Sync labels 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Check out code from GitHub 19 | uses: actions/checkout@v4 20 | - name: Run Label Syncer 21 | uses: micnncim/action-label-syncer@v1.3.0 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | import { Stream } from "stream"; 2 | import { PassThrough } from "readable-stream"; 3 | import { isStream } from "is-stream"; 4 | 5 | export function normalizeInputSource(source) { 6 | if (source === null) { 7 | return Buffer.alloc(0); 8 | } else if (typeof source === "string") { 9 | return Buffer.from(source); 10 | } else if (isStream(source) && !source._readableState) { 11 | var normalized = new PassThrough(); 12 | source.pipe(normalized); 13 | return normalized; 14 | } 15 | return source; 16 | } 17 | 18 | export default { 19 | normalizeInputSource, 20 | }; 21 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Node Publish Package 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4.2.1 12 | - uses: actions/setup-node@v4.0.4 13 | with: 14 | node-version: 20 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4.2.1 23 | - uses: actions/setup-node@v4.0.4 24 | with: 25 | node-version: 20 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 31 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "schedule:daily", ":pinDevDependencies"], 4 | "timezone": "America/Chicago", 5 | "labels": ["dependencies"], 6 | "packageRules": [ 7 | { 8 | "matchManagers": ["npm", "nvm"], 9 | "separateMultipleMajor": true 10 | }, 11 | { 12 | "matchManagers": ["npm", "nvm"], 13 | "matchUpdateTypes": ["minor", "patch"], 14 | "automerge": true 15 | }, 16 | { 17 | "matchManagers": ["github-actions"], 18 | "addLabels": ["ci", "github-actions"], 19 | "rangeStrategy": "pin" 20 | }, 21 | { 22 | "matchManagers": ["github-actions"], 23 | "matchUpdateTypes": ["minor", "patch"], 24 | "automerge": true 25 | } 26 | ], 27 | "constraints": { 28 | "npm": "<9" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18.x, 20.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v4.2.1 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4.0.4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | - name: npm install and test 26 | run: | 27 | npm ci 28 | npm test 29 | env: 30 | CI: true 31 | - name: Archive production artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: tmp-zip-node-v${{ matrix.node-version }} 35 | path: tmp/*.zip 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compress Commons 2 | 3 | Compress Commons is a library that defines a common interface for working with archive formats within node. 4 | 5 | [![NPM](https://nodei.co/npm/compress-commons.png)](https://nodei.co/npm/compress-commons/) 6 | 7 | ## Install 8 | 9 | ```bash 10 | npm install compress-commons --save 11 | ``` 12 | 13 | You can also use `npm install https://github.com/archiverjs/node-compress-commons/archive/master.tar.gz` to test upcoming versions. 14 | 15 | ## Things of Interest 16 | 17 | - [Changelog](https://github.com/archiverjs/node-compress-commons/releases) 18 | - [Contributing](https://github.com/archiverjs/node-compress-commons/blob/master/CONTRIBUTING.md) 19 | - [MIT License](https://github.com/archiverjs/node-compress-commons/blob/master/LICENSE-MIT) 20 | 21 | ## Credits 22 | 23 | Concept inspired by [Apache Commons Compress](http://commons.apache.org/proper/commons-compress/)™. 24 | 25 | Some logic derived from [Apache Commons Compress](http://commons.apache.org/proper/commons-compress/)™ and [OpenJDK 7](http://openjdk.java.net/). 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Chris Talkington, contributors. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name-template: "$RESOLVED_VERSION" 3 | tag-template: "$RESOLVED_VERSION" 4 | change-template: "- $TITLE @$AUTHOR (#$NUMBER)" 5 | sort-direction: ascending 6 | 7 | categories: 8 | - title: "Breaking changes" 9 | labels: 10 | - "breaking-change" 11 | - title: "New features" 12 | labels: 13 | - "new-feature" 14 | - title: "Bug fixes" 15 | labels: 16 | - "bugfix" 17 | - title: "Enhancements" 18 | labels: 19 | - "enhancement" 20 | - "refactor" 21 | - "performance" 22 | - title: "Maintenance" 23 | labels: 24 | - "maintenance" 25 | - "ci" 26 | - title: "Documentation" 27 | labels: 28 | - "documentation" 29 | - title: "Dependency updates" 30 | labels: 31 | - "dependencies" 32 | 33 | version-resolver: 34 | major: 35 | labels: 36 | - "major" 37 | - "breaking-change" 38 | minor: 39 | labels: 40 | - "minor" 41 | - "new-feature" 42 | patch: 43 | labels: 44 | - "bugfix" 45 | - "chore" 46 | - "ci" 47 | - "dependencies" 48 | - "documentation" 49 | - "enhancement" 50 | - "performance" 51 | - "refactor" 52 | default: patch 53 | 54 | template: | 55 | ## What’s changed 56 | 57 | $CHANGES 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "compress-commons", 3 | "version": "7.0.0", 4 | "description": "a library that defines a common interface for working with archive formats within node", 5 | "homepage": "https://github.com/archiverjs/node-compress-commons", 6 | "author": { 7 | "name": "Chris Talkington", 8 | "url": "http://christalkington.com/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/archiverjs/node-compress-commons.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/archiverjs/node-compress-commons/issues" 16 | }, 17 | "license": "MIT", 18 | "type": "module", 19 | "exports": "./lib/compress-commons.js", 20 | "files": [ 21 | "lib" 22 | ], 23 | "engines": { 24 | "node": ">=18" 25 | }, 26 | "scripts": { 27 | "test": "mocha --reporter dot" 28 | }, 29 | "dependencies": { 30 | "crc-32": "^1.2.0", 31 | "crc32-stream": "^7.0.1", 32 | "is-stream": "^4.0.0", 33 | "normalize-path": "^3.0.0", 34 | "readable-stream": "^4.0.0" 35 | }, 36 | "devDependencies": { 37 | "chai": "5.1.1", 38 | "mkdirp": "3.0.1", 39 | "mocha": "10.7.3", 40 | "prettier": "3.3.3", 41 | "rimraf": "5.0.10" 42 | }, 43 | "keywords": [ 44 | "compress", 45 | "commons", 46 | "archive" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /test/helpers/index.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import { WriteStream } from "fs"; 3 | import { inherits } from "util"; 4 | import { Stream } from "stream"; 5 | import { Readable } from "readable-stream"; 6 | import { Writable } from "readable-stream"; 7 | 8 | export function binaryBuffer(n) { 9 | var buffer = Buffer.alloc(n); 10 | for (var i = 0; i < n; i++) { 11 | buffer.writeUInt8(i & 255, i); 12 | } 13 | return buffer; 14 | } 15 | 16 | export class BinaryStream extends Readable { 17 | constructor(size, options) { 18 | super(options); 19 | var buf = Buffer.alloc(size); 20 | for (var i = 0; i < size; i++) { 21 | buf.writeUInt8(i & 255, i); 22 | } 23 | this.push(buf); 24 | this.push(null); 25 | } 26 | 27 | _read(size) {} 28 | } 29 | 30 | export class DeadEndStream extends Writable { 31 | constructor(options) { 32 | super(options); 33 | } 34 | 35 | _write(chuck, encoding, callback) { 36 | callback(); 37 | } 38 | } 39 | 40 | export function fileBuffer(filepath) { 41 | return fs.readFileSync(filepath); 42 | } 43 | 44 | export class UnBufferedStream extends Stream { 45 | constructor() { 46 | super(); 47 | this.readable = true; 48 | } 49 | } 50 | 51 | export class WriteHashStream extends WriteStream { 52 | constructor(path, options) { 53 | super(path, options); 54 | this.hash = crypto.createHash("sha1"); 55 | this.digest = null; 56 | this.on("close", function () { 57 | this.digest = this.hash.digest("hex"); 58 | }); 59 | } 60 | 61 | write(chunk) { 62 | if (chunk) { 63 | this.hash.update(chunk); 64 | } 65 | return super.write(chunk); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/general-purpose-bit.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import GeneralPurposeBit from "../lib/archivers/zip/general-purpose-bit.js"; 3 | /*global before,describe,it */ 4 | 5 | var gpb; 6 | describe("GeneralPurposeBit", function () { 7 | beforeEach(function () { 8 | gpb = new GeneralPurposeBit(); 9 | }); 10 | describe("#encode", function () { 11 | it("should return a Buffer", function () { 12 | gpb.useDataDescriptor(); 13 | assert.ok(Buffer.isBuffer(gpb.encode())); 14 | }); 15 | }); 16 | describe("#parse", function () { 17 | it.skip("should be tested", function () {}); 18 | }); 19 | describe("#setNumberOfShannonFanoTrees", function () { 20 | it.skip("should be tested", function () {}); 21 | }); 22 | describe("#getNumberOfShannonFanoTrees", function () { 23 | it.skip("should be tested", function () {}); 24 | }); 25 | describe("#setSlidingDictionarySize", function () { 26 | it.skip("should be tested", function () {}); 27 | }); 28 | describe("#getSlidingDictionarySize", function () { 29 | it.skip("should be tested", function () {}); 30 | }); 31 | describe("#useDataDescriptor", function () { 32 | it.skip("should be tested", function () {}); 33 | }); 34 | describe("#usesDataDescriptor", function () { 35 | it.skip("should be tested", function () {}); 36 | }); 37 | describe("#useEncryption", function () { 38 | it.skip("should be tested", function () {}); 39 | }); 40 | describe("#usesEncryption", function () { 41 | it.skip("should be tested", function () {}); 42 | }); 43 | describe("#useStrongEncryption", function () { 44 | it.skip("should be tested", function () {}); 45 | }); 46 | describe("#usesStrongEncryption", function () { 47 | it.skip("should be tested", function () {}); 48 | }); 49 | describe("#useUTF8ForNames", function () { 50 | it.skip("should be tested", function () {}); 51 | }); 52 | describe("#usesUTF8ForNames", function () { 53 | it.skip("should be tested", function () {}); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /lib/archivers/zip/util.js: -------------------------------------------------------------------------------- 1 | export function dateToDos(d, forceLocalTime) { 2 | forceLocalTime = forceLocalTime || false; 3 | var year = forceLocalTime ? d.getFullYear() : d.getUTCFullYear(); 4 | if (year < 1980) { 5 | return 2162688; // 1980-1-1 00:00:00 6 | } else if (year >= 2044) { 7 | return 2141175677; // 2043-12-31 23:59:58 8 | } 9 | var val = { 10 | year: year, 11 | month: forceLocalTime ? d.getMonth() : d.getUTCMonth(), 12 | date: forceLocalTime ? d.getDate() : d.getUTCDate(), 13 | hours: forceLocalTime ? d.getHours() : d.getUTCHours(), 14 | minutes: forceLocalTime ? d.getMinutes() : d.getUTCMinutes(), 15 | seconds: forceLocalTime ? d.getSeconds() : d.getUTCSeconds(), 16 | }; 17 | return ( 18 | ((val.year - 1980) << 25) | 19 | ((val.month + 1) << 21) | 20 | (val.date << 16) | 21 | (val.hours << 11) | 22 | (val.minutes << 5) | 23 | (val.seconds / 2) 24 | ); 25 | } 26 | export function dosToDate(dos) { 27 | return new Date( 28 | ((dos >> 25) & 0x7f) + 1980, 29 | ((dos >> 21) & 0x0f) - 1, 30 | (dos >> 16) & 0x1f, 31 | (dos >> 11) & 0x1f, 32 | (dos >> 5) & 0x3f, 33 | (dos & 0x1f) << 1, 34 | ); 35 | } 36 | export function fromDosTime(buf) { 37 | return dosToDate(buf.readUInt32LE(0)); 38 | } 39 | export function getEightBytes(v) { 40 | var buf = Buffer.alloc(8); 41 | buf.writeUInt32LE(v % 0x0100000000, 0); 42 | buf.writeUInt32LE((v / 0x0100000000) | 0, 4); 43 | return buf; 44 | } 45 | export function getShortBytes(v) { 46 | var buf = Buffer.alloc(2); 47 | buf.writeUInt16LE((v & 0xffff) >>> 0, 0); 48 | return buf; 49 | } 50 | export function getShortBytesValue(buf, offset) { 51 | return buf.readUInt16LE(offset); 52 | } 53 | export function getLongBytes(v) { 54 | var buf = Buffer.alloc(4); 55 | buf.writeUInt32LE((v & 0xffffffff) >>> 0, 0); 56 | return buf; 57 | } 58 | export function getLongBytesValue(buf, offset) { 59 | return buf.readUInt32LE(offset); 60 | } 61 | export function toDosTime(d) { 62 | return getLongBytes(util.dateToDos(d)); 63 | } 64 | export default { 65 | dateToDos, 66 | dosToDate, 67 | fromDosTime, 68 | getEightBytes, 69 | getShortBytes, 70 | getShortBytesValue, 71 | getLongBytes, 72 | getLongBytesValue, 73 | toDosTime, 74 | }; 75 | -------------------------------------------------------------------------------- /lib/archivers/zip/general-purpose-bit.js: -------------------------------------------------------------------------------- 1 | import { getShortBytes, getShortBytesValue } from "./util.js"; 2 | 3 | var DATA_DESCRIPTOR_FLAG = 1 << 3; 4 | var ENCRYPTION_FLAG = 1 << 0; 5 | var NUMBER_OF_SHANNON_FANO_TREES_FLAG = 1 << 2; 6 | var SLIDING_DICTIONARY_SIZE_FLAG = 1 << 1; 7 | var STRONG_ENCRYPTION_FLAG = 1 << 6; 8 | var UFT8_NAMES_FLAG = 1 << 11; 9 | 10 | export default class GeneralPurposeBit { 11 | constructor() { 12 | this.descriptor = false; 13 | this.encryption = false; 14 | this.utf8 = false; 15 | this.numberOfShannonFanoTrees = 0; 16 | this.strongEncryption = false; 17 | this.slidingDictionarySize = 0; 18 | return this; 19 | } 20 | encode() { 21 | return getShortBytes( 22 | (this.descriptor ? DATA_DESCRIPTOR_FLAG : 0) | 23 | (this.utf8 ? UFT8_NAMES_FLAG : 0) | 24 | (this.encryption ? ENCRYPTION_FLAG : 0) | 25 | (this.strongEncryption ? STRONG_ENCRYPTION_FLAG : 0), 26 | ); 27 | } 28 | static parse(buf, offset) { 29 | var flag = getShortBytesValue(buf, offset); 30 | var gbp = new GeneralPurposeBit(); 31 | gbp.useDataDescriptor((flag & DATA_DESCRIPTOR_FLAG) !== 0); 32 | gbp.useUTF8ForNames((flag & UFT8_NAMES_FLAG) !== 0); 33 | gbp.useStrongEncryption((flag & STRONG_ENCRYPTION_FLAG) !== 0); 34 | gbp.useEncryption((flag & ENCRYPTION_FLAG) !== 0); 35 | gbp.setSlidingDictionarySize( 36 | (flag & SLIDING_DICTIONARY_SIZE_FLAG) !== 0 ? 8192 : 4096, 37 | ); 38 | gbp.setNumberOfShannonFanoTrees( 39 | (flag & NUMBER_OF_SHANNON_FANO_TREES_FLAG) !== 0 ? 3 : 2, 40 | ); 41 | return gbp; 42 | } 43 | setNumberOfShannonFanoTrees(n) { 44 | this.numberOfShannonFanoTrees = n; 45 | } 46 | getNumberOfShannonFanoTrees() { 47 | return this.numberOfShannonFanoTrees; 48 | } 49 | setSlidingDictionarySize(n) { 50 | this.slidingDictionarySize = n; 51 | } 52 | getSlidingDictionarySize() { 53 | return this.slidingDictionarySize; 54 | } 55 | useDataDescriptor(b) { 56 | this.descriptor = b; 57 | } 58 | usesDataDescriptor() { 59 | return this.descriptor; 60 | } 61 | useEncryption(b) { 62 | this.encryption = b; 63 | } 64 | usesEncryption() { 65 | return this.encryption; 66 | } 67 | useStrongEncryption(b) { 68 | this.strongEncryption = b; 69 | } 70 | usesStrongEncryption() { 71 | return this.strongEncryption; 72 | } 73 | useUTF8ForNames(b) { 74 | this.utf8 = b; 75 | } 76 | usesUTF8ForNames() { 77 | return this.utf8; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /lib/archivers/archive-output-stream.js: -------------------------------------------------------------------------------- 1 | import { inherits } from "util"; 2 | import { isStream } from "is-stream"; 3 | import { Transform } from "readable-stream"; 4 | import ArchiveEntry from "./archive-entry.js"; 5 | import { normalizeInputSource } from "../util/index.js"; 6 | 7 | export default class ArchiveOutputStream extends Transform { 8 | constructor(options) { 9 | super(options); 10 | 11 | this.offset = 0; 12 | this._archive = { 13 | finish: false, 14 | finished: false, 15 | processing: false, 16 | }; 17 | } 18 | 19 | _appendBuffer(zae, source, callback) { 20 | // scaffold only 21 | } 22 | 23 | _appendStream(zae, source, callback) { 24 | // scaffold only 25 | } 26 | 27 | _emitErrorCallback = function (err) { 28 | if (err) { 29 | this.emit("error", err); 30 | } 31 | }; 32 | 33 | _finish(ae) { 34 | // scaffold only 35 | } 36 | 37 | _normalizeEntry(ae) { 38 | // scaffold only 39 | } 40 | 41 | _transform(chunk, encoding, callback) { 42 | callback(null, chunk); 43 | } 44 | 45 | entry(ae, source, callback) { 46 | source = source || null; 47 | if (typeof callback !== "function") { 48 | callback = this._emitErrorCallback.bind(this); 49 | } 50 | if (!(ae instanceof ArchiveEntry)) { 51 | callback(new Error("not a valid instance of ArchiveEntry")); 52 | return; 53 | } 54 | if (this._archive.finish || this._archive.finished) { 55 | callback(new Error("unacceptable entry after finish")); 56 | return; 57 | } 58 | if (this._archive.processing) { 59 | callback(new Error("already processing an entry")); 60 | return; 61 | } 62 | this._archive.processing = true; 63 | this._normalizeEntry(ae); 64 | this._entry = ae; 65 | source = normalizeInputSource(source); 66 | if (Buffer.isBuffer(source)) { 67 | this._appendBuffer(ae, source, callback); 68 | } else if (isStream(source)) { 69 | this._appendStream(ae, source, callback); 70 | } else { 71 | this._archive.processing = false; 72 | callback( 73 | new Error("input source must be valid Stream or Buffer instance"), 74 | ); 75 | return; 76 | } 77 | return this; 78 | } 79 | 80 | finish() { 81 | if (this._archive.processing) { 82 | this._archive.finish = true; 83 | return; 84 | } 85 | this._finish(); 86 | } 87 | 88 | getBytesWritten() { 89 | return this.offset; 90 | } 91 | 92 | write(chunk, cb) { 93 | if (chunk) { 94 | this.offset += chunk.length; 95 | } 96 | return super.write(chunk, cb); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "breaking-change" 3 | color: ee0701 4 | description: "A breaking change for existing users." 5 | - name: "bugfix" 6 | color: ee0701 7 | description: "Inconsistencies or issues which will cause a problem for users or implementors." 8 | - name: "documentation" 9 | color: 0052cc 10 | description: "Solely about the documentation of the project." 11 | - name: "enhancement" 12 | color: 1d76db 13 | description: "Enhancement of the code, not introducing new features." 14 | - name: "refactor" 15 | color: 1d76db 16 | description: "Improvement of existing code, not introducing new features." 17 | - name: "performance" 18 | color: 1d76db 19 | description: "Improving performance, not introducing new features." 20 | - name: "new-feature" 21 | color: 0e8a16 22 | description: "New features or options." 23 | - name: "maintenance" 24 | color: 2af79e 25 | description: "Generic maintenance tasks." 26 | - name: "ci" 27 | color: 1d76db 28 | description: "Work that improves the continue integration." 29 | - name: "dependencies" 30 | color: 1d76db 31 | description: "Upgrade or downgrade of project dependencies." 32 | 33 | - name: "in-progress" 34 | color: fbca04 35 | description: "Issue is currently being resolved by a developer." 36 | - name: "stale" 37 | color: fef2c0 38 | description: "There has not been activity on this issue or PR for quite some time." 39 | - name: "no-stale" 40 | color: fef2c0 41 | description: "This issue or PR is exempted from the stable bot." 42 | 43 | - name: "security" 44 | color: ee0701 45 | description: "Marks a security issue that needs to be resolved asap." 46 | - name: "incomplete" 47 | color: fef2c0 48 | description: "Marks a PR or issue that is missing information." 49 | - name: "invalid" 50 | color: fef2c0 51 | description: "Marks a PR or issue that is missing information." 52 | 53 | - name: "beginner-friendly" 54 | color: 0e8a16 55 | description: "Good first issue for people wanting to contribute to the project." 56 | - name: "help-wanted" 57 | color: 0e8a16 58 | description: "We need some extra helping hands or expertise in order to resolve this." 59 | 60 | - name: "priority-critical" 61 | color: ee0701 62 | description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." 63 | - name: "priority-high" 64 | color: b60205 65 | description: "After critical issues are fixed, these should be dealt with before any further issues." 66 | - name: "priority-medium" 67 | color: 0e8a16 68 | description: "This issue may be useful, and needs some attention." 69 | - name: "priority-low" 70 | color: e4ea8a 71 | description: "Nice addition, maybe... someday..." 72 | 73 | - name: "major" 74 | color: b60205 75 | description: "This PR causes a major version bump in the version number." 76 | - name: "minor" 77 | color: 0e8a16 78 | description: "This PR causes a minor version bump in the version number." 79 | -------------------------------------------------------------------------------- /lib/archivers/zip/constants.js: -------------------------------------------------------------------------------- 1 | export const WORD = 4; 2 | export const DWORD = 8; 3 | export const EMPTY = Buffer.alloc(0); 4 | export const SHORT = 2; 5 | export const SHORT_MASK = 0xffff; 6 | export const SHORT_SHIFT = 16; 7 | export const SHORT_ZERO = Buffer.from(Array(2)); 8 | export const LONG = 4; 9 | export const LONG_ZERO = Buffer.from(Array(4)); 10 | export const MIN_VERSION_INITIAL = 10; 11 | export const MIN_VERSION_DATA_DESCRIPTOR = 20; 12 | export const MIN_VERSION_ZIP64 = 45; 13 | export const VERSION_MADEBY = 45; 14 | export const METHOD_STORED = 0; 15 | export const METHOD_DEFLATED = 8; 16 | export const PLATFORM_UNIX = 3; 17 | export const PLATFORM_FAT = 0; 18 | export const SIG_LFH = 0x04034b50; 19 | export const SIG_DD = 0x08074b50; 20 | export const SIG_CFH = 0x02014b50; 21 | export const SIG_EOCD = 0x06054b50; 22 | export const SIG_ZIP64_EOCD = 0x06064b50; 23 | export const SIG_ZIP64_EOCD_LOC = 0x07064b50; 24 | export const ZIP64_MAGIC_SHORT = 0xffff; 25 | export const ZIP64_MAGIC = 0xffffffff; 26 | export const ZIP64_EXTRA_ID = 0x0001; 27 | export const ZLIB_NO_COMPRESSION = 0; 28 | export const ZLIB_BEST_SPEED = 1; 29 | export const ZLIB_BEST_COMPRESSION = 9; 30 | export const ZLIB_DEFAULT_COMPRESSION = -1; 31 | export const MODE_MASK = 0xfff; 32 | export const DEFAULT_FILE_MODE = 33188; 33 | export const DEFAULT_DIR_MODE = 16877; 34 | export const EXT_FILE_ATTR_DIR = 1106051088; 35 | export const EXT_FILE_ATTR_FILE = 2175008800; 36 | export const S_IFMT = 61440; 37 | export const S_IFIFO = 4096; 38 | export const S_IFCHR = 8192; 39 | export const S_IFDIR = 16384; 40 | export const S_IFBLK = 24576; 41 | export const S_IFREG = 32768; 42 | export const S_IFLNK = 40960; 43 | export const S_IFSOCK = 49152; 44 | export const S_DOS_A = 32; 45 | export const S_DOS_D = 16; 46 | export const S_DOS_V = 8; 47 | export const S_DOS_S = 4; 48 | export const S_DOS_H = 2; 49 | export const S_DOS_R = 1; // 01 Read Only 50 | export default { 51 | WORD, 52 | DWORD, 53 | EMPTY, 54 | SHORT, 55 | SHORT_MASK, 56 | SHORT_SHIFT, 57 | SHORT_ZERO, 58 | LONG, 59 | LONG_ZERO, 60 | MIN_VERSION_INITIAL, 61 | MIN_VERSION_DATA_DESCRIPTOR, 62 | MIN_VERSION_ZIP64, 63 | VERSION_MADEBY, 64 | METHOD_STORED, 65 | METHOD_DEFLATED, 66 | PLATFORM_UNIX, 67 | PLATFORM_FAT, 68 | SIG_LFH, 69 | SIG_DD, 70 | SIG_CFH, 71 | SIG_EOCD, 72 | SIG_ZIP64_EOCD, 73 | SIG_ZIP64_EOCD_LOC, 74 | ZIP64_MAGIC_SHORT, 75 | ZIP64_MAGIC, 76 | ZIP64_EXTRA_ID, 77 | ZLIB_NO_COMPRESSION, 78 | ZLIB_BEST_SPEED, 79 | ZLIB_BEST_COMPRESSION, 80 | ZLIB_DEFAULT_COMPRESSION, 81 | MODE_MASK, 82 | DEFAULT_FILE_MODE, 83 | DEFAULT_DIR_MODE, 84 | EXT_FILE_ATTR_DIR, 85 | EXT_FILE_ATTR_FILE, 86 | S_IFMT, 87 | S_IFIFO, 88 | S_IFCHR, 89 | S_IFDIR, 90 | S_IFBLK, 91 | S_IFREG, 92 | S_IFLNK, 93 | S_IFSOCK, 94 | S_DOS_A, 95 | S_DOS_D, 96 | S_DOS_V, 97 | S_DOS_S, 98 | S_DOS_H, 99 | S_DOS_R, 100 | }; 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | **7.0.0** — _October 13, 2024_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/6.0.2...7.0.0) 4 | 5 | **6.0.2** — _March 9, 2024_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/6.0.1...6.0.2) 6 | 7 | **6.0.1** — _February 29, 2024_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/6.0.0...6.0.1) 8 | 9 | **6.0.0** — _February 26, 2024_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/5.0.2...6.0.0) 10 | 11 | **5.0.2** — _February 26, 2024_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/5.0.1...5.0.2) 12 | 13 | **5.0.1** — _September 3, 2023_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/5.0.0...5.0.1) 14 | 15 | **5.0.0** — _September 2, 2023_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/4.1.2...5.0.0) 16 | 17 | **4.1.2** — _September 2, 2023_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/4.1.1...4.1.2) 18 | 19 | **4.1.1** — _May 30th, 2021_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/4.1.0...4.1.1) 20 | 21 | ### Maintenance 22 | 23 | - Bump mocha from 8.2.1 to 8.4.0 (#70) 24 | - Bump crc32-stream from 4.0.1 to 4.0.2 (#59) 25 | - Bump y18n from 4.0.0 to 4.0.1 (#69) 26 | - Bump chai from 4.2.0 to 4.3.4 (#67) 27 | - Bump actions/setup-node from 2.1.4 to 2.1.5 (#71) 28 | 29 | **4.1.0** — _March 2, 2021_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/4.0.1...4.1.0) 30 | 31 | ### Features 32 | 33 | - Allow prepending forward slash in entry name (#63) 34 | 35 | ### Maintenance 36 | 37 | - Bump actions/setup-node from v2.1.2 to v2.1.4 (#58) 38 | 39 | **4.0.1** — _July 20, 2020_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/4.0.0...4.0.1) 40 | 41 | - Bump crc32-stream from 3.0.1 to 4.0.0 (#43) @dependabot 42 | 43 | **4.0.0** — _July 18, 2020_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/3.0.0...4.0.0) 44 | 45 | - Bump mocha from 5.2.0 to 8.0.1 (#36) @dependabot 46 | - Bump readable-stream from 2.3.7 to 3.6.0 (#39) @dependabot 47 | - Bump actions/setup-node from v1 to v2.1.0 (#41) @dependabot 48 | - Bump rimraf from 2.7.1 to 3.0.2 (#38) @dependabot 49 | - Bump mkdirp from 0.5.5 to 1.0.4 (#37) @dependabot 50 | - Bump actions/checkout from v1 to v2.3.1 (#40) @dependabot 51 | - remove support for node < 10 (#42) @ctalkington 52 | 53 | **3.0.0** — _April 14, 2020_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/2.1.1...3.0.0) 54 | 55 | - breaking: slowly catch up with node LTS, remove support for versions under 8. 56 | - update multiple deps. 57 | 58 | **2.1.1** — _August 2, 2019_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/2.1.0...2.1.1) 59 | 60 | - update crc32-stream to v3.0.1 61 | 62 | **2.1.0** — _August 2, 2019_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/2.0.0...2.1.0) 63 | 64 | - update crc32-stream to v3.0.0 65 | 66 | **2.0.0** — _July 19, 2019_ — [Diff](https://github.com/archiverjs/node-compress-commons/compare/1.2.2...2.0.0) 67 | 68 | - breaking: follow node LTS, remove support for versions under 6. 69 | - test: now targeting node v10 and v12 70 | - fix: update Buffer calls to alloc/from 71 | - fix: Add offset to buffer call (#31) 72 | - other: update normalize-path@3 (#34) 73 | - other: update dependencies 74 | 75 | [Release Archive](https://github.com/archiverjs/node-compress-commons/releases) 76 | -------------------------------------------------------------------------------- /test/zip-archive-output-stream.js: -------------------------------------------------------------------------------- 1 | import { createReadStream } from "fs"; 2 | import { Stream, Transform } from "stream"; 3 | import { assert } from "chai"; 4 | import { mkdirp } from "mkdirp"; 5 | import { Readable } from "readable-stream"; 6 | import { WriteHashStream, binaryBuffer } from "./helpers/index.js"; 7 | import { 8 | ZipArchiveEntry, 9 | ZipArchiveOutputStream, 10 | } from "../lib/compress-commons.js"; 11 | 12 | var testBuffer = binaryBuffer(1024 * 16); 13 | var testDate = new Date("Jan 03 2013 14:26:38 GMT"); 14 | 15 | describe("ZipArchiveOutputStream", function () { 16 | before(function () { 17 | mkdirp.sync("tmp"); 18 | }); 19 | describe("#entry", function () { 20 | it("should append Buffer sources", function (done) { 21 | var archive = new ZipArchiveOutputStream(); 22 | var testStream = new WriteHashStream("tmp/zip-buffer.zip"); 23 | var entry = new ZipArchiveEntry("buffer.txt"); 24 | testStream.on("close", function () { 25 | done(); 26 | }); 27 | archive.pipe(testStream); 28 | archive.entry(entry, testBuffer).finish(); 29 | }); 30 | it("should append Stream sources", function (done) { 31 | var archive = new ZipArchiveOutputStream(); 32 | var testStream = new WriteHashStream("tmp/zip-stream.zip"); 33 | var entry = new ZipArchiveEntry("stream.txt"); 34 | testStream.on("close", function () { 35 | done(); 36 | }); 37 | archive.pipe(testStream); 38 | archive.entry(entry, createReadStream("test/fixtures/test.txt")).finish(); 39 | }); 40 | it("should append Stream-like sources", function (done) { 41 | var archive = new ZipArchiveOutputStream(); 42 | var testStream = new WriteHashStream("tmp/zip-stream-like.zip"); 43 | var entry = new ZipArchiveEntry("stream-like.txt"); 44 | testStream.on("close", function () { 45 | done(); 46 | }); 47 | archive.pipe(testStream); 48 | archive.entry(entry, Readable.from(["test"])).finish(); 49 | }); 50 | it("should stop streaming on Stream error", function (done) { 51 | var archive = new ZipArchiveOutputStream(); 52 | var testStream = new WriteHashStream("tmp/zip-stream.zip"); 53 | var entry = new ZipArchiveEntry("stream.txt"); 54 | var callbackError = null; 55 | var callbackCalls = 0; 56 | testStream.on("close", function () { 57 | assert.equal(callbackError.message, "something went wrong"); 58 | assert.equal(callbackCalls, 1); 59 | done(); 60 | }); 61 | archive.pipe(testStream); 62 | var file = new Transform(); 63 | archive.entry(entry, file, function (err) { 64 | callbackCalls += 1; 65 | callbackError = err; 66 | }); 67 | archive.finish(); 68 | process.nextTick(function () { 69 | file.emit("error", new Error("something went wrong")); 70 | }); 71 | }); 72 | it("should append multiple sources", function (done) { 73 | var archive = new ZipArchiveOutputStream(); 74 | var testStream = new WriteHashStream("tmp/zip-multiple.zip"); 75 | var entry = new ZipArchiveEntry("string.txt"); 76 | var entry2 = new ZipArchiveEntry("buffer.txt"); 77 | var entry3 = new ZipArchiveEntry("stream.txt"); 78 | var entry4 = new ZipArchiveEntry("stream-store.png"); 79 | entry4.setMethod(0); 80 | var entry5 = new ZipArchiveEntry("buffer-store.txt"); 81 | entry5.setMethod(0); 82 | testStream.on("close", function () { 83 | done(); 84 | }); 85 | archive.pipe(testStream); 86 | archive.entry(entry, "string", function (err) { 87 | if (err) throw err; 88 | archive.entry(entry2, testBuffer, function (err) { 89 | if (err) throw err; 90 | archive.entry( 91 | entry3, 92 | createReadStream("test/fixtures/test.txt"), 93 | function (err) { 94 | if (err) throw err; 95 | archive.entry( 96 | entry4, 97 | createReadStream("test/fixtures/image.png"), 98 | function (err) { 99 | if (err) throw err; 100 | archive.entry(entry5, testBuffer, function (err) { 101 | if (err) throw err; 102 | archive.finish(); 103 | }); 104 | }, 105 | ); 106 | }, 107 | ); 108 | }); 109 | }); 110 | }); 111 | it("should force ZIP64", function (done) { 112 | var archive = new ZipArchiveOutputStream({ 113 | forceZip64: true, 114 | }); 115 | var testStream = new WriteHashStream("tmp/zip-stream64.zip"); 116 | var entry = new ZipArchiveEntry("stream.txt"); 117 | testStream.on("close", function () { 118 | done(); 119 | }); 120 | archive.pipe(testStream); 121 | archive.entry(entry, createReadStream("test/fixtures/test.txt")).finish(); 122 | }); 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /lib/archivers/zip/zip-archive-entry.js: -------------------------------------------------------------------------------- 1 | import { inherits } from "util"; 2 | import normalizePath from "normalize-path"; 3 | import ArchiveEntry from "../archive-entry.js"; 4 | import GeneralPurposeBit from "./general-purpose-bit.js"; 5 | import UnixStat from "./unix-stat.js"; 6 | import { 7 | EMPTY, 8 | MIN_VERSION_INITIAL, 9 | MODE_MASK, 10 | PLATFORM_FAT, 11 | PLATFORM_UNIX, 12 | S_DOS_A, 13 | S_DOS_D, 14 | S_IFDIR, 15 | S_IFREG, 16 | SHORT_MASK, 17 | SHORT_SHIFT, 18 | ZIP64_MAGIC, 19 | } from "./constants.js"; 20 | import { dateToDos, dosToDate } from "./util.js"; 21 | 22 | export default class ZipArchiveEntry extends ArchiveEntry { 23 | constructor(name) { 24 | super(); 25 | this.platform = PLATFORM_FAT; 26 | this.method = -1; 27 | this.name = null; 28 | this.size = 0; 29 | this.csize = 0; 30 | this.gpb = new GeneralPurposeBit(); 31 | this.crc = 0; 32 | this.time = -1; 33 | this.minver = MIN_VERSION_INITIAL; 34 | this.mode = -1; 35 | this.extra = null; 36 | this.exattr = 0; 37 | this.inattr = 0; 38 | this.comment = null; 39 | if (name) { 40 | this.setName(name); 41 | } 42 | } 43 | 44 | /** 45 | * Returns the extra fields related to the entry. 46 | * 47 | * @returns {Buffer} 48 | */ 49 | getCentralDirectoryExtra() { 50 | return this.getExtra(); 51 | } 52 | 53 | /** 54 | * Returns the comment set for the entry. 55 | * 56 | * @returns {string} 57 | */ 58 | getComment() { 59 | return this.comment !== null ? this.comment : ""; 60 | } 61 | 62 | /** 63 | * Returns the compressed size of the entry. 64 | * 65 | * @returns {number} 66 | */ 67 | getCompressedSize() { 68 | return this.csize; 69 | } 70 | 71 | /** 72 | * Returns the CRC32 digest for the entry. 73 | * 74 | * @returns {number} 75 | */ 76 | getCrc() { 77 | return this.crc; 78 | } 79 | 80 | /** 81 | * Returns the external file attributes for the entry. 82 | * 83 | * @returns {number} 84 | */ 85 | getExternalAttributes = function () { 86 | return this.exattr; 87 | }; 88 | 89 | /** 90 | * Returns the extra fields related to the entry. 91 | * 92 | * @returns {Buffer} 93 | */ 94 | getExtra() { 95 | return this.extra !== null ? this.extra : EMPTY; 96 | } 97 | 98 | /** 99 | * Returns the general purpose bits related to the entry. 100 | * 101 | * @returns {GeneralPurposeBit} 102 | */ 103 | getGeneralPurposeBit() { 104 | return this.gpb; 105 | } 106 | 107 | /** 108 | * Returns the internal file attributes for the entry. 109 | * 110 | * @returns {number} 111 | */ 112 | getInternalAttributes() { 113 | return this.inattr; 114 | } 115 | 116 | /** 117 | * Returns the last modified date of the entry. 118 | * 119 | * @returns {number} 120 | */ 121 | getLastModifiedDate() { 122 | return this.getTime(); 123 | } 124 | 125 | /** 126 | * Returns the extra fields related to the entry. 127 | * 128 | * @returns {Buffer} 129 | */ 130 | getLocalFileDataExtra() { 131 | return this.getExtra(); 132 | } 133 | 134 | /** 135 | * Returns the compression method used on the entry. 136 | * 137 | * @returns {number} 138 | */ 139 | getMethod() { 140 | return this.method; 141 | } 142 | 143 | /** 144 | * Returns the filename of the entry. 145 | * 146 | * @returns {string} 147 | */ 148 | getName() { 149 | return this.name; 150 | } 151 | 152 | /** 153 | * Returns the platform on which the entry was made. 154 | * 155 | * @returns {number} 156 | */ 157 | getPlatform() { 158 | return this.platform; 159 | } 160 | /** 161 | * Returns the size of the entry. 162 | * 163 | * @returns {number} 164 | */ 165 | getSize() { 166 | return this.size; 167 | } 168 | /** 169 | * Returns a date object representing the last modified date of the entry. 170 | * 171 | * @returns {number|Date} 172 | */ 173 | getTime() { 174 | return this.time !== -1 ? dosToDate(this.time) : -1; 175 | } 176 | /** 177 | * Returns the DOS timestamp for the entry. 178 | * 179 | * @returns {number} 180 | */ 181 | getTimeDos() { 182 | return this.time !== -1 ? this.time : 0; 183 | } 184 | /** 185 | * Returns the UNIX file permissions for the entry. 186 | * 187 | * @returns {number} 188 | */ 189 | getUnixMode() { 190 | return this.platform !== PLATFORM_UNIX 191 | ? 0 192 | : (this.getExternalAttributes() >> SHORT_SHIFT) & SHORT_MASK; 193 | } 194 | /** 195 | * Returns the version of ZIP needed to extract the entry. 196 | * 197 | * @returns {number} 198 | */ 199 | getVersionNeededToExtract() { 200 | return this.minver; 201 | } 202 | /** 203 | * Sets the comment of the entry. 204 | * 205 | * @param comment 206 | */ 207 | setComment(comment) { 208 | if (Buffer.byteLength(comment) !== comment.length) { 209 | this.getGeneralPurposeBit().useUTF8ForNames(true); 210 | } 211 | this.comment = comment; 212 | } 213 | /** 214 | * Sets the compressed size of the entry. 215 | * 216 | * @param size 217 | */ 218 | setCompressedSize(size) { 219 | if (size < 0) { 220 | throw new Error("invalid entry compressed size"); 221 | } 222 | this.csize = size; 223 | } 224 | /** 225 | * Sets the checksum of the entry. 226 | * 227 | * @param crc 228 | */ 229 | setCrc(crc) { 230 | if (crc < 0) { 231 | throw new Error("invalid entry crc32"); 232 | } 233 | this.crc = crc; 234 | } 235 | /** 236 | * Sets the external file attributes of the entry. 237 | * 238 | * @param attr 239 | */ 240 | setExternalAttributes(attr) { 241 | this.exattr = attr >>> 0; 242 | } 243 | /** 244 | * Sets the extra fields related to the entry. 245 | * 246 | * @param extra 247 | */ 248 | setExtra(extra) { 249 | this.extra = extra; 250 | } 251 | /** 252 | * Sets the general purpose bits related to the entry. 253 | * 254 | * @param gpb 255 | */ 256 | setGeneralPurposeBit(gpb) { 257 | if (!(gpb instanceof GeneralPurposeBit)) { 258 | throw new Error("invalid entry GeneralPurposeBit"); 259 | } 260 | this.gpb = gpb; 261 | } 262 | /** 263 | * Sets the internal file attributes of the entry. 264 | * 265 | * @param attr 266 | */ 267 | setInternalAttributes(attr) { 268 | this.inattr = attr; 269 | } 270 | /** 271 | * Sets the compression method of the entry. 272 | * 273 | * @param method 274 | */ 275 | setMethod(method) { 276 | if (method < 0) { 277 | throw new Error("invalid entry compression method"); 278 | } 279 | this.method = method; 280 | } 281 | /** 282 | * Sets the name of the entry. 283 | * 284 | * @param name 285 | * @param prependSlash 286 | */ 287 | setName(name, prependSlash = false) { 288 | name = normalizePath(name, false) 289 | .replace(/^\w+:/, "") 290 | .replace(/^(\.\.\/|\/)+/, ""); 291 | if (prependSlash) { 292 | name = `/${name}`; 293 | } 294 | if (Buffer.byteLength(name) !== name.length) { 295 | this.getGeneralPurposeBit().useUTF8ForNames(true); 296 | } 297 | this.name = name; 298 | } 299 | /** 300 | * Sets the platform on which the entry was made. 301 | * 302 | * @param platform 303 | */ 304 | setPlatform(platform) { 305 | this.platform = platform; 306 | } 307 | /** 308 | * Sets the size of the entry. 309 | * 310 | * @param size 311 | */ 312 | setSize(size) { 313 | if (size < 0) { 314 | throw new Error("invalid entry size"); 315 | } 316 | this.size = size; 317 | } 318 | /** 319 | * Sets the time of the entry. 320 | * 321 | * @param time 322 | * @param forceLocalTime 323 | */ 324 | setTime(time, forceLocalTime) { 325 | if (!(time instanceof Date)) { 326 | throw new Error("invalid entry time"); 327 | } 328 | this.time = dateToDos(time, forceLocalTime); 329 | } 330 | /** 331 | * Sets the UNIX file permissions for the entry. 332 | * 333 | * @param mode 334 | */ 335 | setUnixMode(mode) { 336 | mode |= this.isDirectory() ? S_IFDIR : S_IFREG; 337 | var extattr = 0; 338 | extattr |= (mode << SHORT_SHIFT) | (this.isDirectory() ? S_DOS_D : S_DOS_A); 339 | this.setExternalAttributes(extattr); 340 | this.mode = mode & MODE_MASK; 341 | this.platform = PLATFORM_UNIX; 342 | } 343 | /** 344 | * Sets the version of ZIP needed to extract this entry. 345 | * 346 | * @param minver 347 | */ 348 | setVersionNeededToExtract(minver) { 349 | this.minver = minver; 350 | } 351 | /** 352 | * Returns true if this entry represents a directory. 353 | * 354 | * @returns {boolean} 355 | */ 356 | isDirectory() { 357 | return this.getName().slice(-1) === "/"; 358 | } 359 | /** 360 | * Returns true if this entry represents a unix symlink, 361 | * in which case the entry's content contains the target path 362 | * for the symlink. 363 | * 364 | * @returns {boolean} 365 | */ 366 | isUnixSymlink() { 367 | return ( 368 | (this.getUnixMode() & UnixStat.FILE_TYPE_FLAG) === UnixStat.LINK_FLAG 369 | ); 370 | } 371 | 372 | /** 373 | * Returns true if this entry is using the ZIP64 extension of ZIP. 374 | * 375 | * @returns {boolean} 376 | */ 377 | isZip64() { 378 | return this.csize > ZIP64_MAGIC || this.size > ZIP64_MAGIC; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /test/zip-archive-entry.js: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import { ZipArchiveEntry } from "../lib/compress-commons.js"; 3 | import GeneralPurposeBit from "../lib/archivers/zip/general-purpose-bit.js"; 4 | import UnixStat from "../lib/archivers/zip/unix-stat.js"; 5 | /*global before,describe,it */ 6 | 7 | var entry; 8 | // Jan 03 2013 14:26:38 GMT 9 | var testDate = new Date(Date.UTC(2013, 0, 3, 14, 26, 38, 0)); 10 | 11 | describe("ZipArchiveEntry", function () { 12 | beforeEach(function () { 13 | entry = new ZipArchiveEntry("file.txt"); 14 | }); 15 | // Getters 16 | describe("#getCentralDirectoryExtra", function () { 17 | it.skip("should be tested", function () {}); 18 | }); 19 | describe("#getComment", function () { 20 | it("should return the comment", function () { 21 | entry.setComment("file comment"); 22 | assert.equal(entry.getComment(), "file comment"); 23 | }); 24 | }); 25 | describe("#getCompressedSize", function () { 26 | it("should return the compressed size", function () { 27 | entry.csize = 10; 28 | assert.equal(entry.getCompressedSize(), 10); 29 | }); 30 | }); 31 | describe("#getCrc", function () { 32 | it("should return the CRC32", function () { 33 | entry.crc = 585446183; 34 | assert.equal(entry.getCrc(), 585446183); 35 | }); 36 | }); 37 | describe("#getExternalAttributes", function () { 38 | it("should return the external attributes", function () { 39 | entry.exattr = 2180972576; 40 | assert.equal(entry.getExternalAttributes(), 2180972576); 41 | }); 42 | }); 43 | describe("#getExtra", function () { 44 | it.skip("should be tested", function () {}); 45 | }); 46 | describe("#getGeneralPurposeBit", function () { 47 | it("should return the general purpose bit flag", function () { 48 | var gpb = new GeneralPurposeBit(); 49 | gpb.useDataDescriptor(true); 50 | entry.gpb = gpb; 51 | assert.equal(entry.getGeneralPurposeBit(), gpb); 52 | }); 53 | }); 54 | describe("#getInternalAttributes", function () { 55 | it("should return the internal attributes", function () { 56 | entry.inattr = 2180972576; 57 | assert.equal(entry.getInternalAttributes(), 2180972576); 58 | }); 59 | }); 60 | describe("#getLastModifiedDate", function () { 61 | it.skip("should be tested", function () {}); 62 | }); 63 | describe("#getLocalFileDataExtra", function () { 64 | it.skip("should be tested", function () {}); 65 | }); 66 | describe("#getMethod", function () { 67 | it("should return the compression method", function () { 68 | entry.method = 0; 69 | assert.equal(entry.getMethod(), 0); 70 | }); 71 | }); 72 | describe("#getName", function () { 73 | it("should return the name", function () { 74 | entry.name = "file.txt"; 75 | assert.equal(entry.getName(), "file.txt"); 76 | }); 77 | }); 78 | describe("#getPlatform", function () { 79 | it("should return the platform", function () { 80 | entry.platform = 3; 81 | assert.equal(entry.getPlatform(), 3); 82 | }); 83 | }); 84 | describe("#getSize", function () { 85 | it("should return the size", function () { 86 | entry.size = 25; 87 | assert.equal(entry.getSize(), 25); 88 | }); 89 | }); 90 | describe("#getTime", function () { 91 | it("should return a Date object", function () { 92 | entry.time = 1109607251; 93 | assert.typeOf(entry.getTime(), "Date"); 94 | }); 95 | }); 96 | describe("#getTimeDos", function () { 97 | it("should return a number", function () { 98 | entry.time = 1109607251; 99 | assert.typeOf(entry.getTimeDos(), "number"); 100 | }); 101 | }); 102 | describe("#getUnixMode", function () { 103 | it("should return the unix filemode", function () { 104 | entry.mode = 511; // 0777 105 | entry.exattr = 2180972576; 106 | entry.platform = 3; 107 | assert.equal(entry.getUnixMode(), 33279); // 0100777 108 | }); 109 | it("should set proper external attributes for an unix directory", function () { 110 | entry = new ZipArchiveEntry("directory/"); 111 | entry.setUnixMode(511); // 0777 112 | assert.ok(entry.getPlatform(), 3); 113 | assert.ok(entry.isDirectory()); 114 | var exattr = entry.getExternalAttributes() >> 16; 115 | assert.equal(exattr & 16384, 16384); // 040000 116 | }); 117 | }); 118 | describe("#getVersionNeededToExtract", function () { 119 | it.skip("should be tested", function () {}); 120 | }); 121 | // Setters 122 | describe("#setComment", function () { 123 | it("should set internal variable", function () { 124 | entry.setComment("file comment"); 125 | assert.propertyVal(entry, "comment", "file comment"); 126 | }); 127 | it("should set utf8 bit when receiving strings byte count != string length", function () { 128 | entry.setComment("ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäçèéêëìíîïñòóôõöùúûüýÿ"); 129 | assert.ok(entry.getGeneralPurposeBit().usesUTF8ForNames()); 130 | }); 131 | }); 132 | describe("#setCompressedSize", function () { 133 | it("should set internal variable", function () { 134 | entry.setCompressedSize(10); 135 | assert.propertyVal(entry, "csize", 10); 136 | }); 137 | }); 138 | describe("#setCrc", function () { 139 | it("should set internal variable", function () { 140 | entry.setCrc(585446183); 141 | assert.propertyVal(entry, "crc", 585446183); 142 | }); 143 | }); 144 | describe("#setExternalAttributes", function () { 145 | it("should set internal variable", function () { 146 | entry.setExternalAttributes(2180972576); 147 | assert.propertyVal(entry, "exattr", 2180972576); 148 | }); 149 | }); 150 | describe("#setExtra", function () { 151 | it.skip("should be tested", function () {}); 152 | }); 153 | describe("#setGeneralPurposeBit", function () { 154 | it("should set internal variable", function () { 155 | var gpb = new GeneralPurposeBit(); 156 | gpb.useDataDescriptor(true); 157 | entry.setGeneralPurposeBit(gpb); 158 | assert.propertyVal(entry, "gpb", gpb); 159 | }); 160 | }); 161 | describe("#setInternalAttributes", function () { 162 | it("should set internal variable", function () { 163 | entry.setInternalAttributes(2180972576); 164 | assert.propertyVal(entry, "inattr", 2180972576); 165 | }); 166 | }); 167 | describe("#setMethod", function () { 168 | it("should set internal variable", function () { 169 | entry.setMethod(8); 170 | assert.propertyVal(entry, "method", 8); 171 | }); 172 | }); 173 | describe("#setName", function () { 174 | it("should set internal variable", function () { 175 | entry.setName("file.txt"); 176 | assert.propertyVal(entry, "name", "file.txt"); 177 | }); 178 | it("should allow setting prefix of / at the beginning of path", function () { 179 | entry.setName("file.txt", true); 180 | assert.propertyVal(entry, "name", "/file.txt"); 181 | }); 182 | it("should allow ./ at the beginning of path", function () { 183 | entry.setName("./file.txt"); 184 | assert.propertyVal(entry, "name", "./file.txt"); 185 | }); 186 | it("should clean windows style paths", function () { 187 | entry.setName("\\windows\\file.txt"); 188 | assert.propertyVal(entry, "name", "windows/file.txt"); 189 | entry.setName("c:\\this\\path\\file.txt"); 190 | assert.propertyVal(entry, "name", "this/path/file.txt"); 191 | entry.setName("\\\\server\\share\\"); 192 | assert.propertyVal(entry, "name", "server/share/"); 193 | }); 194 | it("should clean multiple forward slashes at beginning of path", function () { 195 | entry.setName("//forward/file.txt"); 196 | assert.propertyVal(entry, "name", "forward/file.txt"); 197 | }); 198 | it("should set utf8 bit when receiving strings byte count != string length", function () { 199 | entry.setName("ÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝàáâãäçèéêëìíîïñòóôõöùúûüýÿ.txt"); 200 | assert.ok(entry.getGeneralPurposeBit().usesUTF8ForNames()); 201 | }); 202 | }); 203 | describe("#setPlatform", function () { 204 | it("should set internal variable", function () { 205 | entry.setPlatform(3); 206 | assert.propertyVal(entry, "platform", 3); 207 | }); 208 | }); 209 | describe("#setSize", function () { 210 | it("should set internal variable", function () { 211 | entry.setSize(15); 212 | assert.propertyVal(entry, "size", 15); 213 | }); 214 | }); 215 | describe("#setTime", function () { 216 | it("should set internal variable", function () { 217 | entry.setTime(testDate); 218 | assert.propertyVal(entry, "time", 1109619539); 219 | }); 220 | }); 221 | describe("#setUnixMode", function () { 222 | it("should set internal variables", function () { 223 | entry.setUnixMode(511); 224 | assert.propertyVal(entry, "exattr", 2180972576); 225 | assert.propertyVal(entry, "mode", 511); // 0777 226 | assert.equal(entry.getUnixMode(), 33279); // 0100777 227 | }); 228 | it("should also preserve filetype information", function () { 229 | entry.setUnixMode(41453); 230 | assert.propertyVal(entry, "exattr", 2716663840); 231 | assert.propertyVal(entry, "mode", 493); // 0755 232 | assert.equal(entry.getUnixMode(), 41453); // 0120755 233 | }); 234 | }); 235 | describe("#setVersionNeededToExtract", function () { 236 | it.skip("should be tested", function () {}); 237 | }); 238 | // Others 239 | describe("#isDirectory", function () { 240 | it("should return a boolean based on name of entry", function () { 241 | assert.notOk(entry.isDirectory()); 242 | entry.setName("some/directory/"); 243 | assert.ok(entry.isDirectory()); 244 | }); 245 | }); 246 | describe("#isUnixSymlink", function () { 247 | it("should return a boolean if the entry is a symlink", function () { 248 | entry.setUnixMode(UnixStat.LINK_FLAG); 249 | assert.ok(entry.isUnixSymlink()); 250 | entry.setUnixMode(UnixStat.LINK_FLAG | UnixStat.DIR_FLAG); 251 | assert.notOk(entry.isUnixSymlink()); 252 | }); 253 | }); 254 | }); 255 | -------------------------------------------------------------------------------- /lib/archivers/zip/zip-archive-output-stream.js: -------------------------------------------------------------------------------- 1 | import { inherits } from "util"; 2 | import crc32 from "crc-32"; 3 | import { CRC32Stream, DeflateCRC32Stream } from "crc32-stream"; 4 | import ArchiveOutputStream from "../archive-output-stream.js"; 5 | import ZipArchiveEntry from "./zip-archive-entry.js"; 6 | import GeneralPurposeBit from "./general-purpose-bit.js"; 7 | import { 8 | LONG_ZERO, 9 | METHOD_DEFLATED, 10 | METHOD_STORED, 11 | MIN_VERSION_DATA_DESCRIPTOR, 12 | MIN_VERSION_ZIP64, 13 | SHORT_ZERO, 14 | SIG_EOCD, 15 | SIG_DD, 16 | SIG_CFH, 17 | SIG_LFH, 18 | SIG_ZIP64_EOCD, 19 | SIG_ZIP64_EOCD_LOC, 20 | VERSION_MADEBY, 21 | ZIP64_EXTRA_ID, 22 | ZIP64_MAGIC, 23 | ZIP64_MAGIC_SHORT, 24 | ZLIB_BEST_SPEED, 25 | } from "./constants.js"; 26 | import { getEightBytes, getLongBytes, getShortBytes } from "./util.js"; 27 | 28 | function _defaults(o) { 29 | if (typeof o !== "object") { 30 | o = {}; 31 | } 32 | if (typeof o.zlib !== "object") { 33 | o.zlib = {}; 34 | } 35 | if (typeof o.zlib.level !== "number") { 36 | o.zlib.level = ZLIB_BEST_SPEED; 37 | } 38 | o.forceZip64 = !!o.forceZip64; 39 | o.forceLocalTime = !!o.forceLocalTime; 40 | return o; 41 | } 42 | 43 | export default class ZipArchiveOutputStream extends ArchiveOutputStream { 44 | constructor(options) { 45 | const _options = _defaults(options); 46 | super(_options); 47 | this.options = _options; 48 | this._entry = null; 49 | this._entries = []; 50 | this._archive = { 51 | centralLength: 0, 52 | centralOffset: 0, 53 | comment: "", 54 | finish: false, 55 | finished: false, 56 | processing: false, 57 | forceZip64: _options.forceZip64, 58 | forceLocalTime: _options.forceLocalTime, 59 | }; 60 | } 61 | 62 | _afterAppend(ae) { 63 | this._entries.push(ae); 64 | if (ae.getGeneralPurposeBit().usesDataDescriptor()) { 65 | this._writeDataDescriptor(ae); 66 | } 67 | this._archive.processing = false; 68 | this._entry = null; 69 | if (this._archive.finish && !this._archive.finished) { 70 | this._finish(); 71 | } 72 | } 73 | 74 | _appendBuffer(ae, source, callback) { 75 | if (source.length === 0) { 76 | ae.setMethod(METHOD_STORED); 77 | } 78 | var method = ae.getMethod(); 79 | if (method === METHOD_STORED) { 80 | ae.setSize(source.length); 81 | ae.setCompressedSize(source.length); 82 | ae.setCrc(crc32.buf(source) >>> 0); 83 | } 84 | this._writeLocalFileHeader(ae); 85 | if (method === METHOD_STORED) { 86 | this.write(source); 87 | this._afterAppend(ae); 88 | callback(null, ae); 89 | return; 90 | } else if (method === METHOD_DEFLATED) { 91 | this._smartStream(ae, callback).end(source); 92 | return; 93 | } else { 94 | callback(new Error("compression method " + method + " not implemented")); 95 | return; 96 | } 97 | } 98 | 99 | _appendStream(ae, source, callback) { 100 | ae.getGeneralPurposeBit().useDataDescriptor(true); 101 | ae.setVersionNeededToExtract(MIN_VERSION_DATA_DESCRIPTOR); 102 | this._writeLocalFileHeader(ae); 103 | var smart = this._smartStream(ae, callback); 104 | source.once("error", function (err) { 105 | smart.emit("error", err); 106 | smart.end(); 107 | }); 108 | source.pipe(smart); 109 | } 110 | 111 | _finish() { 112 | this._archive.centralOffset = this.offset; 113 | this._entries.forEach( 114 | function (ae) { 115 | this._writeCentralFileHeader(ae); 116 | }.bind(this), 117 | ); 118 | this._archive.centralLength = this.offset - this._archive.centralOffset; 119 | if (this.isZip64()) { 120 | this._writeCentralDirectoryZip64(); 121 | } 122 | this._writeCentralDirectoryEnd(); 123 | this._archive.processing = false; 124 | this._archive.finish = true; 125 | this._archive.finished = true; 126 | this.end(); 127 | } 128 | 129 | _normalizeEntry(ae) { 130 | if (ae.getMethod() === -1) { 131 | ae.setMethod(METHOD_DEFLATED); 132 | } 133 | if (ae.getMethod() === METHOD_DEFLATED) { 134 | ae.getGeneralPurposeBit().useDataDescriptor(true); 135 | ae.setVersionNeededToExtract(MIN_VERSION_DATA_DESCRIPTOR); 136 | } 137 | if (ae.getTime() === -1) { 138 | ae.setTime(new Date(), this._archive.forceLocalTime); 139 | } 140 | ae._offsets = { 141 | file: 0, 142 | data: 0, 143 | contents: 0, 144 | }; 145 | } 146 | 147 | _smartStream(ae, callback) { 148 | var deflate = ae.getMethod() === METHOD_DEFLATED; 149 | var process = deflate 150 | ? new DeflateCRC32Stream(this.options.zlib) 151 | : new CRC32Stream(); 152 | var error = null; 153 | function handleStuff() { 154 | var digest = process.digest().readUInt32BE(0); 155 | ae.setCrc(digest); 156 | ae.setSize(process.size()); 157 | ae.setCompressedSize(process.size(true)); 158 | this._afterAppend(ae); 159 | callback(error, ae); 160 | } 161 | process.once("end", handleStuff.bind(this)); 162 | process.once("error", function (err) { 163 | error = err; 164 | }); 165 | process.pipe(this, { end: false }); 166 | return process; 167 | } 168 | 169 | _writeCentralDirectoryEnd() { 170 | var records = this._entries.length; 171 | var size = this._archive.centralLength; 172 | var offset = this._archive.centralOffset; 173 | if (this.isZip64()) { 174 | records = ZIP64_MAGIC_SHORT; 175 | size = ZIP64_MAGIC; 176 | offset = ZIP64_MAGIC; 177 | } 178 | // signature 179 | this.write(getLongBytes(SIG_EOCD)); 180 | // disk numbers 181 | this.write(SHORT_ZERO); 182 | this.write(SHORT_ZERO); 183 | // number of entries 184 | this.write(getShortBytes(records)); 185 | this.write(getShortBytes(records)); 186 | // length and location of CD 187 | this.write(getLongBytes(size)); 188 | this.write(getLongBytes(offset)); 189 | // archive comment 190 | var comment = this.getComment(); 191 | var commentLength = Buffer.byteLength(comment); 192 | this.write(getShortBytes(commentLength)); 193 | this.write(comment); 194 | } 195 | 196 | _writeCentralDirectoryZip64() { 197 | // signature 198 | this.write(getLongBytes(SIG_ZIP64_EOCD)); 199 | // size of the ZIP64 EOCD record 200 | this.write(getEightBytes(44)); 201 | // version made by 202 | this.write(getShortBytes(MIN_VERSION_ZIP64)); 203 | // version to extract 204 | this.write(getShortBytes(MIN_VERSION_ZIP64)); 205 | // disk numbers 206 | this.write(LONG_ZERO); 207 | this.write(LONG_ZERO); 208 | // number of entries 209 | this.write(getEightBytes(this._entries.length)); 210 | this.write(getEightBytes(this._entries.length)); 211 | // length and location of CD 212 | this.write(getEightBytes(this._archive.centralLength)); 213 | this.write(getEightBytes(this._archive.centralOffset)); 214 | // extensible data sector 215 | // not implemented at this time 216 | // end of central directory locator 217 | this.write(getLongBytes(SIG_ZIP64_EOCD_LOC)); 218 | // disk number holding the ZIP64 EOCD record 219 | this.write(LONG_ZERO); 220 | // relative offset of the ZIP64 EOCD record 221 | this.write( 222 | getEightBytes(this._archive.centralOffset + this._archive.centralLength), 223 | ); 224 | // total number of disks 225 | this.write(getLongBytes(1)); 226 | } 227 | 228 | _writeCentralFileHeader(ae) { 229 | var gpb = ae.getGeneralPurposeBit(); 230 | var method = ae.getMethod(); 231 | var fileOffset = ae._offsets.file; 232 | var size = ae.getSize(); 233 | var compressedSize = ae.getCompressedSize(); 234 | if (ae.isZip64() || fileOffset > ZIP64_MAGIC) { 235 | size = ZIP64_MAGIC; 236 | compressedSize = ZIP64_MAGIC; 237 | fileOffset = ZIP64_MAGIC; 238 | ae.setVersionNeededToExtract(MIN_VERSION_ZIP64); 239 | var extraBuf = Buffer.concat( 240 | [ 241 | getShortBytes(ZIP64_EXTRA_ID), 242 | getShortBytes(24), 243 | getEightBytes(ae.getSize()), 244 | getEightBytes(ae.getCompressedSize()), 245 | getEightBytes(ae._offsets.file), 246 | ], 247 | 28, 248 | ); 249 | ae.setExtra(extraBuf); 250 | } 251 | // signature 252 | this.write(getLongBytes(SIG_CFH)); 253 | // version made by 254 | this.write(getShortBytes((ae.getPlatform() << 8) | VERSION_MADEBY)); 255 | // version to extract and general bit flag 256 | this.write(getShortBytes(ae.getVersionNeededToExtract())); 257 | this.write(gpb.encode()); 258 | // compression method 259 | this.write(getShortBytes(method)); 260 | // datetime 261 | this.write(getLongBytes(ae.getTimeDos())); 262 | // crc32 checksum 263 | this.write(getLongBytes(ae.getCrc())); 264 | // sizes 265 | this.write(getLongBytes(compressedSize)); 266 | this.write(getLongBytes(size)); 267 | var name = ae.getName(); 268 | var comment = ae.getComment(); 269 | var extra = ae.getCentralDirectoryExtra(); 270 | if (gpb.usesUTF8ForNames()) { 271 | name = Buffer.from(name); 272 | comment = Buffer.from(comment); 273 | } 274 | // name length 275 | this.write(getShortBytes(name.length)); 276 | // extra length 277 | this.write(getShortBytes(extra.length)); 278 | // comments length 279 | this.write(getShortBytes(comment.length)); 280 | // disk number start 281 | this.write(SHORT_ZERO); 282 | // internal attributes 283 | this.write(getShortBytes(ae.getInternalAttributes())); 284 | // external attributes 285 | this.write(getLongBytes(ae.getExternalAttributes())); 286 | // relative offset of LFH 287 | this.write(getLongBytes(fileOffset)); 288 | // name 289 | this.write(name); 290 | // extra 291 | this.write(extra); 292 | // comment 293 | this.write(comment); 294 | } 295 | 296 | _writeDataDescriptor(ae) { 297 | // signature 298 | this.write(getLongBytes(SIG_DD)); 299 | // crc32 checksum 300 | this.write(getLongBytes(ae.getCrc())); 301 | // sizes 302 | if (ae.isZip64()) { 303 | this.write(getEightBytes(ae.getCompressedSize())); 304 | this.write(getEightBytes(ae.getSize())); 305 | } else { 306 | this.write(getLongBytes(ae.getCompressedSize())); 307 | this.write(getLongBytes(ae.getSize())); 308 | } 309 | } 310 | 311 | _writeLocalFileHeader(ae) { 312 | var gpb = ae.getGeneralPurposeBit(); 313 | var method = ae.getMethod(); 314 | var name = ae.getName(); 315 | var extra = ae.getLocalFileDataExtra(); 316 | if (ae.isZip64()) { 317 | gpb.useDataDescriptor(true); 318 | ae.setVersionNeededToExtract(MIN_VERSION_ZIP64); 319 | } 320 | if (gpb.usesUTF8ForNames()) { 321 | name = Buffer.from(name); 322 | } 323 | ae._offsets.file = this.offset; 324 | // signature 325 | this.write(getLongBytes(SIG_LFH)); 326 | // version to extract and general bit flag 327 | this.write(getShortBytes(ae.getVersionNeededToExtract())); 328 | this.write(gpb.encode()); 329 | // compression method 330 | this.write(getShortBytes(method)); 331 | // datetime 332 | this.write(getLongBytes(ae.getTimeDos())); 333 | ae._offsets.data = this.offset; 334 | // crc32 checksum and sizes 335 | if (gpb.usesDataDescriptor()) { 336 | this.write(LONG_ZERO); 337 | this.write(LONG_ZERO); 338 | this.write(LONG_ZERO); 339 | } else { 340 | this.write(getLongBytes(ae.getCrc())); 341 | this.write(getLongBytes(ae.getCompressedSize())); 342 | this.write(getLongBytes(ae.getSize())); 343 | } 344 | // name length 345 | this.write(getShortBytes(name.length)); 346 | // extra length 347 | this.write(getShortBytes(extra.length)); 348 | // name 349 | this.write(name); 350 | // extra 351 | this.write(extra); 352 | ae._offsets.contents = this.offset; 353 | } 354 | 355 | getComment(comment) { 356 | return this._archive.comment !== null ? this._archive.comment : ""; 357 | } 358 | 359 | isZip64() { 360 | return ( 361 | this._archive.forceZip64 || 362 | this._entries.length > ZIP64_MAGIC_SHORT || 363 | this._archive.centralLength > ZIP64_MAGIC || 364 | this._archive.centralOffset > ZIP64_MAGIC 365 | ); 366 | } 367 | 368 | setComment(comment) { 369 | this._archive.comment = comment; 370 | } 371 | } 372 | --------------------------------------------------------------------------------