├── test ├── input │ ├── packthis │ │ ├── emptyfile.txt │ │ ├── dir2 │ │ │ ├── file3.txt │ │ │ └── file2.png │ │ ├── dir1 │ │ │ └── file1.txt │ │ ├── file0.txt │ │ └── .hiddenfile.txt │ ├── packthis-unicode-path │ │ └── dir1 │ │ │ ├── file1.txt │ │ │ └── 女の子.txt │ ├── packthis-glob │ │ ├── x1 │ │ │ └── file1.txt │ │ ├── x2 │ │ │ └── file2.txt │ │ ├── y3 │ │ │ ├── file3.txt │ │ │ ├── x1 │ │ │ │ └── file4.txt │ │ │ └── z1 │ │ │ │ └── x2 │ │ │ │ └── file5.txt │ │ └── z4 │ │ │ └── w1 │ │ │ └── file6.txt │ ├── packthis-subdir │ │ ├── dir1 │ │ │ └── file1.txt │ │ ├── dir2 │ │ │ └── subdir │ │ │ │ ├── file3.txt │ │ │ │ └── file2.png │ │ └── file0.txt │ ├── extractthis-unpack-dir.asar.unpacked │ │ └── dir2 │ │ │ ├── file3.txt │ │ │ └── file2.png │ ├── extractthis.asar │ ├── extractthis-unpack.asar │ ├── extractthis-unpack.asar.unpacked │ │ └── dir2 │ │ │ └── file2.png │ └── extractthis-unpack-dir.asar ├── expected │ ├── extractthis │ │ ├── dir2 │ │ │ ├── file3.txt │ │ │ └── file2.png │ │ ├── emptyfile.txt │ │ ├── file0.txt │ │ └── dir1 │ │ │ └── file1.txt │ ├── packthis-unicode-path-filelist.txt │ ├── packthis.asar │ ├── packthis-unpack.asar │ ├── packthis-transformed.asar │ ├── extractthis-filelist.txt │ ├── packthis-without-hidden.asar │ ├── packthis-unpack-dir-glob.asar │ ├── packthis-unicode-path.asar │ ├── packthis-unpack-dir-globstar.asar │ ├── packthis-unpack.asar.unpacked │ │ └── dir2 │ │ │ └── file2.png │ ├── extractthis-filelist-with-option.txt │ ├── packthis-unpack-dir.asar │ └── packthis-all-unpacked.asar ├── util │ ├── compareFiles.js │ ├── transformStream.js │ └── compareDirectories.js ├── api-spec.js └── cli-spec.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── .travis.yml ├── appveyor.yml ├── snapcraft.yaml ├── lib ├── crawlfs.js ├── snapshot.js ├── filesystem.js ├── disk.js └── asar.js ├── LICENSE.md ├── package.json ├── CHANGELOG.md ├── bin └── asar.js └── README.md /test/input/packthis/emptyfile.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/input/packthis/dir2/file3.txt: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /test/expected/extractthis/dir2/file3.txt: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /test/expected/extractthis/emptyfile.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/input/packthis/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | file one. -------------------------------------------------------------------------------- /test/input/packthis/file0.txt: -------------------------------------------------------------------------------- 1 | file0 content -------------------------------------------------------------------------------- /test/expected/extractthis/file0.txt: -------------------------------------------------------------------------------- 1 | file0 content -------------------------------------------------------------------------------- /test/input/packthis-unicode-path/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/input/packthis-unicode-path/dir1/女の子.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test/input/**/*.txt text eol=lf 2 | -------------------------------------------------------------------------------- /test/expected/extractthis/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | file one. -------------------------------------------------------------------------------- /test/input/packthis-glob/x1/file1.txt: -------------------------------------------------------------------------------- 1 | fileone 2 | -------------------------------------------------------------------------------- /test/input/packthis-glob/x2/file2.txt: -------------------------------------------------------------------------------- 1 | filetwo 2 | -------------------------------------------------------------------------------- /test/input/packthis-glob/y3/file3.txt: -------------------------------------------------------------------------------- 1 | filethree 2 | -------------------------------------------------------------------------------- /test/input/packthis-glob/y3/x1/file4.txt: -------------------------------------------------------------------------------- 1 | filefour 2 | -------------------------------------------------------------------------------- /test/input/packthis-glob/z4/w1/file6.txt: -------------------------------------------------------------------------------- 1 | filesix 2 | -------------------------------------------------------------------------------- /test/input/packthis-subdir/dir1/file1.txt: -------------------------------------------------------------------------------- 1 | file one. -------------------------------------------------------------------------------- /test/input/packthis-subdir/dir2/subdir/file3.txt: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /test/input/packthis-subdir/file0.txt: -------------------------------------------------------------------------------- 1 | file0 content -------------------------------------------------------------------------------- /test/input/packthis-glob/y3/z1/x2/file5.txt: -------------------------------------------------------------------------------- 1 | filefive 2 | -------------------------------------------------------------------------------- /test/input/packthis/.hiddenfile.txt: -------------------------------------------------------------------------------- 1 | This file is hidden -------------------------------------------------------------------------------- /test/input/extractthis-unpack-dir.asar.unpacked/dir2/file3.txt: -------------------------------------------------------------------------------- 1 | 123 -------------------------------------------------------------------------------- /test/expected/packthis-unicode-path-filelist.txt: -------------------------------------------------------------------------------- 1 | /dir1 2 | /dir1/file1.txt 3 | /dir1/女の子.txt -------------------------------------------------------------------------------- /test/expected/packthis.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis.asar -------------------------------------------------------------------------------- /test/input/extractthis.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/extractthis.asar -------------------------------------------------------------------------------- /test/expected/packthis-unpack.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-unpack.asar -------------------------------------------------------------------------------- /test/input/extractthis-unpack.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/extractthis-unpack.asar -------------------------------------------------------------------------------- /test/input/packthis/dir2/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/packthis/dir2/file2.png -------------------------------------------------------------------------------- /test/expected/packthis-transformed.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-transformed.asar -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /build 3 | /tmp 4 | *.swp 5 | *.log 6 | *~ 7 | .DS_Store 8 | .node-version 9 | npm-debug.log 10 | .idea 11 | -------------------------------------------------------------------------------- /test/expected/extractthis-filelist.txt: -------------------------------------------------------------------------------- 1 | /dir1 2 | /dir1/file1.txt 3 | /dir2 4 | /dir2/file2.png 5 | /dir2/file3.txt 6 | /emptyfile.txt 7 | /file0.txt -------------------------------------------------------------------------------- /test/expected/extractthis/dir2/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/extractthis/dir2/file2.png -------------------------------------------------------------------------------- /test/expected/packthis-without-hidden.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-without-hidden.asar -------------------------------------------------------------------------------- /test/expected/packthis-unpack-dir-glob.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-unpack-dir-glob.asar -------------------------------------------------------------------------------- /test/expected/packthis-unicode-path.asar: -------------------------------------------------------------------------------- 1 | tpj{"files":{"dir1":{"files":{"file1.txt":{"size":0,"offset":"0"},"女の子.txt":{"size":0,"offset":"0"}}}}} -------------------------------------------------------------------------------- /test/expected/packthis-unpack-dir-globstar.asar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-unpack-dir-globstar.asar -------------------------------------------------------------------------------- /test/input/packthis-subdir/dir2/subdir/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/packthis-subdir/dir2/subdir/file2.png -------------------------------------------------------------------------------- /test/expected/packthis-unpack.asar.unpacked/dir2/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/expected/packthis-unpack.asar.unpacked/dir2/file2.png -------------------------------------------------------------------------------- /test/input/extractthis-unpack.asar.unpacked/dir2/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/extractthis-unpack.asar.unpacked/dir2/file2.png -------------------------------------------------------------------------------- /test/input/extractthis-unpack-dir.asar.unpacked/dir2/file2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Contrast-Security-OSS/asar/master/test/input/extractthis-unpack-dir.asar.unpacked/dir2/file2.png -------------------------------------------------------------------------------- /test/expected/extractthis-filelist-with-option.txt: -------------------------------------------------------------------------------- 1 | pack : /dir1 2 | pack : /dir1/file1.txt 3 | unpack : /dir2 4 | unpack : /dir2/file2.png 5 | unpack : /dir2/file3.txt 6 | pack : /emptyfile.txt 7 | pack : /file0.txt -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /build 2 | /spec 3 | /tmp 4 | *.coffee 5 | *.log 6 | *~ 7 | *.swp 8 | .DS_Store 9 | .node-version 10 | .npmignore 11 | npm-debug.log 12 | /test 13 | .gitattributes 14 | appveyor.yml 15 | .travis.yml 16 | coffeelint.json 17 | .idea 18 | -------------------------------------------------------------------------------- /test/expected/packthis-unpack-dir.asar: -------------------------------------------------------------------------------- 1 |  {"files":{"dir1":{"files":{"file1.txt":{"size":9,"offset":"0"}}},"dir2":{"unpacked":true,"files":{"file2.png":{"size":182,"unpacked":true},"file3.txt":{"size":3,"unpacked":true}}},"emptyfile.txt":{"size":0,"offset":"9"},"file0.txt":{"size":13,"offset":"9"}}}file one.file0 content -------------------------------------------------------------------------------- /test/input/extractthis-unpack-dir.asar: -------------------------------------------------------------------------------- 1 |  {"files":{"dir1":{"files":{"file1.txt":{"size":9,"offset":"0"}}},"dir2":{"unpacked":true,"files":{"file2.png":{"size":182,"unpacked":true},"file3.txt":{"size":3,"unpacked":true}}},"emptyfile.txt":{"size":0,"offset":"9"},"file0.txt":{"size":13,"offset":"9"}}}file one.file0 content -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | - "10" 6 | 7 | branches: 8 | only: 9 | - master 10 | 11 | install: 12 | - export DISPLAY=':99.0' 13 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 14 | - npm install 15 | 16 | notifications: 17 | email: 18 | on_success: never 19 | on_failure: change 20 | -------------------------------------------------------------------------------- /test/expected/packthis-all-unpacked.asar: -------------------------------------------------------------------------------- 1 | TPI{"files":{".hiddenfile.txt":{"size":19,"unpacked":true},"dir1":{"unpacked":true,"files":{"file1.txt":{"size":9,"unpacked":true}}},"dir2":{"unpacked":true,"files":{"file2.png":{"size":182,"unpacked":true},"file3.txt":{"size":3,"unpacked":true}}},"emptyfile.txt":{"size":0,"unpacked":true},"file0.txt":{"size":13,"unpacked":true}}} -------------------------------------------------------------------------------- /test/util/compareFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const assert = require('assert') 3 | const fs = process.versions.electron ? require('original-fs') : require('fs') 4 | 5 | module.exports = function (filepathA, filepathB) { 6 | const actual = fs.readFileSync(filepathA, 'utf8') 7 | const expected = fs.readFileSync(filepathB, 'utf8') 8 | return assert.equal(actual, expected) 9 | } 10 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: off 2 | 3 | branches: 4 | only: 5 | - master 6 | 7 | skip_tags: true 8 | 9 | environment: 10 | matrix: 11 | - nodejs_version: "6" 12 | - nodejs_version: "8" 13 | - nodejs_version: "10" 14 | 15 | install: 16 | - ps: Install-Product node $env:nodejs_version 17 | - npm install 18 | 19 | test_script: 20 | - node --version 21 | - npm --version 22 | - npm test 23 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: asar 2 | version: git 3 | summary: Manipulate asar archive files 4 | description: | 5 | Asar is a simple extensive archive format, it works like tar that 6 | concatenates all files together without compression, while having 7 | random access support. 8 | 9 | confinement: classic 10 | 11 | parts: 12 | asar: 13 | plugin: nodejs 14 | source: . 15 | 16 | apps: 17 | asar: 18 | command: lib/node_modules/asar/bin/asar.js 19 | -------------------------------------------------------------------------------- /test/util/transformStream.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Transform = require('stream').Transform 3 | const basename = require('path').basename 4 | 5 | class Reverser extends Transform { 6 | constructor () { 7 | super() 8 | this._data = '' 9 | } 10 | 11 | _transform (buf, enc, cb) { 12 | this._data += buf 13 | return cb() 14 | } 15 | 16 | _flush (cb) { 17 | const txt = this._data.toString().split('').reverse().join('') 18 | this.push(txt) 19 | return cb() 20 | } 21 | } 22 | 23 | module.exports = function (filename) { 24 | if (basename(filename) === 'file0.txt') { 25 | return new Reverser() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/crawlfs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const glob = require('glob') 4 | 5 | module.exports = function (dir, options, callback) { 6 | const metadata = {} 7 | return glob(dir, options, function (error, filenames) { 8 | if (error) { return callback(error) } 9 | for (const filename of filenames) { 10 | const stat = fs.lstatSync(filename) 11 | if (stat.isFile()) { 12 | metadata[filename] = {type: 'file', stat: stat} 13 | } else if (stat.isDirectory()) { 14 | metadata[filename] = {type: 'directory', stat: stat} 15 | } else if (stat.isSymbolicLink()) { 16 | metadata[filename] = {type: 'link', stat: stat} 17 | } 18 | } 19 | return callback(null, filenames, metadata) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 GitHub Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "./lib/asar.js", 3 | "name": "asar", 4 | "description": "Creating Electron app packages", 5 | "version": "0.14.8", 6 | "bin": { 7 | "asar": "./bin/asar.js" 8 | }, 9 | "engines": { 10 | "node": ">=4.6" 11 | }, 12 | "license": "MIT", 13 | "homepage": "https://github.com/electron/asar", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/electron/asar.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/electron/asar/issues" 20 | }, 21 | "scripts": { 22 | "test": "xvfb-maybe electron-mocha --reporter spec && mocha --reporter spec && npm run lint", 23 | "lint": "standard" 24 | }, 25 | "standard": { 26 | "env": { 27 | "mocha": true 28 | } 29 | }, 30 | "dependencies": { 31 | "chromium-pickle-js": "^0.2.0", 32 | "commander": "^2.9.0", 33 | "cuint": "^0.2.1", 34 | "glob": "^6.0.4", 35 | "minimatch": "^3.0.3", 36 | "mkdirp": "^0.5.0", 37 | "mksnapshot": "github:Contrast-Security-OSS/node-mksnapshot#v0.3.3", 38 | "tmp": "0.0.28" 39 | }, 40 | "devDependencies": { 41 | "electron": "^1.6.2", 42 | "electron-mocha": "^6.0.4", 43 | "lodash": "^4.2.1", 44 | "mocha": "^5.2.0", 45 | "rimraf": "^2.5.1", 46 | "standard": "^8.6.0", 47 | "xvfb-maybe": "^0.1.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes By Version 2 | 3 | ## 0.14.0 - 2017-11-02 4 | 5 | ### Added 6 | 7 | * Snapcraft metadata (#130) 8 | * `uncache` and `uncacheAll` (#118) 9 | 10 | ### Fixed 11 | 12 | * Use of asar inside of an Electron app (#118) 13 | 14 | ## 0.13.1 - 2017-11-02 15 | 16 | ### Fixed 17 | 18 | - Do not return before the write stream fully closes (#113) 19 | 20 | ## 0.13.0 - 2017-01-09 21 | 22 | ### Changed 23 | 24 | - Dropped support for Node `0.10.0` and `0.12.0`. The minimum supported version 25 | is now Node `4.6.0`. (#100) 26 | - This project was ported from CoffeeScript to JavaScript. The behavior and 27 | APIs should be the same as previous releases. (#100) 28 | 29 | ## 0.12.4 - 2016-12-28 30 | 31 | ### Fixed 32 | 33 | - Unpack glob patterns containing `{}` characters not working properly (#99) 34 | 35 | ## 0.12.3 - 2016-08-29 36 | 37 | ### Fixed 38 | 39 | - Multibyte characters in paths are now supported (#86) 40 | 41 | ## 0.12.2 - 2016-08-22 42 | 43 | ### Fixed 44 | 45 | - Upgraded `minimatch` to `^3.0.3` from `^3.0.0` for [RegExp DOS fix](https://nodesecurity.io/advisories/minimatch_regular-expression-denial-of-service). 46 | 47 | ## 0.12.1 - 2016-07-25 48 | 49 | ### Fixed 50 | 51 | - Fix `Maximum call stack size exceeded` error regression (#80) 52 | 53 | ## 0.12.0 - 2016-07-20 54 | 55 | ### Added 56 | 57 | - Added `transform` option to specify a `stream.Transform` function to the 58 | `createPackageWithOptions` API (#73) 59 | 60 | ## 0.11.0 - 2016-04-06 61 | 62 | ### Fixed 63 | 64 | - Upgraded `mksnapshot` dependency to remove logged `graceful-fs` deprecation 65 | warnings (#61) 66 | -------------------------------------------------------------------------------- /lib/snapshot.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const path = require('path') 4 | const mksnapshot = require('mksnapshot') 5 | const vm = require('vm') 6 | 7 | const stripBOM = function (content) { 8 | if (content.charCodeAt(0) === 0xFEFF) { 9 | content = content.slice(1) 10 | } 11 | return content 12 | } 13 | 14 | const wrapModuleCode = function (script) { 15 | script = script.replace(/^#!.*/, '') 16 | return `(function(exports, require, module, __filename, __dirname) { ${script} \n});` 17 | } 18 | 19 | const dumpObjectToJS = function (content) { 20 | let result = 'var __ATOM_SHELL_SNAPSHOT = {\n' 21 | for (const filename in content) { 22 | const func = content[filename].toString() 23 | result += ` '${filename}': ${func},\n` 24 | } 25 | result += '};\n' 26 | return result 27 | } 28 | 29 | const createSnapshot = function (src, dest, filenames, metadata, options, callback) { 30 | const content = {} 31 | try { 32 | src = path.resolve(src) 33 | for (const filename of filenames) { 34 | const file = metadata[filename] 35 | if ((file.type === 'file' || file.type === 'link') && filename.substr(-3) === '.js') { 36 | const script = wrapModuleCode(stripBOM(fs.readFileSync(filename, 'utf8'))) 37 | const relativeFilename = path.relative(src, filename) 38 | try { 39 | const compiled = vm.runInThisContext(script, {filename: relativeFilename}) 40 | content[relativeFilename] = compiled 41 | } catch (error) { 42 | console.error('Ignoring ' + relativeFilename + ' for ' + error.name) 43 | } 44 | } 45 | } 46 | } catch (error) { 47 | return callback(error) 48 | } 49 | 50 | // run mksnapshot 51 | const str = dumpObjectToJS(content) 52 | const version = options.version 53 | const arch = options.arch 54 | const builddir = options.builddir 55 | let snapshotdir = options.snapshotdir 56 | 57 | if (typeof snapshotdir === 'undefined' || snapshotdir === null) { snapshotdir = path.dirname(dest) } 58 | const target = path.resolve(snapshotdir, 'snapshot_blob.bin') 59 | return mksnapshot(str, target, version, arch, builddir, callback) 60 | } 61 | 62 | module.exports = createSnapshot 63 | -------------------------------------------------------------------------------- /test/util/compareDirectories.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const path = require('path') 4 | 5 | const _ = require('lodash') 6 | 7 | const crawlFilesystem = require('../../lib/crawlfs') 8 | 9 | module.exports = function (dirA, dirB, cb) { 10 | crawlFilesystem(dirA, function (err, pathsA, metadataA) { 11 | if (err != null) return cb(err) 12 | crawlFilesystem(dirB, function (err, pathsB, metadataB) { 13 | if (err != null) return cb(err) 14 | const relativeA = _.map(pathsA, function (pathAItem) { return path.relative(dirA, pathAItem) }) 15 | const relativeB = _.map(pathsB, function (pathBItem) { return path.relative(dirB, pathBItem) }) 16 | const onlyInA = _.difference(relativeA, relativeB) 17 | const onlyInB = _.difference(relativeB, relativeA) 18 | const inBoth = _.intersection(pathsA, pathsB) 19 | const differentFiles = [] 20 | const errorMsgBuilder = [] 21 | err = null 22 | for (let i in inBoth) { 23 | const filename = inBoth[i] 24 | const typeA = metadataA[filename].type 25 | const typeB = metadataB[filename].type 26 | // skip if both are directories 27 | if (typeA === 'directory' && typeB === 'directory') { continue } 28 | // something is wrong if the types don't match up 29 | if (typeA !== typeB) { 30 | differentFiles.push(filename) 31 | continue 32 | } 33 | const fileContentA = fs.readFileSync(path.join(dirA, filename), 'utf8') 34 | const fileContentB = fs.readFileSync(path.join(dirB, filename), 'utf8') 35 | if (fileContentA !== fileContentB) { differentFiles.push(filename) } 36 | } 37 | if (onlyInA.length) { 38 | errorMsgBuilder.push(`\tEntries only in '${dirA}':`) 39 | for (const file of onlyInA) { errorMsgBuilder.push(`\t ${file}`) } 40 | } 41 | if (onlyInB.length) { 42 | errorMsgBuilder.push(`\tEntries only in '${dirB}':`) 43 | for (const file of onlyInB) { errorMsgBuilder.push(`\t ${file}`) } 44 | } 45 | if (differentFiles.length) { 46 | errorMsgBuilder.push('\tDifferent file content:') 47 | for (const file of differentFiles) { errorMsgBuilder.push(`\t ${file}`) } 48 | } 49 | if (errorMsgBuilder.length) { err = new Error('\n' + errorMsgBuilder.join('\n')) } 50 | cb(err) 51 | }) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /bin/asar.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var asar = require('../lib/asar') 3 | var program = require('commander') 4 | 5 | program.version('v' + require('../package.json').version) 6 | .description('Manipulate asar archive files') 7 | 8 | program.command('pack ') 9 | .alias('p') 10 | .description('create asar archive') 11 | .option('--ordering ', 'path to a text file for ordering contents') 12 | .option('--unpack ', 'do not pack files matching glob ') 13 | .option('--unpack-dir ', 'do not pack dirs matching glob or starting with literal ') 14 | .option('--snapshot', 'create snapshot') 15 | .option('--exclude-hidden', 'exclude hidden files') 16 | .option('--sv ', '(snapshot) version of Electron') 17 | .option('--sa ', '(snapshot) arch of Electron') 18 | .option('--sb ', '(snapshot) where to put downloaded files') 19 | .action(function (dir, output, options) { 20 | options = { 21 | unpack: options.unpack, 22 | unpackDir: options.unpackDir, 23 | snapshot: options.snapshot, 24 | ordering: options.ordering, 25 | version: options.sv, 26 | arch: options.sa, 27 | builddir: options.sb, 28 | dot: !options.excludeHidden 29 | } 30 | asar.createPackageWithOptions(dir, output, options, function (error) { 31 | if (error) { 32 | console.error(error.stack) 33 | process.exit(1) 34 | } 35 | }) 36 | }) 37 | 38 | program.command('list ') 39 | .alias('l') 40 | .description('list files of asar archive') 41 | .option('-i, --is-pack', 'each file in the asar is pack or unpack') 42 | .action(function (archive, options) { 43 | options = { 44 | isPack: options.isPack 45 | } 46 | var files = asar.listPackage(archive, options) 47 | for (var i in files) { 48 | console.log(files[i]) 49 | } 50 | // This is in order to disappear help 51 | process.exit(0) 52 | }) 53 | 54 | program.command('extract-file ') 55 | .alias('ef') 56 | .description('extract one file from archive') 57 | .action(function (archive, filename) { 58 | require('fs').writeFileSync(require('path').basename(filename), 59 | asar.extractFile(archive, filename)) 60 | }) 61 | 62 | program.command('extract ') 63 | .alias('e') 64 | .description('extract archive') 65 | .action(function (archive, dest) { 66 | asar.extractAll(archive, dest) 67 | }) 68 | 69 | program.command('*') 70 | .action(function (cmd) { 71 | console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', cmd) 72 | }) 73 | 74 | program.parse(process.argv) 75 | 76 | if (program.args.length === 0) { 77 | program.help() 78 | } 79 | -------------------------------------------------------------------------------- /lib/filesystem.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const path = require('path') 4 | const tmp = require('tmp') 5 | const UINT64 = require('cuint').UINT64 6 | 7 | class Filesystem { 8 | constructor (src) { 9 | this.src = path.resolve(src) 10 | this.header = {files: {}} 11 | this.offset = UINT64(0) 12 | } 13 | 14 | searchNodeFromDirectory (p) { 15 | let json = this.header 16 | const dirs = p.split(path.sep) 17 | for (const dir of dirs) { 18 | if (dir !== '.') { 19 | json = json.files[dir] 20 | } 21 | } 22 | return json 23 | } 24 | 25 | searchNodeFromPath (p) { 26 | p = path.relative(this.src, p) 27 | if (!p) { return this.header } 28 | const name = path.basename(p) 29 | const node = this.searchNodeFromDirectory(path.dirname(p)) 30 | if (node.files == null) { 31 | node.files = {} 32 | } 33 | if (node.files[name] == null) { 34 | node.files[name] = {} 35 | } 36 | return node.files[name] 37 | } 38 | 39 | insertDirectory (p, shouldUnpack) { 40 | const node = this.searchNodeFromPath(p) 41 | if (shouldUnpack) { 42 | node.unpacked = shouldUnpack 43 | } 44 | node.files = {} 45 | return node.files 46 | } 47 | 48 | insertFile (p, shouldUnpack, file, options, callback) { 49 | const dirNode = this.searchNodeFromPath(path.dirname(p)) 50 | const node = this.searchNodeFromPath(p) 51 | if (shouldUnpack || dirNode.unpacked) { 52 | node.size = file.stat.size 53 | node.unpacked = true 54 | process.nextTick(callback) 55 | return 56 | } 57 | 58 | const handler = () => { 59 | const size = file.transformed ? file.transformed.stat.size : file.stat.size 60 | 61 | // JavaScript can not precisely present integers >= UINT32_MAX. 62 | if (size > 4294967295) { 63 | throw new Error(`${p}: file size can not be larger than 4.2GB`) 64 | } 65 | 66 | node.size = size 67 | node.offset = this.offset.toString() 68 | if (process.platform !== 'win32' && (file.stat.mode & 0o100)) { 69 | node.executable = true 70 | } 71 | this.offset.add(UINT64(size)) 72 | 73 | return callback() 74 | } 75 | 76 | const tr = options.transform && options.transform(p) 77 | if (tr) { 78 | return tmp.file(function (err, path) { 79 | if (err) { return handler() } 80 | const out = fs.createWriteStream(path) 81 | const stream = fs.createReadStream(p) 82 | 83 | stream.pipe(tr).pipe(out) 84 | return out.on('close', function () { 85 | file.transformed = { 86 | path, 87 | stat: fs.lstatSync(path) 88 | } 89 | return handler() 90 | }) 91 | }) 92 | } else { 93 | return process.nextTick(handler) 94 | } 95 | } 96 | 97 | insertLink (p) { 98 | const link = path.relative(fs.realpathSync(this.src), fs.realpathSync(p)) 99 | if (link.substr(0, 2) === '..') { 100 | throw new Error(`${p}: file links out of the package`) 101 | } 102 | const node = this.searchNodeFromPath(p) 103 | node.link = link 104 | return link 105 | } 106 | 107 | listFiles (options) { 108 | const files = [] 109 | const fillFilesFromHeader = function (p, json) { 110 | if (!json.files) { 111 | return 112 | } 113 | return (() => { 114 | const result = [] 115 | for (const f in json.files) { 116 | const fullPath = path.join(p, f) 117 | const packState = json.files[f].unpacked === true ? 'unpack' : 'pack ' 118 | files.push((options && options.isPack) ? `${packState} : ${fullPath}` : fullPath) 119 | result.push(fillFilesFromHeader(fullPath, json.files[f])) 120 | } 121 | return result 122 | })() 123 | } 124 | 125 | fillFilesFromHeader('/', this.header) 126 | return files 127 | } 128 | 129 | getNode (p) { 130 | const node = this.searchNodeFromDirectory(path.dirname(p)) 131 | const name = path.basename(p) 132 | if (name) { 133 | return node.files[name] 134 | } else { 135 | return node 136 | } 137 | } 138 | 139 | getFile (p, followLinks) { 140 | followLinks = typeof followLinks === 'undefined' ? true : followLinks 141 | const info = this.getNode(p) 142 | 143 | // if followLinks is false we don't resolve symlinks 144 | if (info.link && followLinks) { 145 | return this.getFile(info.link) 146 | } else { 147 | return info 148 | } 149 | } 150 | } 151 | 152 | module.exports = Filesystem 153 | -------------------------------------------------------------------------------- /lib/disk.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const path = require('path') 4 | const mkdirp = require('mkdirp') 5 | const pickle = require('chromium-pickle-js') 6 | 7 | const Filesystem = require('./filesystem') 8 | let filesystemCache = {} 9 | 10 | const copyFileToSync = function (dest, src, filename) { 11 | const srcFile = path.join(src, filename) 12 | const targetFile = path.join(dest, filename) 13 | 14 | const content = fs.readFileSync(srcFile) 15 | const stats = fs.statSync(srcFile) 16 | mkdirp.sync(path.dirname(targetFile)) 17 | return fs.writeFileSync(targetFile, content, {mode: stats.mode}) 18 | } 19 | 20 | const writeFileListToStream = function (dest, filesystem, out, list, metadata, callback) { 21 | for (let i = 0; i < list.length; i++) { 22 | const file = list[i] 23 | if (file.unpack) { 24 | // the file should not be packed into archive. 25 | const filename = path.relative(filesystem.src, file.filename) 26 | try { 27 | copyFileToSync(`${dest}.unpacked`, filesystem.src, filename) 28 | } catch (error) { 29 | return callback(error) 30 | } 31 | } else { 32 | const tr = metadata[file.filename].transformed 33 | const stream = fs.createReadStream((tr ? tr.path : file.filename)) 34 | stream.pipe(out, {end: false}) 35 | stream.on('error', callback) 36 | return stream.on('end', function () { 37 | return writeFileListToStream(dest, filesystem, out, list.slice(i + 1), metadata, callback) 38 | }) 39 | } 40 | } 41 | out.end() 42 | return callback(null) 43 | } 44 | 45 | module.exports.writeFilesystem = function (dest, filesystem, files, metadata, callback) { 46 | let sizeBuf 47 | let headerBuf 48 | try { 49 | const headerPickle = pickle.createEmpty() 50 | headerPickle.writeString(JSON.stringify(filesystem.header)) 51 | headerBuf = headerPickle.toBuffer() 52 | 53 | const sizePickle = pickle.createEmpty() 54 | sizePickle.writeUInt32(headerBuf.length) 55 | sizeBuf = sizePickle.toBuffer() 56 | } catch (error) { 57 | return callback(error) 58 | } 59 | 60 | const out = fs.createWriteStream(dest) 61 | out.on('error', callback) 62 | out.write(sizeBuf) 63 | return out.write(headerBuf, function () { 64 | return writeFileListToStream(dest, filesystem, out, files, metadata, callback) 65 | }) 66 | } 67 | 68 | module.exports.readArchiveHeaderSync = function (archive) { 69 | const fd = fs.openSync(archive, 'r') 70 | let size 71 | let headerBuf 72 | try { 73 | const sizeBuf = new Buffer(8) 74 | if (fs.readSync(fd, sizeBuf, 0, 8, null) !== 8) { 75 | throw new Error('Unable to read header size') 76 | } 77 | 78 | const sizePickle = pickle.createFromBuffer(sizeBuf) 79 | size = sizePickle.createIterator().readUInt32() 80 | headerBuf = new Buffer(size) 81 | if (fs.readSync(fd, headerBuf, 0, size, null) !== size) { 82 | throw new Error('Unable to read header') 83 | } 84 | } finally { 85 | fs.closeSync(fd) 86 | } 87 | 88 | const headerPickle = pickle.createFromBuffer(headerBuf) 89 | const header = headerPickle.createIterator().readString() 90 | return {header: JSON.parse(header), headerSize: size} 91 | } 92 | 93 | module.exports.readFilesystemSync = function (archive) { 94 | if (!filesystemCache[archive]) { 95 | const header = this.readArchiveHeaderSync(archive) 96 | const filesystem = new Filesystem(archive) 97 | filesystem.header = header.header 98 | filesystem.headerSize = header.headerSize 99 | filesystemCache[archive] = filesystem 100 | } 101 | return filesystemCache[archive] 102 | } 103 | 104 | module.exports.uncacheFilesystem = function (archive) { 105 | if (filesystemCache[archive]) { 106 | filesystemCache[archive] = undefined 107 | return true 108 | } 109 | return false 110 | } 111 | 112 | module.exports.uncacheAll = function () { 113 | filesystemCache = {} 114 | } 115 | 116 | module.exports.readFileSync = function (filesystem, filename, info) { 117 | let buffer = new Buffer(info.size) 118 | if (info.size <= 0) { return buffer } 119 | if (info.unpacked) { 120 | // it's an unpacked file, copy it. 121 | buffer = fs.readFileSync(path.join(`${filesystem.src}.unpacked`, filename)) 122 | } else { 123 | // Node throws an exception when reading 0 bytes into a 0-size buffer, 124 | // so we short-circuit the read in this case. 125 | const fd = fs.openSync(filesystem.src, 'r') 126 | try { 127 | const offset = 8 + filesystem.headerSize + parseInt(info.offset) 128 | fs.readSync(fd, buffer, 0, info.size, offset) 129 | } finally { 130 | fs.closeSync(fd) 131 | } 132 | } 133 | return buffer 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # asar - Electron Archive 2 | 3 | [![Travis build status](https://travis-ci.org/electron/asar.svg?branch=master)](https://travis-ci.org/electron/asar) 4 | [![AppVeyor build status](https://ci.appveyor.com/api/projects/status/mrfwfr0uxlbwkuq3?svg=true)](https://ci.appveyor.com/project/electron-bot/asar) 5 | [![dependencies](http://img.shields.io/david/electron/asar.svg?style=flat-square)](https://david-dm.org/electron/asar) 6 | [![npm version](http://img.shields.io/npm/v/asar.svg?style=flat-square)](https://npmjs.org/package/asar) 7 | 8 | Asar is a simple extensive archive format, it works like `tar` that concatenates 9 | all files together without compression, while having random access support. 10 | 11 | ## Features 12 | 13 | * Support random access 14 | * Use JSON to store files' information 15 | * Very easy to write a parser 16 | 17 | ## Command line utility 18 | 19 | ### Install 20 | 21 | ```bash 22 | $ npm install asar 23 | ``` 24 | 25 | ### Usage 26 | 27 | ```bash 28 | $ asar --help 29 | 30 | Usage: asar [options] [command] 31 | 32 | Commands: 33 | 34 | pack|p 35 | create asar archive 36 | 37 | list|l 38 | list files of asar archive 39 | 40 | extract-file|ef 41 | extract one file from archive 42 | 43 | extract|e 44 | extract archive 45 | 46 | 47 | Options: 48 | 49 | -h, --help output usage information 50 | -V, --version output the version number 51 | 52 | ``` 53 | 54 | #### Excluding multiple resources from being packed 55 | 56 | Given: 57 | ``` 58 | app 59 | (a) ├── x1 60 | (b) ├── x2 61 | (c) ├── y3 62 | (d) │   ├── x1 63 | (e) │   └── z1 64 | (f) │   └── x2 65 | (g) └── z4 66 | (h) └── w1 67 | ``` 68 | 69 | Exclude: a, b 70 | ```bash 71 | $ asar pack app app.asar --unpack-dir "{x1,x2}" 72 | ``` 73 | 74 | Exclude: a, b, d, f 75 | ```bash 76 | $ asar pack app app.asar --unpack-dir "**/{x1,x2}" 77 | ``` 78 | 79 | Exclude: a, b, d, f, h 80 | ```bash 81 | $ asar pack app app.asar --unpack-dir "{**/x1,**/x2,z4/w1}" 82 | ``` 83 | 84 | ## Using programatically 85 | 86 | ### Example 87 | 88 | ```js 89 | var asar = require('asar'); 90 | 91 | var src = 'some/path/'; 92 | var dest = 'name.asar'; 93 | 94 | asar.createPackage(src, dest, function() { 95 | console.log('done.'); 96 | }) 97 | ``` 98 | 99 | Please note that there is currently **no** error handling provided! 100 | 101 | ### Transform 102 | You can pass in a `transform` option, that is a function, which either returns 103 | nothing, or a `stream.Transform`. The latter will be used on files that will be 104 | in the `.asar` file to transform them (e.g. compress). 105 | 106 | ```js 107 | var asar = require('asar'); 108 | 109 | var src = 'some/path/'; 110 | var dest = 'name.asar'; 111 | 112 | function transform(filename) { 113 | return new CustomTransformStream() 114 | } 115 | 116 | asar.createPackageWithOptions(src, dest, { transform: transform }, function() { 117 | console.log('done.'); 118 | }) 119 | ``` 120 | 121 | ## Using with grunt 122 | 123 | There is also an unofficial grunt plugin to generate asar archives at [bwin/grunt-asar][grunt-asar]. 124 | 125 | ## Format 126 | 127 | Asar uses [Pickle][pickle] to safely serialize binary value to file, there is 128 | also a [node.js binding][node-pickle] of `Pickle` class. 129 | 130 | The format of asar is very flat: 131 | 132 | ``` 133 | | UInt32: header_size | String: header | Bytes: file1 | ... | Bytes: file42 | 134 | ``` 135 | 136 | The `header_size` and `header` are serialized with [Pickle][pickle] class, and 137 | `header_size`'s [Pickle][pickle] object is 8 bytes. 138 | 139 | The `header` is a JSON string, and the `header_size` is the size of `header`'s 140 | `Pickle` object. 141 | 142 | Structure of `header` is something like this: 143 | 144 | ```json 145 | { 146 | "files": { 147 | "tmp": { 148 | "files": {} 149 | }, 150 | "usr" : { 151 | "files": { 152 | "bin": { 153 | "files": { 154 | "ls": { 155 | "offset": "0", 156 | "size": 100, 157 | "executable": true 158 | }, 159 | "cd": { 160 | "offset": "100", 161 | "size": 100, 162 | "executable": true 163 | } 164 | } 165 | } 166 | } 167 | }, 168 | "etc": { 169 | "files": { 170 | "hosts": { 171 | "offset": "200", 172 | "size": 32 173 | } 174 | } 175 | } 176 | } 177 | } 178 | ``` 179 | 180 | `offset` and `size` records the information to read the file from archive, the 181 | `offset` starts from 0 so you have to manually add the size of `header_size` and 182 | `header` to the `offset` to get the real offset of the file. 183 | 184 | `offset` is a UINT64 number represented in string, because there is no way to 185 | precisely represent UINT64 in JavaScript `Number`. `size` is a JavaScript 186 | `Number` that is no larger than `Number.MAX_SAFE_INTEGER`, which has a value of 187 | `9007199254740991` and is about 8PB in size. We didn't store `size` in UINT64 188 | because file size in Node.js is represented as `Number` and it is not safe to 189 | convert `Number` to UINT64. 190 | 191 | [pickle]: https://chromium.googlesource.com/chromium/src/+/master/base/pickle.h 192 | [node-pickle]: https://www.npmjs.org/package/chromium-pickle 193 | [grunt-asar]: https://github.com/bwin/grunt-asar 194 | -------------------------------------------------------------------------------- /test/api-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const assert = require('assert') 3 | const fs = process.versions.electron ? require('original-fs') : require('fs') 4 | const os = require('os') 5 | const path = require('path') 6 | const rimraf = require('rimraf') 7 | 8 | const asar = require('..') 9 | const compDirs = require('./util/compareDirectories') 10 | const compFiles = require('./util/compareFiles') 11 | const transform = require('./util/transformStream') 12 | 13 | describe('api', function () { 14 | beforeEach(function () { 15 | rimraf.sync(path.join(__dirname, '..', 'tmp'), fs) 16 | }) 17 | it('should create archive from directory', function (done) { 18 | asar.createPackage('test/input/packthis/', 'tmp/packthis-api.asar', function (error) { 19 | if (error != null) return done(error) 20 | done(compFiles('tmp/packthis-api.asar', 'test/expected/packthis.asar')) 21 | }) 22 | }) 23 | if (os.platform() === 'win32') { 24 | it('should create archive with windows-style path separators', function (done) { 25 | asar.createPackage('test\\input\\packthis\\', 'tmp\\packthis-api.asar', function (error) { 26 | if (error != null) return done(error) 27 | done(compFiles('tmp/packthis-api.asar', 'test/expected/packthis.asar')) 28 | }) 29 | }) 30 | } 31 | it('should create archive from directory (without hidden files)', function (done) { 32 | asar.createPackageWithOptions('test/input/packthis/', 'tmp/packthis-without-hidden-api.asar', {dot: false}, function (error) { 33 | if (error != null) return done(error) 34 | done(compFiles('tmp/packthis-without-hidden-api.asar', 'test/expected/packthis-without-hidden.asar')) 35 | }) 36 | }) 37 | it('should create archive from directory (with transformed files)', function (done) { 38 | asar.createPackageWithOptions('test/input/packthis/', 'tmp/packthis-api-transformed.asar', {transform}, function (error) { 39 | if (error != null) return done(error) 40 | done(compFiles('tmp/packthis-api-transformed.asar', 'test/expected/packthis-transformed.asar')) 41 | }) 42 | }) 43 | it('should create archive from directory (with nothing packed)', function (done) { 44 | asar.createPackageWithOptions('test/input/packthis/', 'tmp/packthis-api-unpacked.asar', { unpackDir: '**' }, function (error) { 45 | if (error != null) return done(error) 46 | compFiles('tmp/packthis-api-unpacked.asar', 'test/expected/packthis-all-unpacked.asar') 47 | compDirs('tmp/packthis-api-unpacked.asar.unpacked', 'test/expected/extractthis', done) 48 | }) 49 | }) 50 | it('should list files/dirs in archive', function () { 51 | const actual = asar.listPackage('test/input/extractthis.asar').join('\n') 52 | let expected = fs.readFileSync('test/expected/extractthis-filelist.txt', 'utf8') 53 | // on windows replace slashes with backslashes and crlf with lf 54 | if (os.platform() === 'win32') { 55 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 56 | } 57 | return assert.equal(actual, expected) 58 | }) 59 | it('should list files/dirs in archive with option', function () { 60 | const actual = asar.listPackage('test/input/extractthis-unpack-dir.asar', {isPack: true}).join('\n') 61 | let expected = fs.readFileSync('test/expected/extractthis-filelist-with-option.txt', 'utf8') 62 | // on windows replace slashes with backslashes and crlf with lf 63 | if (os.platform() === 'win32') { 64 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 65 | } 66 | return assert.equal(actual, expected) 67 | }) 68 | it('should extract a text file from archive', function () { 69 | const actual = asar.extractFile('test/input/extractthis.asar', 'dir1/file1.txt').toString('utf8') 70 | let expected = fs.readFileSync('test/expected/extractthis/dir1/file1.txt', 'utf8') 71 | // on windows replace crlf with lf 72 | if (os.platform() === 'win32') { 73 | expected = expected.replace(/\r\n/g, '\n') 74 | } 75 | return assert.equal(actual, expected) 76 | }) 77 | it('should extract a binary file from archive', function () { 78 | const actual = asar.extractFile('test/input/extractthis.asar', 'dir2/file2.png') 79 | const expected = fs.readFileSync('test/expected/extractthis/dir2/file2.png', 'utf8') 80 | return assert.equal(actual, expected) 81 | }) 82 | it('should extract a binary file from archive with unpacked files', function () { 83 | const actual = asar.extractFile('test/input/extractthis-unpack.asar', 'dir2/file2.png') 84 | const expected = fs.readFileSync('test/expected/extractthis/dir2/file2.png', 'utf8') 85 | return assert.equal(actual, expected) 86 | }) 87 | it('should extract an archive', function (done) { 88 | asar.extractAll('test/input/extractthis.asar', 'tmp/extractthis-api/') 89 | compDirs('tmp/extractthis-api/', 'test/expected/extractthis', done) 90 | }) 91 | it('should extract an archive with unpacked files', function (done) { 92 | asar.extractAll('test/input/extractthis-unpack.asar', 'tmp/extractthis-unpack-api/') 93 | compDirs('tmp/extractthis-unpack-api/', 'test/expected/extractthis', done) 94 | }) 95 | it('should extract a binary file from archive with unpacked files', function () { 96 | const actual = asar.extractFile('test/input/extractthis-unpack-dir.asar', 'dir1/file1.txt') 97 | const expected = fs.readFileSync('test/expected/extractthis/dir1/file1.txt', 'utf8') 98 | return assert.equal(actual, expected) 99 | }) 100 | it('should extract an archive with unpacked dirs', function (done) { 101 | asar.extractAll('test/input/extractthis-unpack-dir.asar', 'tmp/extractthis-unpack-dir-api/') 102 | compDirs('tmp/extractthis-unpack-dir-api/', 'test/expected/extractthis', done) 103 | }) 104 | it('should handle multibyte characters in paths', function (done) { 105 | asar.createPackage('test/input/packthis-unicode-path/', 'tmp/packthis-unicode-path.asar', function (error) { 106 | if (error != null) return done(error) 107 | done(compFiles('tmp/packthis-unicode-path.asar', 'test/expected/packthis-unicode-path.asar')) 108 | }) 109 | }) 110 | it('should extract a text file from archive with multibyte characters in path', function () { 111 | const actual = asar.extractFile('test/expected/packthis-unicode-path.asar', 'dir1/女の子.txt').toString('utf8') 112 | let expected = fs.readFileSync('test/input/packthis-unicode-path/dir1/女の子.txt', 'utf8') 113 | // on windows replace crlf with lf 114 | if (os.platform() === 'win32') { 115 | expected = expected.replace(/\r\n/g, '\n') 116 | } 117 | return assert.equal(actual, expected) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /lib/asar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const fs = process.versions.electron ? require('original-fs') : require('fs') 3 | const path = require('path') 4 | const minimatch = require('minimatch') 5 | const mkdirp = require('mkdirp') 6 | 7 | const Filesystem = require('./filesystem') 8 | const disk = require('./disk') 9 | const crawlFilesystem = require('./crawlfs') 10 | const createSnapshot = require('./snapshot') 11 | 12 | // Return whether or not a directory should be excluded from packing due to 13 | // "--unpack-dir" option 14 | // 15 | // @param {string} path - diretory path to check 16 | // @param {string} pattern - literal prefix [for backward compatibility] or glob pattern 17 | // @param {array} unpackDirs - Array of directory paths previously marked as unpacked 18 | // 19 | const isUnpackDir = function (path, pattern, unpackDirs) { 20 | if (path.indexOf(pattern) === 0 || minimatch(path, pattern)) { 21 | if (unpackDirs.indexOf(path) === -1) { 22 | unpackDirs.push(path) 23 | } 24 | return true 25 | } else { 26 | for (let i = 0; i < unpackDirs.length; i++) { 27 | if (path.indexOf(unpackDirs[i]) === 0) { 28 | return true 29 | } 30 | } 31 | return false 32 | } 33 | } 34 | 35 | module.exports.createPackage = function (src, dest, callback) { 36 | return module.exports.createPackageWithOptions(src, dest, {}, callback) 37 | } 38 | 39 | module.exports.createPackageWithOptions = function (src, dest, options, callback) { 40 | const globOptions = options.globOptions ? options.globOptions : {} 41 | globOptions.dot = options.dot === undefined ? true : options.dot 42 | 43 | let pattern = src + '/**/*' 44 | if (options.pattern) { 45 | pattern = src + options.pattern 46 | } 47 | 48 | return crawlFilesystem(pattern, globOptions, function (error, filenames, metadata) { 49 | if (error) { return callback(error) } 50 | module.exports.createPackageFromFiles(src, dest, filenames, metadata, options, callback) 51 | }) 52 | } 53 | 54 | /* 55 | createPackageFromFiles - Create an asar-archive from a list of filenames 56 | src: Base path. All files are relative to this. 57 | dest: Archive filename (& path). 58 | filenames: Array of filenames relative to src. 59 | metadata: Object with filenames as keys and {type='directory|file|link', stat: fs.stat} as values. (Optional) 60 | options: The options. 61 | callback: The callback function. Accepts (err). 62 | */ 63 | module.exports.createPackageFromFiles = function (src, dest, filenames, metadata, options, callback) { 64 | if (typeof metadata === 'undefined' || metadata === null) { metadata = {} } 65 | if (typeof options === 'undefined' || options === null) { options = {} } 66 | 67 | src = path.normalize(src) 68 | dest = path.normalize(dest) 69 | filenames = filenames.map(function (filename) { return path.normalize(filename) }) 70 | 71 | const filesystem = new Filesystem(src) 72 | const files = [] 73 | const unpackDirs = [] 74 | 75 | let filenamesSorted = [] 76 | if (options.ordering) { 77 | const orderingFiles = fs.readFileSync(options.ordering).toString().split('\n').map(function (line) { 78 | if (line.includes(':')) { line = line.split(':').pop() } 79 | line = line.trim() 80 | if (line.startsWith('/')) { line = line.slice(1) } 81 | return line 82 | }) 83 | 84 | const ordering = [] 85 | for (const file of orderingFiles) { 86 | const pathComponents = file.split(path.sep) 87 | let str = src 88 | for (const pathComponent of pathComponents) { 89 | str = path.join(str, pathComponent) 90 | ordering.push(str) 91 | } 92 | } 93 | 94 | let missing = 0 95 | const total = filenames.length 96 | 97 | for (const file of ordering) { 98 | if (!filenamesSorted.includes(file) && filenames.includes(file)) { 99 | filenamesSorted.push(file) 100 | } 101 | } 102 | 103 | for (const file of filenames) { 104 | if (!filenamesSorted.includes(file)) { 105 | filenamesSorted.push(file) 106 | missing += 1 107 | } 108 | } 109 | 110 | console.log(`Ordering file has ${((total - missing) / total) * 100}% coverage.`) 111 | } else { 112 | filenamesSorted = filenames 113 | } 114 | 115 | const handleFile = function (filename, done) { 116 | let file = metadata[filename] 117 | let type 118 | if (!file) { 119 | const stat = fs.lstatSync(filename) 120 | if (stat.isDirectory()) { type = 'directory' } 121 | if (stat.isFile()) { type = 'file' } 122 | if (stat.isSymbolicLink()) { type = 'link' } 123 | file = {stat, type} 124 | metadata[filename] = file 125 | } 126 | 127 | let shouldUnpack 128 | switch (file.type) { 129 | case 'directory': 130 | shouldUnpack = options.unpackDir 131 | ? isUnpackDir(path.relative(src, filename), options.unpackDir, unpackDirs) 132 | : false 133 | filesystem.insertDirectory(filename, shouldUnpack) 134 | break 135 | case 'file': 136 | shouldUnpack = false 137 | if (options.unpack) { 138 | shouldUnpack = minimatch(filename, options.unpack, {matchBase: true}) 139 | } 140 | if (!shouldUnpack && options.unpackDir) { 141 | const dirName = path.relative(src, path.dirname(filename)) 142 | shouldUnpack = isUnpackDir(dirName, options.unpackDir, unpackDirs) 143 | } 144 | files.push({filename: filename, unpack: shouldUnpack}) 145 | filesystem.insertFile(filename, shouldUnpack, file, options, done) 146 | return 147 | case 'link': 148 | filesystem.insertLink(filename, file.stat) 149 | break 150 | } 151 | return process.nextTick(done) 152 | } 153 | 154 | const insertsDone = function () { 155 | return mkdirp(path.dirname(dest), function (error) { 156 | if (error) { return callback(error) } 157 | return disk.writeFilesystem(dest, filesystem, files, metadata, function (error) { 158 | if (error) { return callback(error) } 159 | if (options.snapshot) { 160 | return createSnapshot(src, dest, filenames, metadata, options, callback) 161 | } else { 162 | return callback(null) 163 | } 164 | }) 165 | }) 166 | } 167 | 168 | const names = filenamesSorted.slice() 169 | 170 | const next = function (name) { 171 | if (!name) { return insertsDone() } 172 | 173 | return handleFile(name, function () { 174 | return next(names.shift()) 175 | }) 176 | } 177 | 178 | return next(names.shift()) 179 | } 180 | 181 | module.exports.statFile = function (archive, filename, followLinks) { 182 | const filesystem = disk.readFilesystemSync(archive) 183 | return filesystem.getFile(filename, followLinks) 184 | } 185 | 186 | module.exports.listPackage = function (archive, options) { 187 | return disk.readFilesystemSync(archive).listFiles(options) 188 | } 189 | 190 | module.exports.extractFile = function (archive, filename) { 191 | const filesystem = disk.readFilesystemSync(archive) 192 | return disk.readFileSync(filesystem, filename, filesystem.getFile(filename)) 193 | } 194 | 195 | module.exports.extractAll = function (archive, dest) { 196 | const filesystem = disk.readFilesystemSync(archive) 197 | const filenames = filesystem.listFiles() 198 | 199 | // under windows just extract links as regular files 200 | const followLinks = process.platform === 'win32' 201 | 202 | // create destination directory 203 | mkdirp.sync(dest) 204 | 205 | return filenames.map((filename) => { 206 | filename = filename.substr(1) // get rid of leading slash 207 | const destFilename = path.join(dest, filename) 208 | const file = filesystem.getFile(filename, followLinks) 209 | if (file.files) { 210 | // it's a directory, create it and continue with the next entry 211 | mkdirp.sync(destFilename) 212 | } else if (file.link) { 213 | // it's a symlink, create a symlink 214 | const linkSrcPath = path.dirname(path.join(dest, file.link)) 215 | const linkDestPath = path.dirname(destFilename) 216 | const relativePath = path.relative(linkDestPath, linkSrcPath); 217 | // try to delete output file, because we can't overwrite a link 218 | (() => { 219 | try { 220 | fs.unlinkSync(destFilename) 221 | } catch (error) {} 222 | })() 223 | const linkTo = path.join(relativePath, path.basename(file.link)) 224 | fs.symlinkSync(linkTo, destFilename) 225 | } else { 226 | // it's a file, extract it 227 | const content = disk.readFileSync(filesystem, filename, file) 228 | fs.writeFileSync(destFilename, content) 229 | } 230 | }) 231 | } 232 | 233 | module.exports.uncache = function (archive) { 234 | return disk.uncacheFilesystem(archive) 235 | } 236 | 237 | module.exports.uncacheAll = function () { 238 | disk.uncacheAll() 239 | } 240 | -------------------------------------------------------------------------------- /test/cli-spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const assert = require('assert') 3 | const exec = require('child_process').exec 4 | const fs = process.versions.electron ? require('original-fs') : require('fs') 5 | const os = require('os') 6 | const path = require('path') 7 | const rimraf = require('rimraf') 8 | 9 | const compDirs = require('./util/compareDirectories') 10 | const compFiles = require('./util/compareFiles') 11 | 12 | describe('command line interface', function () { 13 | beforeEach(function () { 14 | rimraf.sync(path.join(__dirname, '..', 'tmp'), fs) 15 | }) 16 | it('should create archive from directory', function (done) { 17 | exec('node bin/asar p test/input/packthis/ tmp/packthis-cli.asar', function (error, stdout, stderr) { 18 | if (error != null) return done(error) 19 | done(compFiles('tmp/packthis-cli.asar', 'test/expected/packthis.asar')) 20 | }) 21 | }) 22 | if (os.platform() === 'win32') { 23 | it('should create archive from directory with windows-style path separators', function (done) { 24 | exec('node bin/asar p test\\input\\packthis\\ tmp\\packthis-cli.asar', function (error, stdout, stderr) { 25 | if (error != null) return done(error) 26 | done(compFiles('tmp/packthis-cli.asar', 'test/expected/packthis.asar')) 27 | }) 28 | }) 29 | } 30 | it('should create archive from directory without hidden files', function (done) { 31 | exec('node bin/asar p test/input/packthis/ tmp/packthis-without-hidden-cli.asar --exclude-hidden', function (error, stdout, stderr) { 32 | if (error != null) return done(error) 33 | done(compFiles('tmp/packthis-without-hidden-cli.asar', 'test/expected/packthis-without-hidden.asar')) 34 | }) 35 | }) 36 | it('should create archive from directory with unpacked files', function (done) { 37 | exec('node bin/asar p test/input/packthis/ tmp/packthis-unpack-cli.asar --unpack *.png --exclude-hidden', function (error, stdout, stderr) { 38 | if (error != null) return done(error) 39 | assert.ok(fs.existsSync('tmp/packthis-unpack-cli.asar.unpacked/dir2/file2.png')) 40 | done(compFiles('tmp/packthis-unpack-cli.asar', 'test/expected/packthis-unpack.asar')) 41 | }) 42 | }) 43 | it('should list files/dirs in archive', function (done) { 44 | exec('node bin/asar l test/input/extractthis.asar', function (error, stdout, stderr) { 45 | if (error != null) return done(error) 46 | const actual = stdout 47 | let expected = fs.readFileSync('test/expected/extractthis-filelist.txt', 'utf8') + '\n' 48 | // on windows replace slashes with backslashes and crlf with lf 49 | if (os.platform() === 'win32') { 50 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 51 | } 52 | done(assert.equal(actual, expected)) 53 | }) 54 | }) 55 | it('should list files/dirs in archive with unpacked files', function (done) { 56 | exec('node bin/asar l test/input/extractthis-unpack.asar', function (error, stdout, stderr) { 57 | if (error != null) return done(error) 58 | const actual = stdout 59 | let expected = fs.readFileSync('test/expected/extractthis-filelist.txt', 'utf8') + '\n' 60 | // on windows replace slashes with backslashes and crlf with lf 61 | if (os.platform() === 'win32') { 62 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 63 | } 64 | done(assert.equal(actual, expected)) 65 | }) 66 | }) 67 | it('should list files/dirs with multibyte characters in path', function (done) { 68 | exec('node bin/asar l test/expected/packthis-unicode-path.asar', function (error, stdout, stderr) { 69 | if (error != null) return done(error) 70 | const actual = stdout 71 | let expected = fs.readFileSync('test/expected/packthis-unicode-path-filelist.txt', 'utf8') + '\n' 72 | // on windows replace slashes with backslashes and crlf with lf 73 | if (os.platform() === 'win32') { 74 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 75 | } 76 | done(assert.equal(actual, expected)) 77 | }) 78 | }) 79 | // we need a way to set a path to extract to first, otherwise we pollute our project dir 80 | // or we fake it by setting our cwd, but I don't like that 81 | /* 82 | it('should extract a text file from archive', function(done) { 83 | exec('node bin/asar ef test/input/extractthis.asar dir1/file1.txt', function (error, stdout, stderr) { 84 | const actual = fs.readFileSync('tmp/file1.txt', 'utf8'); 85 | let expected = fs.readFileSync('test/expected/extractthis/dir1/file1.txt', 'utf8'); 86 | // on windows replace crlf with lf 87 | if (os.platform() === 'win32') { 88 | expected = expected.replace(/\r\n/g, '\n'); 89 | } 90 | done(assert.equal(actual, expected)); 91 | }); 92 | }); 93 | 94 | it('should extract a binary file from archive', function(done) { 95 | exec('node bin/asar ef test/input/extractthis.asar dir2/file2.png', function (error, stdout, stderr) { 96 | const actual = fs.readFileSync('tmp/file2.png', 'utf8'); 97 | const expected = fs.readFileSync('test/expected/extractthis/dir2/file2.png', 'utf8'); 98 | done(assert.equal(actual, expected)); 99 | }); 100 | }); 101 | */ 102 | it('should extract an archive', function (done) { 103 | exec('node bin/asar e test/input/extractthis.asar tmp/extractthis-cli/', function (error, stdout, stderr) { 104 | if (error != null) return done(error) 105 | compDirs('tmp/extractthis-cli/', 'test/expected/extractthis', done) 106 | }) 107 | }) 108 | it('should extract an archive with unpacked files', function (done) { 109 | exec('node bin/asar e test/input/extractthis-unpack.asar tmp/extractthis-unpack-cli/', function (error, stdout, stderr) { 110 | if (error != null) return done(error) 111 | compDirs('tmp/extractthis-unpack-cli/', 'test/expected/extractthis', done) 112 | }) 113 | }) 114 | it('should create archive from directory with unpacked dirs', function (done) { 115 | exec('node bin/asar p test/input/packthis/ tmp/packthis-unpack-dir-cli.asar --unpack-dir dir2 --exclude-hidden', function (error, stdout, stderr) { 116 | if (error != null) return done(error) 117 | assert.ok(fs.existsSync('tmp/packthis-unpack-dir-cli.asar.unpacked/dir2/file2.png')) 118 | assert.ok(fs.existsSync('tmp/packthis-unpack-dir-cli.asar.unpacked/dir2/file3.txt')) 119 | done(compFiles('tmp/packthis-unpack-dir-cli.asar', 'test/expected/packthis-unpack-dir.asar')) 120 | }) 121 | }) 122 | it('should create archive from directory with unpacked dirs specified by glob pattern', function (done) { 123 | const tmpFile = 'tmp/packthis-unpack-dir-glob-cli.asar' 124 | const tmpUnpacked = 'tmp/packthis-unpack-dir-glob-cli.asar.unpacked' 125 | exec('node bin/asar p test/input/packthis-glob/ ' + tmpFile + ' --unpack-dir "{x1,x2}" --exclude-hidden', function (error, stdout, stderr) { 126 | if (error != null) return done(error) 127 | assert.ok(fs.existsSync(tmpUnpacked + '/x1/file1.txt')) 128 | assert.ok(fs.existsSync(tmpUnpacked + '/x2/file2.txt')) 129 | done(compFiles(tmpFile, 'test/expected/packthis-unpack-dir-glob.asar')) 130 | }) 131 | }) 132 | it('should create archive from directory with unpacked dirs specified by globstar pattern', function (done) { 133 | const tmpFile = 'tmp/packthis-unpack-dir-globstar-cli.asar' 134 | const tmpUnpacked = 'tmp/packthis-unpack-dir-globstar-cli.asar.unpacked' 135 | exec('node bin/asar p test/input/packthis-glob/ ' + tmpFile + ' --unpack-dir "**/{x1,x2}" --exclude-hidden', function (error, stdout, stderr) { 136 | if (error != null) return done(error) 137 | assert.ok(fs.existsSync(tmpUnpacked + '/x1/file1.txt')) 138 | assert.ok(fs.existsSync(tmpUnpacked + '/x2/file2.txt')) 139 | assert.ok(fs.existsSync(tmpUnpacked + '/y3/x1/file4.txt')) 140 | assert.ok(fs.existsSync(tmpUnpacked + '/y3/z1/x2/file5.txt')) 141 | done(compFiles(tmpFile, 'test/expected/packthis-unpack-dir-globstar.asar')) 142 | }) 143 | }) 144 | it('should create archive from directory with unpacked dirs specified by foo/{bar,baz} style pattern', function (done) { 145 | const tmpFile = 'tmp/packthis-unpack-dir-globstar-cli.asar' 146 | const tmpUnpacked = 'tmp/packthis-unpack-dir-globstar-cli.asar.unpacked' 147 | exec('node bin/asar p test/input/packthis-glob/ ' + tmpFile + ' --unpack-dir "y3/{x1,z1}" --exclude-hidden', function (error, stdout, stderr) { 148 | if (error != null) return done(error) 149 | assert.ok(fs.existsSync(tmpUnpacked + '/y3/x1/file4.txt')) 150 | assert.ok(fs.existsSync(tmpUnpacked + '/y3/z1/x2/file5.txt')) 151 | done() 152 | }) 153 | }) 154 | it('should list files/dirs in archive with unpacked dirs', function (done) { 155 | exec('node bin/asar l test/expected/packthis-unpack-dir.asar', function (error, stdout, stderr) { 156 | if (error != null) return done(error) 157 | const actual = stdout 158 | let expected = fs.readFileSync('test/expected/extractthis-filelist.txt', 'utf8') + '\n' 159 | // on windows replace slashes with backslashes and crlf with lf 160 | if (os.platform() === 'win32') { 161 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 162 | } 163 | done(assert.equal(actual, expected)) 164 | }) 165 | }) 166 | it('should list files/dirs in archive with unpacked dirs & is-pack option', function (done) { 167 | exec('node bin/asar l test/expected/packthis-unpack-dir.asar --is-pack', function (error, stdout, stderr) { 168 | if (error != null) return done(error) 169 | const actual = stdout 170 | let expected = fs.readFileSync('test/expected/extractthis-filelist-with-option.txt', 'utf8') + '\n' 171 | // on windows replace slashes with backslashes and crlf with lf 172 | if (os.platform() === 'win32') { 173 | expected = expected.replace(/\//g, '\\').replace(/\r\n/g, '\n') 174 | } 175 | done(assert.equal(actual, expected)) 176 | }) 177 | }) 178 | it('should extract an archive with unpacked dirs', function (done) { 179 | exec('node bin/asar e test/input/extractthis-unpack-dir.asar tmp/extractthis-unpack-dir/', function (error, stdout, stderr) { 180 | if (error != null) return done(error) 181 | compDirs('tmp/extractthis-unpack-dir/', 'test/expected/extractthis', done) 182 | }) 183 | }) 184 | it('should create archive from directory with unpacked dirs and files', function (done) { 185 | exec('node bin/asar p test/input/packthis/ tmp/packthis-unpack-dir-file-cli.asar --unpack *.png --unpack-dir dir2 --exclude-hidden', function (error, stdout, stderr) { 186 | if (error != null) return done(error) 187 | assert.ok(fs.existsSync('tmp/packthis-unpack-dir-file-cli.asar.unpacked/dir2/file2.png')) 188 | assert.ok(fs.existsSync('tmp/packthis-unpack-dir-file-cli.asar.unpacked/dir2/file3.txt')) 189 | done(compFiles('tmp/packthis-unpack-dir-file-cli.asar', 'test/expected/packthis-unpack-dir.asar')) 190 | }) 191 | }) 192 | it('should create archive from directory with unpacked subdirs and files', function (done) { 193 | exec('node bin/asar p test/input/packthis-subdir/ tmp/packthis-unpack-subdir-cli.asar --unpack *.txt --unpack-dir dir2/subdir --exclude-hidden', function (error, stdout, stderr) { 194 | if (error != null) return done(error) 195 | assert.ok(fs.existsSync('tmp/packthis-unpack-subdir-cli.asar.unpacked/file0.txt')) 196 | assert.ok(fs.existsSync('tmp/packthis-unpack-subdir-cli.asar.unpacked/dir1/file1.txt')) 197 | assert.ok(fs.existsSync('tmp/packthis-unpack-subdir-cli.asar.unpacked/dir2/subdir/file2.png')) 198 | assert.ok(fs.existsSync('tmp/packthis-unpack-subdir-cli.asar.unpacked/dir2/subdir/file3.txt')) 199 | done() 200 | }) 201 | }) 202 | }) 203 | --------------------------------------------------------------------------------