pack.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. const { Readable, Writable, getStreamError } = require('streamx')
  2. const b4a = require('b4a')
  3. const constants = require('./constants')
  4. const headers = require('./headers')
  5. const DMODE = 0o755
  6. const FMODE = 0o644
  7. const END_OF_TAR = b4a.alloc(1024)
  8. class Sink extends Writable {
  9. constructor (pack, header, callback) {
  10. super({ mapWritable, eagerOpen: true })
  11. this.written = 0
  12. this.header = header
  13. this._callback = callback
  14. this._linkname = null
  15. this._isLinkname = header.type === 'symlink' && !header.linkname
  16. this._isVoid = header.type !== 'file' && header.type !== 'contiguous-file'
  17. this._finished = false
  18. this._pack = pack
  19. this._openCallback = null
  20. if (this._pack._stream === null) this._pack._stream = this
  21. else this._pack._pending.push(this)
  22. }
  23. _open (cb) {
  24. this._openCallback = cb
  25. if (this._pack._stream === this) this._continueOpen()
  26. }
  27. _continuePack (err) {
  28. if (this._callback === null) return
  29. const callback = this._callback
  30. this._callback = null
  31. callback(err)
  32. }
  33. _continueOpen () {
  34. if (this._pack._stream === null) this._pack._stream = this
  35. const cb = this._openCallback
  36. this._openCallback = null
  37. if (cb === null) return
  38. if (this._pack.destroying) return cb(new Error('pack stream destroyed'))
  39. if (this._pack._finalized) return cb(new Error('pack stream is already finalized'))
  40. this._pack._stream = this
  41. if (!this._isLinkname) {
  42. this._pack._encode(this.header)
  43. }
  44. if (this._isVoid) {
  45. this._finish()
  46. this._continuePack(null)
  47. }
  48. cb(null)
  49. }
  50. _write (data, cb) {
  51. if (this._isLinkname) {
  52. this._linkname = this._linkname ? b4a.concat([this._linkname, data]) : data
  53. return cb(null)
  54. }
  55. if (this._isVoid) {
  56. if (data.byteLength > 0) {
  57. return cb(new Error('No body allowed for this entry'))
  58. }
  59. return cb()
  60. }
  61. this.written += data.byteLength
  62. if (this._pack.push(data)) return cb()
  63. this._pack._drain = cb
  64. }
  65. _finish () {
  66. if (this._finished) return
  67. this._finished = true
  68. if (this._isLinkname) {
  69. this.header.linkname = this._linkname ? b4a.toString(this._linkname, 'utf-8') : ''
  70. this._pack._encode(this.header)
  71. }
  72. overflow(this._pack, this.header.size)
  73. this._pack._done(this)
  74. }
  75. _final (cb) {
  76. if (this.written !== this.header.size) { // corrupting tar
  77. return cb(new Error('Size mismatch'))
  78. }
  79. this._finish()
  80. cb(null)
  81. }
  82. _getError () {
  83. return getStreamError(this) || new Error('tar entry destroyed')
  84. }
  85. _predestroy () {
  86. this._pack.destroy(this._getError())
  87. }
  88. _destroy (cb) {
  89. this._pack._done(this)
  90. this._continuePack(this._finished ? null : this._getError())
  91. cb()
  92. }
  93. }
  94. class Pack extends Readable {
  95. constructor (opts) {
  96. super(opts)
  97. this._drain = noop
  98. this._finalized = false
  99. this._finalizing = false
  100. this._pending = []
  101. this._stream = null
  102. }
  103. entry (header, buffer, callback) {
  104. if (this._finalized || this.destroying) throw new Error('already finalized or destroyed')
  105. if (typeof buffer === 'function') {
  106. callback = buffer
  107. buffer = null
  108. }
  109. if (!callback) callback = noop
  110. if (!header.size || header.type === 'symlink') header.size = 0
  111. if (!header.type) header.type = modeToType(header.mode)
  112. if (!header.mode) header.mode = header.type === 'directory' ? DMODE : FMODE
  113. if (!header.uid) header.uid = 0
  114. if (!header.gid) header.gid = 0
  115. if (!header.mtime) header.mtime = new Date()
  116. if (typeof buffer === 'string') buffer = b4a.from(buffer)
  117. const sink = new Sink(this, header, callback)
  118. if (b4a.isBuffer(buffer)) {
  119. header.size = buffer.byteLength
  120. sink.write(buffer)
  121. sink.end()
  122. return sink
  123. }
  124. if (sink._isVoid) {
  125. return sink
  126. }
  127. return sink
  128. }
  129. finalize () {
  130. if (this._stream || this._pending.length > 0) {
  131. this._finalizing = true
  132. return
  133. }
  134. if (this._finalized) return
  135. this._finalized = true
  136. this.push(END_OF_TAR)
  137. this.push(null)
  138. }
  139. _done (stream) {
  140. if (stream !== this._stream) return
  141. this._stream = null
  142. if (this._finalizing) this.finalize()
  143. if (this._pending.length) this._pending.shift()._continueOpen()
  144. }
  145. _encode (header) {
  146. if (!header.pax) {
  147. const buf = headers.encode(header)
  148. if (buf) {
  149. this.push(buf)
  150. return
  151. }
  152. }
  153. this._encodePax(header)
  154. }
  155. _encodePax (header) {
  156. const paxHeader = headers.encodePax({
  157. name: header.name,
  158. linkname: header.linkname,
  159. pax: header.pax
  160. })
  161. const newHeader = {
  162. name: 'PaxHeader',
  163. mode: header.mode,
  164. uid: header.uid,
  165. gid: header.gid,
  166. size: paxHeader.byteLength,
  167. mtime: header.mtime,
  168. type: 'pax-header',
  169. linkname: header.linkname && 'PaxHeader',
  170. uname: header.uname,
  171. gname: header.gname,
  172. devmajor: header.devmajor,
  173. devminor: header.devminor
  174. }
  175. this.push(headers.encode(newHeader))
  176. this.push(paxHeader)
  177. overflow(this, paxHeader.byteLength)
  178. newHeader.size = header.size
  179. newHeader.type = header.type
  180. this.push(headers.encode(newHeader))
  181. }
  182. _doDrain () {
  183. const drain = this._drain
  184. this._drain = noop
  185. drain()
  186. }
  187. _predestroy () {
  188. const err = getStreamError(this)
  189. if (this._stream) this._stream.destroy(err)
  190. while (this._pending.length) {
  191. const stream = this._pending.shift()
  192. stream.destroy(err)
  193. stream._continueOpen()
  194. }
  195. this._doDrain()
  196. }
  197. _read (cb) {
  198. this._doDrain()
  199. cb()
  200. }
  201. }
  202. module.exports = function pack (opts) {
  203. return new Pack(opts)
  204. }
  205. function modeToType (mode) {
  206. switch (mode & constants.S_IFMT) {
  207. case constants.S_IFBLK: return 'block-device'
  208. case constants.S_IFCHR: return 'character-device'
  209. case constants.S_IFDIR: return 'directory'
  210. case constants.S_IFIFO: return 'fifo'
  211. case constants.S_IFLNK: return 'symlink'
  212. }
  213. return 'file'
  214. }
  215. function noop () {}
  216. function overflow (self, size) {
  217. size &= 511
  218. if (size) self.push(END_OF_TAR.subarray(0, 512 - size))
  219. }
  220. function mapWritable (buf) {
  221. return b4a.isBuffer(buf) ? buf : b4a.from(buf)
  222. }