Add capacitorjs runtime

This commit is contained in:
olcxja 2026-05-03 17:09:55 +02:00
commit f90c0e6c40
8362 changed files with 1502407 additions and 1 deletions

View file

@ -0,0 +1,15 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View file

@ -0,0 +1,3 @@
Like `chown -R`.
Takes the same arguments as `fs.chown()`

View file

@ -0,0 +1,167 @@
'use strict'
const fs = require('fs')
const path = require('path')
/* istanbul ignore next */
const LCHOWN = fs.lchown ? 'lchown' : 'chown'
/* istanbul ignore next */
const LCHOWNSYNC = fs.lchownSync ? 'lchownSync' : 'chownSync'
/* istanbul ignore next */
const needEISDIRHandled = fs.lchown &&
!process.version.match(/v1[1-9]+\./) &&
!process.version.match(/v10\.[6-9]/)
const lchownSync = (path, uid, gid) => {
try {
return fs[LCHOWNSYNC](path, uid, gid)
} catch (er) {
if (er.code !== 'ENOENT')
throw er
}
}
/* istanbul ignore next */
const chownSync = (path, uid, gid) => {
try {
return fs.chownSync(path, uid, gid)
} catch (er) {
if (er.code !== 'ENOENT')
throw er
}
}
/* istanbul ignore next */
const handleEISDIR =
needEISDIRHandled ? (path, uid, gid, cb) => er => {
// Node prior to v10 had a very questionable implementation of
// fs.lchown, which would always try to call fs.open on a directory
// Fall back to fs.chown in those cases.
if (!er || er.code !== 'EISDIR')
cb(er)
else
fs.chown(path, uid, gid, cb)
}
: (_, __, ___, cb) => cb
/* istanbul ignore next */
const handleEISDirSync =
needEISDIRHandled ? (path, uid, gid) => {
try {
return lchownSync(path, uid, gid)
} catch (er) {
if (er.code !== 'EISDIR')
throw er
chownSync(path, uid, gid)
}
}
: (path, uid, gid) => lchownSync(path, uid, gid)
// fs.readdir could only accept an options object as of node v6
const nodeVersion = process.version
let readdir = (path, options, cb) => fs.readdir(path, options, cb)
let readdirSync = (path, options) => fs.readdirSync(path, options)
/* istanbul ignore next */
if (/^v4\./.test(nodeVersion))
readdir = (path, options, cb) => fs.readdir(path, cb)
const chown = (cpath, uid, gid, cb) => {
fs[LCHOWN](cpath, uid, gid, handleEISDIR(cpath, uid, gid, er => {
// Skip ENOENT error
cb(er && er.code !== 'ENOENT' ? er : null)
}))
}
const chownrKid = (p, child, uid, gid, cb) => {
if (typeof child === 'string')
return fs.lstat(path.resolve(p, child), (er, stats) => {
// Skip ENOENT error
if (er)
return cb(er.code !== 'ENOENT' ? er : null)
stats.name = child
chownrKid(p, stats, uid, gid, cb)
})
if (child.isDirectory()) {
chownr(path.resolve(p, child.name), uid, gid, er => {
if (er)
return cb(er)
const cpath = path.resolve(p, child.name)
chown(cpath, uid, gid, cb)
})
} else {
const cpath = path.resolve(p, child.name)
chown(cpath, uid, gid, cb)
}
}
const chownr = (p, uid, gid, cb) => {
readdir(p, { withFileTypes: true }, (er, children) => {
// any error other than ENOTDIR or ENOTSUP means it's not readable,
// or doesn't exist. give up.
if (er) {
if (er.code === 'ENOENT')
return cb()
else if (er.code !== 'ENOTDIR' && er.code !== 'ENOTSUP')
return cb(er)
}
if (er || !children.length)
return chown(p, uid, gid, cb)
let len = children.length
let errState = null
const then = er => {
if (errState)
return
if (er)
return cb(errState = er)
if (-- len === 0)
return chown(p, uid, gid, cb)
}
children.forEach(child => chownrKid(p, child, uid, gid, then))
})
}
const chownrKidSync = (p, child, uid, gid) => {
if (typeof child === 'string') {
try {
const stats = fs.lstatSync(path.resolve(p, child))
stats.name = child
child = stats
} catch (er) {
if (er.code === 'ENOENT')
return
else
throw er
}
}
if (child.isDirectory())
chownrSync(path.resolve(p, child.name), uid, gid)
handleEISDirSync(path.resolve(p, child.name), uid, gid)
}
const chownrSync = (p, uid, gid) => {
let children
try {
children = readdirSync(p, { withFileTypes: true })
} catch (er) {
if (er.code === 'ENOENT')
return
else if (er.code === 'ENOTDIR' || er.code === 'ENOTSUP')
return handleEISDirSync(p, uid, gid)
else
throw er
}
if (children && children.length)
children.forEach(child => chownrKidSync(p, child, uid, gid))
return handleEISDirSync(p, uid, gid)
}
module.exports = chownr
chownr.sync = chownrSync

View file

@ -0,0 +1,29 @@
{
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"name": "chownr",
"description": "like `chown -R`",
"version": "1.1.4",
"repository": {
"type": "git",
"url": "git://github.com/isaacs/chownr.git"
},
"main": "chownr.js",
"files": [
"chownr.js"
],
"devDependencies": {
"mkdirp": "0.3",
"rimraf": "^2.7.1",
"tap": "^14.10.6"
},
"tap": {
"check-coverage": true
},
"scripts": {
"test": "tap",
"preversion": "npm test",
"postversion": "npm publish",
"prepublishOnly": "git push origin --follow-tags"
},
"license": "ISC"
}

View file

@ -0,0 +1,6 @@
language: node_js
node_js:
- 8
- 10
- 12
- 14

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Mathias Buus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,165 @@
# tar-fs
filesystem bindings for [tar-stream](https://github.com/mafintosh/tar-stream).
```
npm install tar-fs
```
[![build status](https://secure.travis-ci.org/mafintosh/tar-fs.png)](http://travis-ci.org/mafintosh/tar-fs)
## Usage
tar-fs allows you to pack directories into tarballs and extract tarballs into directories.
It doesn't gunzip for you, so if you want to extract a `.tar.gz` with this you'll need to use something like [gunzip-maybe](https://github.com/mafintosh/gunzip-maybe) in addition to this.
``` js
var tar = require('tar-fs')
var fs = require('fs')
// packing a directory
tar.pack('./my-directory').pipe(fs.createWriteStream('my-tarball.tar'))
// extracting a directory
fs.createReadStream('my-other-tarball.tar').pipe(tar.extract('./my-other-directory'))
```
To ignore various files when packing or extracting add a ignore function to the options. `ignore`
is also an alias for `filter`. Additionally you get `header` if you use ignore while extracting.
That way you could also filter by metadata.
``` js
var pack = tar.pack('./my-directory', {
ignore: function(name) {
return path.extname(name) === '.bin' // ignore .bin files when packing
}
})
var extract = tar.extract('./my-other-directory', {
ignore: function(name) {
return path.extname(name) === '.bin' // ignore .bin files inside the tarball when extracing
}
})
var extractFilesDirs = tar.extract('./my-other-other-directory', {
ignore: function(_, header) {
// pass files & directories, ignore e.g. symlinks
return header.type !== 'file' && header.type !== 'directory'
}
})
```
You can also specify which entries to pack using the `entries` option
```js
var pack = tar.pack('./my-directory', {
entries: ['file1', 'subdir/file2'] // only the specific entries will be packed
})
```
If you want to modify the headers when packing/extracting add a map function to the options
``` js
var pack = tar.pack('./my-directory', {
map: function(header) {
header.name = 'prefixed/'+header.name
return header
}
})
var extract = tar.extract('./my-directory', {
map: function(header) {
header.name = 'another-prefix/'+header.name
return header
}
})
```
Similarly you can use `mapStream` incase you wanna modify the input/output file streams
``` js
var pack = tar.pack('./my-directory', {
mapStream: function(fileStream, header) {
// NOTE: the returned stream HAS to have the same length as the input stream.
// If not make sure to update the size in the header passed in here.
if (path.extname(header.name) === '.js') {
return fileStream.pipe(someTransform)
}
return fileStream;
}
})
var extract = tar.extract('./my-directory', {
mapStream: function(fileStream, header) {
if (path.extname(header.name) === '.js') {
return fileStream.pipe(someTransform)
}
return fileStream;
}
})
```
Set `options.fmode` and `options.dmode` to ensure that files/directories extracted have the corresponding modes
``` js
var extract = tar.extract('./my-directory', {
dmode: parseInt(555, 8), // all dirs should be readable
fmode: parseInt(444, 8) // all files should be readable
})
```
It can be useful to use `dmode` and `fmode` if you are packing/unpacking tarballs between *nix/windows to ensure that all files/directories unpacked are readable.
Alternatively you can set `options.readable` and/or `options.writable` to set the dmode and fmode to readable/writable.
``` js
var extract = tar.extract('./my-directory', {
readable: true, // all dirs and files should be readable
writable: true, // all dirs and files should be writable
})
```
Set `options.strict` to `false` if you want to ignore errors due to unsupported entry types (like device files)
To dereference symlinks (pack the contents of the symlink instead of the link itself) set `options.dereference` to `true`.
## Copy a directory
Copying a directory with permissions and mtime intact is as simple as
``` js
tar.pack('source-directory').pipe(tar.extract('dest-directory'))
```
## Interaction with [`tar-stream`](https://github.com/mafintosh/tar-stream)
Use `finalize: false` and the `finish` hook to
leave the pack stream open for further entries (see
[`tar-stream#pack`](https://github.com/mafintosh/tar-stream#packing)),
and use `pack` to pass an existing pack stream.
``` js
var mypack = tar.pack('./my-directory', {
finalize: false,
finish: function(sameAsMypack) {
mypack.entry({name: 'generated-file.txt'}, "hello")
tar.pack('./other-directory', {
pack: sameAsMypack
})
}
})
```
## Performance
Packing and extracting a 6.1 GB with 2496 directories and 2398 files yields the following results on my Macbook Air.
[See the benchmark here](https://gist.github.com/mafintosh/8102201)
* tar-fs: 34.261 seconds
* [node-tar](https://github.com/isaacs/node-tar): 366.123 seconds (or 10x slower)
## License
MIT

View file

@ -0,0 +1,363 @@
var chownr = require('chownr')
var tar = require('tar-stream')
var pump = require('pump')
var mkdirp = require('mkdirp-classic')
var fs = require('fs')
var path = require('path')
var os = require('os')
var win32 = os.platform() === 'win32'
var noop = function () {}
var echo = function (name) {
return name
}
var normalize = !win32 ? echo : function (name) {
return name.replace(/\\/g, '/').replace(/[:?<>|]/g, '_')
}
var statAll = function (fs, stat, cwd, ignore, entries, sort) {
var queue = entries || ['.']
return function loop (callback) {
if (!queue.length) return callback()
var next = queue.shift()
var nextAbs = path.join(cwd, next)
stat.call(fs, nextAbs, function (err, stat) {
if (err) return callback(err)
if (!stat.isDirectory()) return callback(null, next, stat)
fs.readdir(nextAbs, function (err, files) {
if (err) return callback(err)
if (sort) files.sort()
for (var i = 0; i < files.length; i++) {
if (!ignore(path.join(cwd, next, files[i]))) queue.push(path.join(next, files[i]))
}
callback(null, next, stat)
})
})
}
}
var strip = function (map, level) {
return function (header) {
header.name = header.name.split('/').slice(level).join('/')
var linkname = header.linkname
if (linkname && (header.type === 'link' || path.isAbsolute(linkname))) {
header.linkname = linkname.split('/').slice(level).join('/')
}
return map(header)
}
}
exports.pack = function (cwd, opts) {
if (!cwd) cwd = '.'
if (!opts) opts = {}
var xfs = opts.fs || fs
var ignore = opts.ignore || opts.filter || noop
var map = opts.map || noop
var mapStream = opts.mapStream || echo
var statNext = statAll(xfs, opts.dereference ? xfs.stat : xfs.lstat, cwd, ignore, opts.entries, opts.sort)
var strict = opts.strict !== false
var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask()
var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0
var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0
var pack = opts.pack || tar.pack()
var finish = opts.finish || noop
if (opts.strip) map = strip(map, opts.strip)
if (opts.readable) {
dmode |= parseInt(555, 8)
fmode |= parseInt(444, 8)
}
if (opts.writable) {
dmode |= parseInt(333, 8)
fmode |= parseInt(222, 8)
}
var onsymlink = function (filename, header) {
xfs.readlink(path.join(cwd, filename), function (err, linkname) {
if (err) return pack.destroy(err)
header.linkname = normalize(linkname)
pack.entry(header, onnextentry)
})
}
var onstat = function (err, filename, stat) {
if (err) return pack.destroy(err)
if (!filename) {
if (opts.finalize !== false) pack.finalize()
return finish(pack)
}
if (stat.isSocket()) return onnextentry() // tar does not support sockets...
var header = {
name: normalize(filename),
mode: (stat.mode | (stat.isDirectory() ? dmode : fmode)) & umask,
mtime: stat.mtime,
size: stat.size,
type: 'file',
uid: stat.uid,
gid: stat.gid
}
if (stat.isDirectory()) {
header.size = 0
header.type = 'directory'
header = map(header) || header
return pack.entry(header, onnextentry)
}
if (stat.isSymbolicLink()) {
header.size = 0
header.type = 'symlink'
header = map(header) || header
return onsymlink(filename, header)
}
// TODO: add fifo etc...
header = map(header) || header
if (!stat.isFile()) {
if (strict) return pack.destroy(new Error('unsupported type for ' + filename))
return onnextentry()
}
var entry = pack.entry(header, onnextentry)
if (!entry) return
var rs = mapStream(xfs.createReadStream(path.join(cwd, filename), { start: 0, end: header.size > 0 ? header.size - 1 : header.size }), header)
rs.on('error', function (err) { // always forward errors on destroy
entry.destroy(err)
})
pump(rs, entry)
}
var onnextentry = function (err) {
if (err) return pack.destroy(err)
statNext(onstat)
}
onnextentry()
return pack
}
var head = function (list) {
return list.length ? list[list.length - 1] : null
}
var processGetuid = function () {
return process.getuid ? process.getuid() : -1
}
var processUmask = function () {
return process.umask ? process.umask() : 0
}
exports.extract = function (cwd, opts) {
if (!cwd) cwd = '.'
if (!opts) opts = {}
var xfs = opts.fs || fs
var ignore = opts.ignore || opts.filter || noop
var map = opts.map || noop
var mapStream = opts.mapStream || echo
var own = opts.chown !== false && !win32 && processGetuid() === 0
var extract = opts.extract || tar.extract()
var stack = []
var now = new Date()
var umask = typeof opts.umask === 'number' ? ~opts.umask : ~processUmask()
var dmode = typeof opts.dmode === 'number' ? opts.dmode : 0
var fmode = typeof opts.fmode === 'number' ? opts.fmode : 0
var strict = opts.strict !== false
if (opts.strip) map = strip(map, opts.strip)
if (opts.readable) {
dmode |= parseInt(555, 8)
fmode |= parseInt(444, 8)
}
if (opts.writable) {
dmode |= parseInt(333, 8)
fmode |= parseInt(222, 8)
}
var utimesParent = function (name, cb) { // we just set the mtime on the parent dir again everytime we write an entry
var top
while ((top = head(stack)) && name.slice(0, top[0].length) !== top[0]) stack.pop()
if (!top) return cb()
xfs.utimes(top[0], now, top[1], cb)
}
var utimes = function (name, header, cb) {
if (opts.utimes === false) return cb()
if (header.type === 'directory') return xfs.utimes(name, now, header.mtime, cb)
if (header.type === 'symlink') return utimesParent(name, cb) // TODO: how to set mtime on link?
xfs.utimes(name, now, header.mtime, function (err) {
if (err) return cb(err)
utimesParent(name, cb)
})
}
var chperm = function (name, header, cb) {
var link = header.type === 'symlink'
/* eslint-disable node/no-deprecated-api */
var chmod = link ? xfs.lchmod : xfs.chmod
var chown = link ? xfs.lchown : xfs.chown
/* eslint-enable node/no-deprecated-api */
if (!chmod) return cb()
var mode = (header.mode | (header.type === 'directory' ? dmode : fmode)) & umask
if (chown && own) chown.call(xfs, name, header.uid, header.gid, onchown)
else onchown(null)
function onchown (err) {
if (err) return cb(err)
if (!chmod) return cb()
chmod.call(xfs, name, mode, cb)
}
}
extract.on('entry', function (header, stream, next) {
header = map(header) || header
header.name = normalize(header.name)
var name = path.join(cwd, path.join('/', header.name))
if (ignore(name, header)) {
stream.resume()
return next()
}
var stat = function (err) {
if (err) return next(err)
utimes(name, header, function (err) {
if (err) return next(err)
if (win32) return next()
chperm(name, header, next)
})
}
var onsymlink = function () {
if (win32) return next() // skip symlinks on win for now before it can be tested
xfs.unlink(name, function () {
var dst = path.resolve(path.dirname(name), header.linkname)
if (!inCwd(dst, cwd)) return next(new Error(name + ' is not a valid symlink'))
xfs.symlink(header.linkname, name, stat)
})
}
var onlink = function () {
if (win32) return next() // skip links on win for now before it can be tested
xfs.unlink(name, function () {
var srcpath = path.join(cwd, path.join('/', header.linkname))
xfs.realpath(srcpath, function (err, dst) {
if (err || !inCwd(dst, cwd)) return next(new Error(name + ' is not a valid hardlink'))
xfs.link(dst, name, function (err) {
if (err && err.code === 'EPERM' && opts.hardlinkAsFilesFallback) {
stream = xfs.createReadStream(srcpath)
return onfile()
}
stat(err)
})
})
})
}
var onfile = function () {
var ws = xfs.createWriteStream(name)
var rs = mapStream(stream, header)
ws.on('error', function (err) { // always forward errors on destroy
rs.destroy(err)
})
pump(rs, ws, function (err) {
if (err) return next(err)
ws.on('close', stat)
})
}
if (header.type === 'directory') {
stack.push([name, header.mtime])
return mkdirfix(name, {
fs: xfs, own: own, uid: header.uid, gid: header.gid
}, stat)
}
var dir = path.dirname(name)
validate(xfs, dir, path.join(cwd, '.'), function (err, valid) {
if (err) return next(err)
if (!valid) return next(new Error(dir + ' is not a valid path'))
mkdirfix(dir, {
fs: xfs, own: own, uid: header.uid, gid: header.gid
}, function (err) {
if (err) return next(err)
switch (header.type) {
case 'file': return onfile()
case 'link': return onlink()
case 'symlink': return onsymlink()
}
if (strict) return next(new Error('unsupported type for ' + name + ' (' + header.type + ')'))
stream.resume()
next()
})
})
})
if (opts.finish) extract.on('finish', opts.finish)
return extract
}
function validate (fs, name, root, cb) {
if (name === root) return cb(null, true)
fs.lstat(name, function (err, st) {
if (err && err.code !== 'ENOENT') return cb(err)
if (err || st.isDirectory()) return validate(fs, path.join(name, '..'), root, cb)
cb(null, false)
})
}
function mkdirfix (name, opts, cb) {
mkdirp(name, { fs: opts.fs }, function (err, made) {
if (!err && made && opts.own) {
chownr(made, opts.uid, opts.gid, cb)
} else {
cb(err)
}
})
}
function inCwd (dst, cwd) {
cwd = path.resolve(cwd)
return cwd === dst || dst.startsWith(cwd + path.sep)
}

View file

@ -0,0 +1,41 @@
{
"name": "tar-fs",
"version": "2.1.4",
"description": "filesystem bindings for tar-stream",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
},
"keywords": [
"tar",
"fs",
"file",
"tarball",
"directory",
"stream"
],
"devDependencies": {
"rimraf": "^2.6.3",
"standard": "^13.0.1",
"tape": "^4.9.2"
},
"scripts": {
"test": "standard && tape test/index.js"
},
"bugs": {
"url": "https://github.com/mafintosh/tar-fs/issues"
},
"homepage": "https://github.com/mafintosh/tar-fs",
"main": "index.js",
"directories": {
"test": "test"
},
"author": "Mathias Buus",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/mafintosh/tar-fs.git"
}
}

View file

@ -0,0 +1 @@
hello world

View file

@ -0,0 +1 @@
test

Binary file not shown.

View file

@ -0,0 +1,346 @@
var test = require('tape')
var rimraf = require('rimraf')
var tar = require('../index')
var tarStream = require('tar-stream')
var path = require('path')
var fs = require('fs')
var os = require('os')
var win32 = os.platform() === 'win32'
var mtime = function (st) {
return Math.floor(st.mtime.getTime() / 1000)
}
test('copy a -> copy/a', function (t) {
t.plan(5)
var a = path.join(__dirname, 'fixtures', 'a')
var b = path.join(__dirname, 'fixtures', 'copy', 'a')
rimraf.sync(b)
tar.pack(a)
.pipe(tar.extract(b))
.on('finish', function () {
var files = fs.readdirSync(b)
t.same(files.length, 1)
t.same(files[0], 'hello.txt')
var fileB = path.join(b, files[0])
var fileA = path.join(a, files[0])
t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8'))
t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode)
t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA)))
})
})
test('copy b -> copy/b', function (t) {
t.plan(8)
var a = path.join(__dirname, 'fixtures', 'b')
var b = path.join(__dirname, 'fixtures', 'copy', 'b')
rimraf.sync(b)
tar.pack(a)
.pipe(tar.extract(b))
.on('finish', function () {
var files = fs.readdirSync(b)
t.same(files.length, 1)
t.same(files[0], 'a')
var dirB = path.join(b, files[0])
var dirA = path.join(a, files[0])
t.same(fs.statSync(dirB).mode, fs.statSync(dirA).mode)
t.same(mtime(fs.statSync(dirB)), mtime(fs.statSync(dirA)))
t.ok(fs.statSync(dirB).isDirectory())
var fileB = path.join(dirB, 'test.txt')
var fileA = path.join(dirA, 'test.txt')
t.same(fs.readFileSync(fileB, 'utf-8'), fs.readFileSync(fileA, 'utf-8'))
t.same(fs.statSync(fileB).mode, fs.statSync(fileA).mode)
t.same(mtime(fs.statSync(fileB)), mtime(fs.statSync(fileA)))
})
})
test('symlink', function (t) {
if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow
t.plan(1)
t.ok(true)
return
}
t.plan(5)
var a = path.join(__dirname, 'fixtures', 'c')
rimraf.sync(path.join(a, 'link'))
fs.symlinkSync('.gitignore', path.join(a, 'link'))
var b = path.join(__dirname, 'fixtures', 'copy', 'c')
rimraf.sync(b)
tar.pack(a)
.pipe(tar.extract(b))
.on('finish', function () {
var files = fs.readdirSync(b).sort()
t.same(files.length, 2)
t.same(files[0], '.gitignore')
t.same(files[1], 'link')
var linkA = path.join(a, 'link')
var linkB = path.join(b, 'link')
t.same(mtime(fs.lstatSync(linkB)), mtime(fs.lstatSync(linkA)))
t.same(fs.readlinkSync(linkB), fs.readlinkSync(linkA))
})
})
test('follow symlinks', function (t) {
if (win32) { // no symlink support on win32 currently. TODO: test if this can be enabled somehow
t.plan(1)
t.ok(true)
return
}
t.plan(5)
var a = path.join(__dirname, 'fixtures', 'c')
rimraf.sync(path.join(a, 'link'))
fs.symlinkSync('.gitignore', path.join(a, 'link'))
var b = path.join(__dirname, 'fixtures', 'copy', 'c-dereference')
rimraf.sync(b)
tar.pack(a, { dereference: true })
.pipe(tar.extract(b))
.on('finish', function () {
var files = fs.readdirSync(b).sort()
t.same(files.length, 2)
t.same(files[0], '.gitignore')
t.same(files[1], 'link')
var file1 = path.join(b, '.gitignore')
var file2 = path.join(b, 'link')
t.same(mtime(fs.lstatSync(file1)), mtime(fs.lstatSync(file2)))
t.same(fs.readFileSync(file1), fs.readFileSync(file2))
})
})
test('strip', function (t) {
t.plan(2)
var a = path.join(__dirname, 'fixtures', 'b')
var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip')
rimraf.sync(b)
tar.pack(a)
.pipe(tar.extract(b, { strip: 1 }))
.on('finish', function () {
var files = fs.readdirSync(b).sort()
t.same(files.length, 1)
t.same(files[0], 'test.txt')
})
})
test('strip + map', function (t) {
t.plan(2)
var a = path.join(__dirname, 'fixtures', 'b')
var b = path.join(__dirname, 'fixtures', 'copy', 'b-strip')
rimraf.sync(b)
var uppercase = function (header) {
header.name = header.name.toUpperCase()
return header
}
tar.pack(a)
.pipe(tar.extract(b, { strip: 1, map: uppercase }))
.on('finish', function () {
var files = fs.readdirSync(b).sort()
t.same(files.length, 1)
t.same(files[0], 'TEST.TXT')
})
})
test('map + dir + permissions', function (t) {
t.plan(win32 ? 1 : 2) // skip chmod test, it's not working like unix
var a = path.join(__dirname, 'fixtures', 'b')
var b = path.join(__dirname, 'fixtures', 'copy', 'a-perms')
rimraf.sync(b)
var aWithMode = function (header) {
if (header.name === 'a') {
header.mode = parseInt(700, 8)
}
return header
}
tar.pack(a)
.pipe(tar.extract(b, { map: aWithMode }))
.on('finish', function () {
var files = fs.readdirSync(b).sort()
var stat = fs.statSync(path.join(b, 'a'))
t.same(files.length, 1)
if (!win32) {
t.same(stat.mode & parseInt(777, 8), parseInt(700, 8))
}
})
})
test('specific entries', function (t) {
t.plan(6)
var a = path.join(__dirname, 'fixtures', 'd')
var b = path.join(__dirname, 'fixtures', 'copy', 'd-entries')
var entries = ['file1', 'sub-files/file3', 'sub-dir']
rimraf.sync(b)
tar.pack(a, { entries: entries })
.pipe(tar.extract(b))
.on('finish', function () {
var files = fs.readdirSync(b)
t.same(files.length, 3)
t.notSame(files.indexOf('file1'), -1)
t.notSame(files.indexOf('sub-files'), -1)
t.notSame(files.indexOf('sub-dir'), -1)
var subFiles = fs.readdirSync(path.join(b, 'sub-files'))
t.same(subFiles, ['file3'])
var subDir = fs.readdirSync(path.join(b, 'sub-dir'))
t.same(subDir, ['file5'])
})
})
test('check type while mapping header on packing', function (t) {
t.plan(3)
var e = path.join(__dirname, 'fixtures', 'e')
var checkHeaderType = function (header) {
if (header.name.indexOf('.') === -1) t.same(header.type, header.name)
}
tar.pack(e, { map: checkHeaderType })
})
test('finish callbacks', function (t) {
t.plan(3)
var a = path.join(__dirname, 'fixtures', 'a')
var b = path.join(__dirname, 'fixtures', 'copy', 'a')
rimraf.sync(b)
var packEntries = 0
var extractEntries = 0
var countPackEntry = function (header) { packEntries++ }
var countExtractEntry = function (header) { extractEntries++ }
var pack
var onPackFinish = function (passedPack) {
t.equal(packEntries, 2, 'All entries have been packed') // 2 entries - the file and base directory
t.equal(passedPack, pack, 'The finish hook passes the pack')
}
var onExtractFinish = function () { t.equal(extractEntries, 2) }
pack = tar.pack(a, { map: countPackEntry, finish: onPackFinish })
pack.pipe(tar.extract(b, { map: countExtractEntry, finish: onExtractFinish }))
.on('finish', function () {
t.end()
})
})
test('not finalizing the pack', function (t) {
t.plan(2)
var a = path.join(__dirname, 'fixtures', 'a')
var b = path.join(__dirname, 'fixtures', 'b')
var out = path.join(__dirname, 'fixtures', 'copy', 'merged-packs')
rimraf.sync(out)
var prefixer = function (prefix) {
return function (header) {
header.name = path.join(prefix, header.name)
return header
}
}
tar.pack(a, {
map: prefixer('a-files'),
finalize: false,
finish: packB
})
function packB (pack) {
tar.pack(b, { pack: pack, map: prefixer('b-files') })
.pipe(tar.extract(out))
.on('finish', assertResults)
}
function assertResults () {
var containers = fs.readdirSync(out)
t.deepEqual(containers, ['a-files', 'b-files'])
var aFiles = fs.readdirSync(path.join(out, 'a-files'))
t.deepEqual(aFiles, ['hello.txt'])
}
})
test('do not extract invalid tar', function (t) {
var a = path.join(__dirname, 'fixtures', 'invalid.tar')
var out = path.join(__dirname, 'fixtures', 'invalid')
rimraf.sync(out)
fs.createReadStream(a)
.pipe(tar.extract(out))
.on('error', function (err) {
t.ok(/is not a valid symlink/i.test(err.message))
fs.stat(path.join(out, '../bar'), function (err) {
t.ok(err)
t.end()
})
})
})
test('no abs hardlink targets', function (t) {
var out = path.join(__dirname, 'fixtures', 'invalid')
var outside = path.join(__dirname, 'fixtures', 'outside')
rimraf.sync(out)
var s = tarStream.pack()
fs.writeFileSync(outside, 'something')
s.entry({
type: 'link',
name: 'link',
linkname: outside
})
s.entry({
name: 'link'
}, 'overwrite')
s.finalize()
s.pipe(tar.extract(out))
.on('error', function (err) {
t.ok(err, 'had error')
fs.readFile(outside, 'utf-8', function (err, str) {
t.error(err, 'no error')
t.same(str, 'something')
t.end()
})
})
})

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Mathias Buus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,168 @@
# tar-stream
tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system.
Note that you still need to gunzip your data if you have a `.tar.gz`. We recommend using [gunzip-maybe](https://github.com/mafintosh/gunzip-maybe) in conjunction with this.
```
npm install tar-stream
```
[![build status](https://secure.travis-ci.org/mafintosh/tar-stream.png)](http://travis-ci.org/mafintosh/tar-stream)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT)
## Usage
tar-stream exposes two streams, [pack](https://github.com/mafintosh/tar-stream#packing) which creates tarballs and [extract](https://github.com/mafintosh/tar-stream#extracting) which extracts tarballs. To [modify an existing tarball](https://github.com/mafintosh/tar-stream#modifying-existing-tarballs) use both.
It implementes USTAR with additional support for pax extended headers. It should be compatible with all popular tar distributions out there (gnutar, bsdtar etc)
## Related
If you want to pack/unpack directories on the file system check out [tar-fs](https://github.com/mafintosh/tar-fs) which provides file system bindings to this module.
## Packing
To create a pack stream use `tar.pack()` and call `pack.entry(header, [callback])` to add tar entries.
``` js
var tar = require('tar-stream')
var pack = tar.pack() // pack is a streams2 stream
// add a file called my-test.txt with the content "Hello World!"
pack.entry({ name: 'my-test.txt' }, 'Hello World!')
// add a file called my-stream-test.txt from a stream
var entry = pack.entry({ name: 'my-stream-test.txt', size: 11 }, function(err) {
// the stream was added
// no more entries
pack.finalize()
})
entry.write('hello')
entry.write(' ')
entry.write('world')
entry.end()
// pipe the pack stream somewhere
pack.pipe(process.stdout)
```
## Extracting
To extract a stream use `tar.extract()` and listen for `extract.on('entry', (header, stream, next) )`
``` js
var extract = tar.extract()
extract.on('entry', function(header, stream, next) {
// header is the tar header
// stream is the content body (might be an empty stream)
// call next when you are done with this entry
stream.on('end', function() {
next() // ready for next entry
})
stream.resume() // just auto drain the stream
})
extract.on('finish', function() {
// all entries read
})
pack.pipe(extract)
```
The tar archive is streamed sequentially, meaning you **must** drain each entry's stream as you get them or else the main extract stream will receive backpressure and stop reading.
## Headers
The header object using in `entry` should contain the following properties.
Most of these values can be found by stat'ing a file.
``` js
{
name: 'path/to/this/entry.txt',
size: 1314, // entry size. defaults to 0
mode: 0o644, // entry mode. defaults to to 0o755 for dirs and 0o644 otherwise
mtime: new Date(), // last modified date for entry. defaults to now.
type: 'file', // type of entry. defaults to file. can be:
// file | link | symlink | directory | block-device
// character-device | fifo | contiguous-file
linkname: 'path', // linked file name
uid: 0, // uid of entry owner. defaults to 0
gid: 0, // gid of entry owner. defaults to 0
uname: 'maf', // uname of entry owner. defaults to null
gname: 'staff', // gname of entry owner. defaults to null
devmajor: 0, // device major version. defaults to 0
devminor: 0 // device minor version. defaults to 0
}
```
## Modifying existing tarballs
Using tar-stream it is easy to rewrite paths / change modes etc in an existing tarball.
``` js
var extract = tar.extract()
var pack = tar.pack()
var path = require('path')
extract.on('entry', function(header, stream, callback) {
// let's prefix all names with 'tmp'
header.name = path.join('tmp', header.name)
// write the new entry to the pack stream
stream.pipe(pack.entry(header, callback))
})
extract.on('finish', function() {
// all entries done - lets finalize it
pack.finalize()
})
// pipe the old tarball to the extractor
oldTarballStream.pipe(extract)
// pipe the new tarball the another stream
pack.pipe(newTarballStream)
```
## Saving tarball to fs
``` js
var fs = require('fs')
var tar = require('tar-stream')
var pack = tar.pack() // pack is a streams2 stream
var path = 'YourTarBall.tar'
var yourTarball = fs.createWriteStream(path)
// add a file called YourFile.txt with the content "Hello World!"
pack.entry({name: 'YourFile.txt'}, 'Hello World!', function (err) {
if (err) throw err
pack.finalize()
})
// pipe the pack stream to your file
pack.pipe(yourTarball)
yourTarball.on('close', function () {
console.log(path + ' has been written')
fs.stat(path, function(err, stats) {
if (err) throw err
console.log(stats)
console.log('Got file info successfully!')
})
})
```
## Performance
[See tar-fs for a performance comparison with node-tar](https://github.com/mafintosh/tar-fs/blob/master/README.md#performance)
# License
MIT

View file

@ -0,0 +1,257 @@
var util = require('util')
var bl = require('bl')
var headers = require('./headers')
var Writable = require('readable-stream').Writable
var PassThrough = require('readable-stream').PassThrough
var noop = function () {}
var overflow = function (size) {
size &= 511
return size && 512 - size
}
var emptyStream = function (self, offset) {
var s = new Source(self, offset)
s.end()
return s
}
var mixinPax = function (header, pax) {
if (pax.path) header.name = pax.path
if (pax.linkpath) header.linkname = pax.linkpath
if (pax.size) header.size = parseInt(pax.size, 10)
header.pax = pax
return header
}
var Source = function (self, offset) {
this._parent = self
this.offset = offset
PassThrough.call(this, { autoDestroy: false })
}
util.inherits(Source, PassThrough)
Source.prototype.destroy = function (err) {
this._parent.destroy(err)
}
var Extract = function (opts) {
if (!(this instanceof Extract)) return new Extract(opts)
Writable.call(this, opts)
opts = opts || {}
this._offset = 0
this._buffer = bl()
this._missing = 0
this._partial = false
this._onparse = noop
this._header = null
this._stream = null
this._overflow = null
this._cb = null
this._locked = false
this._destroyed = false
this._pax = null
this._paxGlobal = null
this._gnuLongPath = null
this._gnuLongLinkPath = null
var self = this
var b = self._buffer
var oncontinue = function () {
self._continue()
}
var onunlock = function (err) {
self._locked = false
if (err) return self.destroy(err)
if (!self._stream) oncontinue()
}
var onstreamend = function () {
self._stream = null
var drain = overflow(self._header.size)
if (drain) self._parse(drain, ondrain)
else self._parse(512, onheader)
if (!self._locked) oncontinue()
}
var ondrain = function () {
self._buffer.consume(overflow(self._header.size))
self._parse(512, onheader)
oncontinue()
}
var onpaxglobalheader = function () {
var size = self._header.size
self._paxGlobal = headers.decodePax(b.slice(0, size))
b.consume(size)
onstreamend()
}
var onpaxheader = function () {
var size = self._header.size
self._pax = headers.decodePax(b.slice(0, size))
if (self._paxGlobal) self._pax = Object.assign({}, self._paxGlobal, self._pax)
b.consume(size)
onstreamend()
}
var ongnulongpath = function () {
var size = self._header.size
this._gnuLongPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding)
b.consume(size)
onstreamend()
}
var ongnulonglinkpath = function () {
var size = self._header.size
this._gnuLongLinkPath = headers.decodeLongPath(b.slice(0, size), opts.filenameEncoding)
b.consume(size)
onstreamend()
}
var onheader = function () {
var offset = self._offset
var header
try {
header = self._header = headers.decode(b.slice(0, 512), opts.filenameEncoding, opts.allowUnknownFormat)
} catch (err) {
self.emit('error', err)
}
b.consume(512)
if (!header) {
self._parse(512, onheader)
oncontinue()
return
}
if (header.type === 'gnu-long-path') {
self._parse(header.size, ongnulongpath)
oncontinue()
return
}
if (header.type === 'gnu-long-link-path') {
self._parse(header.size, ongnulonglinkpath)
oncontinue()
return
}
if (header.type === 'pax-global-header') {
self._parse(header.size, onpaxglobalheader)
oncontinue()
return
}
if (header.type === 'pax-header') {
self._parse(header.size, onpaxheader)
oncontinue()
return
}
if (self._gnuLongPath) {
header.name = self._gnuLongPath
self._gnuLongPath = null
}
if (self._gnuLongLinkPath) {
header.linkname = self._gnuLongLinkPath
self._gnuLongLinkPath = null
}
if (self._pax) {
self._header = header = mixinPax(header, self._pax)
self._pax = null
}
self._locked = true
if (!header.size || header.type === 'directory') {
self._parse(512, onheader)
self.emit('entry', header, emptyStream(self, offset), onunlock)
return
}
self._stream = new Source(self, offset)
self.emit('entry', header, self._stream, onunlock)
self._parse(header.size, onstreamend)
oncontinue()
}
this._onheader = onheader
this._parse(512, onheader)
}
util.inherits(Extract, Writable)
Extract.prototype.destroy = function (err) {
if (this._destroyed) return
this._destroyed = true
if (err) this.emit('error', err)
this.emit('close')
if (this._stream) this._stream.emit('close')
}
Extract.prototype._parse = function (size, onparse) {
if (this._destroyed) return
this._offset += size
this._missing = size
if (onparse === this._onheader) this._partial = false
this._onparse = onparse
}
Extract.prototype._continue = function () {
if (this._destroyed) return
var cb = this._cb
this._cb = noop
if (this._overflow) this._write(this._overflow, undefined, cb)
else cb()
}
Extract.prototype._write = function (data, enc, cb) {
if (this._destroyed) return
var s = this._stream
var b = this._buffer
var missing = this._missing
if (data.length) this._partial = true
// we do not reach end-of-chunk now. just forward it
if (data.length < missing) {
this._missing -= data.length
this._overflow = null
if (s) return s.write(data, cb)
b.append(data)
return cb()
}
// end-of-chunk. the parser should call cb.
this._cb = cb
this._missing = 0
var overflow = null
if (data.length > missing) {
overflow = data.slice(missing)
data = data.slice(0, missing)
}
if (s) s.end(data)
else b.append(data)
this._overflow = overflow
this._onparse()
}
Extract.prototype._final = function (cb) {
if (this._partial) return this.destroy(new Error('Unexpected end of data'))
cb()
}
module.exports = Extract

View file

@ -0,0 +1,295 @@
var alloc = Buffer.alloc
var ZEROS = '0000000000000000000'
var SEVENS = '7777777777777777777'
var ZERO_OFFSET = '0'.charCodeAt(0)
var USTAR_MAGIC = Buffer.from('ustar\x00', 'binary')
var USTAR_VER = Buffer.from('00', 'binary')
var GNU_MAGIC = Buffer.from('ustar\x20', 'binary')
var GNU_VER = Buffer.from('\x20\x00', 'binary')
var MASK = parseInt('7777', 8)
var MAGIC_OFFSET = 257
var VERSION_OFFSET = 263
var clamp = function (index, len, defaultValue) {
if (typeof index !== 'number') return defaultValue
index = ~~index // Coerce to integer.
if (index >= len) return len
if (index >= 0) return index
index += len
if (index >= 0) return index
return 0
}
var toType = function (flag) {
switch (flag) {
case 0:
return 'file'
case 1:
return 'link'
case 2:
return 'symlink'
case 3:
return 'character-device'
case 4:
return 'block-device'
case 5:
return 'directory'
case 6:
return 'fifo'
case 7:
return 'contiguous-file'
case 72:
return 'pax-header'
case 55:
return 'pax-global-header'
case 27:
return 'gnu-long-link-path'
case 28:
case 30:
return 'gnu-long-path'
}
return null
}
var toTypeflag = function (flag) {
switch (flag) {
case 'file':
return 0
case 'link':
return 1
case 'symlink':
return 2
case 'character-device':
return 3
case 'block-device':
return 4
case 'directory':
return 5
case 'fifo':
return 6
case 'contiguous-file':
return 7
case 'pax-header':
return 72
}
return 0
}
var indexOf = function (block, num, offset, end) {
for (; offset < end; offset++) {
if (block[offset] === num) return offset
}
return end
}
var cksum = function (block) {
var sum = 8 * 32
for (var i = 0; i < 148; i++) sum += block[i]
for (var j = 156; j < 512; j++) sum += block[j]
return sum
}
var encodeOct = function (val, n) {
val = val.toString(8)
if (val.length > n) return SEVENS.slice(0, n) + ' '
else return ZEROS.slice(0, n - val.length) + val + ' '
}
/* Copied from the node-tar repo and modified to meet
* tar-stream coding standard.
*
* Source: https://github.com/npm/node-tar/blob/51b6627a1f357d2eb433e7378e5f05e83b7aa6cd/lib/header.js#L349
*/
function parse256 (buf) {
// first byte MUST be either 80 or FF
// 80 for positive, FF for 2's comp
var positive
if (buf[0] === 0x80) positive = true
else if (buf[0] === 0xFF) positive = false
else return null
// build up a base-256 tuple from the least sig to the highest
var tuple = []
for (var i = buf.length - 1; i > 0; i--) {
var byte = buf[i]
if (positive) tuple.push(byte)
else tuple.push(0xFF - byte)
}
var sum = 0
var l = tuple.length
for (i = 0; i < l; i++) {
sum += tuple[i] * Math.pow(256, i)
}
return positive ? sum : -1 * sum
}
var decodeOct = function (val, offset, length) {
val = val.slice(offset, offset + length)
offset = 0
// If prefixed with 0x80 then parse as a base-256 integer
if (val[offset] & 0x80) {
return parse256(val)
} else {
// Older versions of tar can prefix with spaces
while (offset < val.length && val[offset] === 32) offset++
var end = clamp(indexOf(val, 32, offset, val.length), val.length, val.length)
while (offset < end && val[offset] === 0) offset++
if (end === offset) return 0
return parseInt(val.slice(offset, end).toString(), 8)
}
}
var decodeStr = function (val, offset, length, encoding) {
return val.slice(offset, indexOf(val, 0, offset, offset + length)).toString(encoding)
}
var addLength = function (str) {
var len = Buffer.byteLength(str)
var digits = Math.floor(Math.log(len) / Math.log(10)) + 1
if (len + digits >= Math.pow(10, digits)) digits++
return (len + digits) + str
}
exports.decodeLongPath = function (buf, encoding) {
return decodeStr(buf, 0, buf.length, encoding)
}
exports.encodePax = function (opts) { // TODO: encode more stuff in pax
var result = ''
if (opts.name) result += addLength(' path=' + opts.name + '\n')
if (opts.linkname) result += addLength(' linkpath=' + opts.linkname + '\n')
var pax = opts.pax
if (pax) {
for (var key in pax) {
result += addLength(' ' + key + '=' + pax[key] + '\n')
}
}
return Buffer.from(result)
}
exports.decodePax = function (buf) {
var result = {}
while (buf.length) {
var i = 0
while (i < buf.length && buf[i] !== 32) i++
var len = parseInt(buf.slice(0, i).toString(), 10)
if (!len) return result
var b = buf.slice(i + 1, len - 1).toString()
var keyIndex = b.indexOf('=')
if (keyIndex === -1) return result
result[b.slice(0, keyIndex)] = b.slice(keyIndex + 1)
buf = buf.slice(len)
}
return result
}
exports.encode = function (opts) {
var buf = alloc(512)
var name = opts.name
var prefix = ''
if (opts.typeflag === 5 && name[name.length - 1] !== '/') name += '/'
if (Buffer.byteLength(name) !== name.length) return null // utf-8
while (Buffer.byteLength(name) > 100) {
var i = name.indexOf('/')
if (i === -1) return null
prefix += prefix ? '/' + name.slice(0, i) : name.slice(0, i)
name = name.slice(i + 1)
}
if (Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155) return null
if (opts.linkname && Buffer.byteLength(opts.linkname) > 100) return null
buf.write(name)
buf.write(encodeOct(opts.mode & MASK, 6), 100)
buf.write(encodeOct(opts.uid, 6), 108)
buf.write(encodeOct(opts.gid, 6), 116)
buf.write(encodeOct(opts.size, 11), 124)
buf.write(encodeOct((opts.mtime.getTime() / 1000) | 0, 11), 136)
buf[156] = ZERO_OFFSET + toTypeflag(opts.type)
if (opts.linkname) buf.write(opts.linkname, 157)
USTAR_MAGIC.copy(buf, MAGIC_OFFSET)
USTAR_VER.copy(buf, VERSION_OFFSET)
if (opts.uname) buf.write(opts.uname, 265)
if (opts.gname) buf.write(opts.gname, 297)
buf.write(encodeOct(opts.devmajor || 0, 6), 329)
buf.write(encodeOct(opts.devminor || 0, 6), 337)
if (prefix) buf.write(prefix, 345)
buf.write(encodeOct(cksum(buf), 6), 148)
return buf
}
exports.decode = function (buf, filenameEncoding, allowUnknownFormat) {
var typeflag = buf[156] === 0 ? 0 : buf[156] - ZERO_OFFSET
var name = decodeStr(buf, 0, 100, filenameEncoding)
var mode = decodeOct(buf, 100, 8)
var uid = decodeOct(buf, 108, 8)
var gid = decodeOct(buf, 116, 8)
var size = decodeOct(buf, 124, 12)
var mtime = decodeOct(buf, 136, 12)
var type = toType(typeflag)
var linkname = buf[157] === 0 ? null : decodeStr(buf, 157, 100, filenameEncoding)
var uname = decodeStr(buf, 265, 32)
var gname = decodeStr(buf, 297, 32)
var devmajor = decodeOct(buf, 329, 8)
var devminor = decodeOct(buf, 337, 8)
var c = cksum(buf)
// checksum is still initial value if header was null.
if (c === 8 * 32) return null
// valid checksum
if (c !== decodeOct(buf, 148, 8)) throw new Error('Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?')
if (USTAR_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0) {
// ustar (posix) format.
// prepend prefix, if present.
if (buf[345]) name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name
} else if (GNU_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0 &&
GNU_VER.compare(buf, VERSION_OFFSET, VERSION_OFFSET + 2) === 0) {
// 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and
// multi-volume tarballs.
} else {
if (!allowUnknownFormat) {
throw new Error('Invalid tar header: unknown format.')
}
}
// to support old tar versions that use trailing / to indicate dirs
if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5
return {
name,
mode,
uid,
gid,
size,
mtime: new Date(1000 * mtime),
type,
linkname,
uname,
gname,
devmajor,
devminor
}
}

View file

@ -0,0 +1,2 @@
exports.extract = require('./extract')
exports.pack = require('./pack')

View file

@ -0,0 +1,255 @@
var constants = require('fs-constants')
var eos = require('end-of-stream')
var inherits = require('inherits')
var alloc = Buffer.alloc
var Readable = require('readable-stream').Readable
var Writable = require('readable-stream').Writable
var StringDecoder = require('string_decoder').StringDecoder
var headers = require('./headers')
var DMODE = parseInt('755', 8)
var FMODE = parseInt('644', 8)
var END_OF_TAR = alloc(1024)
var noop = function () {}
var overflow = function (self, size) {
size &= 511
if (size) self.push(END_OF_TAR.slice(0, 512 - size))
}
function modeToType (mode) {
switch (mode & constants.S_IFMT) {
case constants.S_IFBLK: return 'block-device'
case constants.S_IFCHR: return 'character-device'
case constants.S_IFDIR: return 'directory'
case constants.S_IFIFO: return 'fifo'
case constants.S_IFLNK: return 'symlink'
}
return 'file'
}
var Sink = function (to) {
Writable.call(this)
this.written = 0
this._to = to
this._destroyed = false
}
inherits(Sink, Writable)
Sink.prototype._write = function (data, enc, cb) {
this.written += data.length
if (this._to.push(data)) return cb()
this._to._drain = cb
}
Sink.prototype.destroy = function () {
if (this._destroyed) return
this._destroyed = true
this.emit('close')
}
var LinkSink = function () {
Writable.call(this)
this.linkname = ''
this._decoder = new StringDecoder('utf-8')
this._destroyed = false
}
inherits(LinkSink, Writable)
LinkSink.prototype._write = function (data, enc, cb) {
this.linkname += this._decoder.write(data)
cb()
}
LinkSink.prototype.destroy = function () {
if (this._destroyed) return
this._destroyed = true
this.emit('close')
}
var Void = function () {
Writable.call(this)
this._destroyed = false
}
inherits(Void, Writable)
Void.prototype._write = function (data, enc, cb) {
cb(new Error('No body allowed for this entry'))
}
Void.prototype.destroy = function () {
if (this._destroyed) return
this._destroyed = true
this.emit('close')
}
var Pack = function (opts) {
if (!(this instanceof Pack)) return new Pack(opts)
Readable.call(this, opts)
this._drain = noop
this._finalized = false
this._finalizing = false
this._destroyed = false
this._stream = null
}
inherits(Pack, Readable)
Pack.prototype.entry = function (header, buffer, callback) {
if (this._stream) throw new Error('already piping an entry')
if (this._finalized || this._destroyed) return
if (typeof buffer === 'function') {
callback = buffer
buffer = null
}
if (!callback) callback = noop
var self = this
if (!header.size || header.type === 'symlink') header.size = 0
if (!header.type) header.type = modeToType(header.mode)
if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE
if (!header.uid) header.uid = 0
if (!header.gid) header.gid = 0
if (!header.mtime) header.mtime = new Date()
if (typeof buffer === 'string') buffer = Buffer.from(buffer)
if (Buffer.isBuffer(buffer)) {
header.size = buffer.length
this._encode(header)
var ok = this.push(buffer)
overflow(self, header.size)
if (ok) process.nextTick(callback)
else this._drain = callback
return new Void()
}
if (header.type === 'symlink' && !header.linkname) {
var linkSink = new LinkSink()
eos(linkSink, function (err) {
if (err) { // stream was closed
self.destroy()
return callback(err)
}
header.linkname = linkSink.linkname
self._encode(header)
callback()
})
return linkSink
}
this._encode(header)
if (header.type !== 'file' && header.type !== 'contiguous-file') {
process.nextTick(callback)
return new Void()
}
var sink = new Sink(this)
this._stream = sink
eos(sink, function (err) {
self._stream = null
if (err) { // stream was closed
self.destroy()
return callback(err)
}
if (sink.written !== header.size) { // corrupting tar
self.destroy()
return callback(new Error('size mismatch'))
}
overflow(self, header.size)
if (self._finalizing) self.finalize()
callback()
})
return sink
}
Pack.prototype.finalize = function () {
if (this._stream) {
this._finalizing = true
return
}
if (this._finalized) return
this._finalized = true
this.push(END_OF_TAR)
this.push(null)
}
Pack.prototype.destroy = function (err) {
if (this._destroyed) return
this._destroyed = true
if (err) this.emit('error', err)
this.emit('close')
if (this._stream && this._stream.destroy) this._stream.destroy()
}
Pack.prototype._encode = function (header) {
if (!header.pax) {
var buf = headers.encode(header)
if (buf) {
this.push(buf)
return
}
}
this._encodePax(header)
}
Pack.prototype._encodePax = function (header) {
var paxHeader = headers.encodePax({
name: header.name,
linkname: header.linkname,
pax: header.pax
})
var newHeader = {
name: 'PaxHeader',
mode: header.mode,
uid: header.uid,
gid: header.gid,
size: paxHeader.length,
mtime: header.mtime,
type: 'pax-header',
linkname: header.linkname && 'PaxHeader',
uname: header.uname,
gname: header.gname,
devmajor: header.devmajor,
devminor: header.devminor
}
this.push(headers.encode(newHeader))
this.push(paxHeader)
overflow(this, paxHeader.length)
newHeader.size = header.size
newHeader.type = header.type
this.push(headers.encode(newHeader))
}
Pack.prototype._read = function (n) {
var drain = this._drain
this._drain = noop
drain()
}
module.exports = Pack

View file

@ -0,0 +1,58 @@
{
"name": "tar-stream",
"version": "2.2.0",
"description": "tar-stream is a streaming tar parser and generator and nothing else. It is streams2 and operates purely using streams which means you can easily extract/parse tarballs without ever hitting the file system.",
"author": "Mathias Buus <mathiasbuus@gmail.com>",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"devDependencies": {
"concat-stream": "^2.0.0",
"standard": "^12.0.1",
"tape": "^4.9.2"
},
"scripts": {
"test": "standard && tape test/extract.js test/pack.js",
"test-all": "standard && tape test/*.js"
},
"keywords": [
"tar",
"tarball",
"parse",
"parser",
"generate",
"generator",
"stream",
"stream2",
"streams",
"streams2",
"streaming",
"pack",
"extract",
"modify"
],
"bugs": {
"url": "https://github.com/mafintosh/tar-stream/issues"
},
"homepage": "https://github.com/mafintosh/tar-stream",
"main": "index.js",
"files": [
"*.js",
"LICENSE"
],
"directories": {
"test": "test"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/mafintosh/tar-stream.git"
},
"engines": {
"node": ">=6"
}
}

View file

@ -0,0 +1,11 @@
const tar = require('tar-stream')
const fs = require('fs')
const path = require('path')
const pipeline = require('pump') // eequire('stream').pipeline
fs.createReadStream('test.tar')
.pipe(tar.extract())
.on('entry', function (header, stream, done) {
console.log(header.name)
pipeline(stream, fs.createWriteStream(path.join('/tmp', header.name)), done)
})