1'use strict'
2// parse a 512-byte header block to a data object, or vice-versa
3// encode returns `true` if a pax extended header is needed, because
4// the data could not be faithfully encoded in a simple header.
5// (Also, check header.needPax to see if it needs a pax header.)
6
7const types = require('./types.js')
8const pathModule = require('path').posix
9const large = require('./large-numbers.js')
10
11const SLURP = Symbol('slurp')
12const TYPE = Symbol('type')
13
14class Header {
15  constructor (data, off, ex, gex) {
16    this.cksumValid = false
17    this.needPax = false
18    this.nullBlock = false
19
20    this.block = null
21    this.path = null
22    this.mode = null
23    this.uid = null
24    this.gid = null
25    this.size = null
26    this.mtime = null
27    this.cksum = null
28    this[TYPE] = '0'
29    this.linkpath = null
30    this.uname = null
31    this.gname = null
32    this.devmaj = 0
33    this.devmin = 0
34    this.atime = null
35    this.ctime = null
36
37    if (Buffer.isBuffer(data)) {
38      this.decode(data, off || 0, ex, gex)
39    } else if (data) {
40      this.set(data)
41    }
42  }
43
44  decode (buf, off, ex, gex) {
45    if (!off) {
46      off = 0
47    }
48
49    if (!buf || !(buf.length >= off + 512)) {
50      throw new Error('need 512 bytes for header')
51    }
52
53    this.path = decString(buf, off, 100)
54    this.mode = decNumber(buf, off + 100, 8)
55    this.uid = decNumber(buf, off + 108, 8)
56    this.gid = decNumber(buf, off + 116, 8)
57    this.size = decNumber(buf, off + 124, 12)
58    this.mtime = decDate(buf, off + 136, 12)
59    this.cksum = decNumber(buf, off + 148, 12)
60
61    // if we have extended or global extended headers, apply them now
62    // See https://github.com/npm/node-tar/pull/187
63    this[SLURP](ex)
64    this[SLURP](gex, true)
65
66    // old tar versions marked dirs as a file with a trailing /
67    this[TYPE] = decString(buf, off + 156, 1)
68    if (this[TYPE] === '') {
69      this[TYPE] = '0'
70    }
71    if (this[TYPE] === '0' && this.path.slice(-1) === '/') {
72      this[TYPE] = '5'
73    }
74
75    // tar implementations sometimes incorrectly put the stat(dir).size
76    // as the size in the tarball, even though Directory entries are
77    // not able to have any body at all.  In the very rare chance that
78    // it actually DOES have a body, we weren't going to do anything with
79    // it anyway, and it'll just be a warning about an invalid header.
80    if (this[TYPE] === '5') {
81      this.size = 0
82    }
83
84    this.linkpath = decString(buf, off + 157, 100)
85    if (buf.slice(off + 257, off + 265).toString() === 'ustar\u000000') {
86      this.uname = decString(buf, off + 265, 32)
87      this.gname = decString(buf, off + 297, 32)
88      this.devmaj = decNumber(buf, off + 329, 8)
89      this.devmin = decNumber(buf, off + 337, 8)
90      if (buf[off + 475] !== 0) {
91        // definitely a prefix, definitely >130 chars.
92        const prefix = decString(buf, off + 345, 155)
93        this.path = prefix + '/' + this.path
94      } else {
95        const prefix = decString(buf, off + 345, 130)
96        if (prefix) {
97          this.path = prefix + '/' + this.path
98        }
99        this.atime = decDate(buf, off + 476, 12)
100        this.ctime = decDate(buf, off + 488, 12)
101      }
102    }
103
104    let sum = 8 * 0x20
105    for (let i = off; i < off + 148; i++) {
106      sum += buf[i]
107    }
108
109    for (let i = off + 156; i < off + 512; i++) {
110      sum += buf[i]
111    }
112
113    this.cksumValid = sum === this.cksum
114    if (this.cksum === null && sum === 8 * 0x20) {
115      this.nullBlock = true
116    }
117  }
118
119  [SLURP] (ex, global) {
120    for (const k in ex) {
121      // we slurp in everything except for the path attribute in
122      // a global extended header, because that's weird.
123      if (ex[k] !== null && ex[k] !== undefined &&
124          !(global && k === 'path')) {
125        this[k] = ex[k]
126      }
127    }
128  }
129
130  encode (buf, off) {
131    if (!buf) {
132      buf = this.block = Buffer.alloc(512)
133      off = 0
134    }
135
136    if (!off) {
137      off = 0
138    }
139
140    if (!(buf.length >= off + 512)) {
141      throw new Error('need 512 bytes for header')
142    }
143
144    const prefixSize = this.ctime || this.atime ? 130 : 155
145    const split = splitPrefix(this.path || '', prefixSize)
146    const path = split[0]
147    const prefix = split[1]
148    this.needPax = split[2]
149
150    this.needPax = encString(buf, off, 100, path) || this.needPax
151    this.needPax = encNumber(buf, off + 100, 8, this.mode) || this.needPax
152    this.needPax = encNumber(buf, off + 108, 8, this.uid) || this.needPax
153    this.needPax = encNumber(buf, off + 116, 8, this.gid) || this.needPax
154    this.needPax = encNumber(buf, off + 124, 12, this.size) || this.needPax
155    this.needPax = encDate(buf, off + 136, 12, this.mtime) || this.needPax
156    buf[off + 156] = this[TYPE].charCodeAt(0)
157    this.needPax = encString(buf, off + 157, 100, this.linkpath) || this.needPax
158    buf.write('ustar\u000000', off + 257, 8)
159    this.needPax = encString(buf, off + 265, 32, this.uname) || this.needPax
160    this.needPax = encString(buf, off + 297, 32, this.gname) || this.needPax
161    this.needPax = encNumber(buf, off + 329, 8, this.devmaj) || this.needPax
162    this.needPax = encNumber(buf, off + 337, 8, this.devmin) || this.needPax
163    this.needPax = encString(buf, off + 345, prefixSize, prefix) || this.needPax
164    if (buf[off + 475] !== 0) {
165      this.needPax = encString(buf, off + 345, 155, prefix) || this.needPax
166    } else {
167      this.needPax = encString(buf, off + 345, 130, prefix) || this.needPax
168      this.needPax = encDate(buf, off + 476, 12, this.atime) || this.needPax
169      this.needPax = encDate(buf, off + 488, 12, this.ctime) || this.needPax
170    }
171
172    let sum = 8 * 0x20
173    for (let i = off; i < off + 148; i++) {
174      sum += buf[i]
175    }
176
177    for (let i = off + 156; i < off + 512; i++) {
178      sum += buf[i]
179    }
180
181    this.cksum = sum
182    encNumber(buf, off + 148, 8, this.cksum)
183    this.cksumValid = true
184
185    return this.needPax
186  }
187
188  set (data) {
189    for (const i in data) {
190      if (data[i] !== null && data[i] !== undefined) {
191        this[i] = data[i]
192      }
193    }
194  }
195
196  get type () {
197    return types.name.get(this[TYPE]) || this[TYPE]
198  }
199
200  get typeKey () {
201    return this[TYPE]
202  }
203
204  set type (type) {
205    if (types.code.has(type)) {
206      this[TYPE] = types.code.get(type)
207    } else {
208      this[TYPE] = type
209    }
210  }
211}
212
213const splitPrefix = (p, prefixSize) => {
214  const pathSize = 100
215  let pp = p
216  let prefix = ''
217  let ret
218  const root = pathModule.parse(p).root || '.'
219
220  if (Buffer.byteLength(pp) < pathSize) {
221    ret = [pp, prefix, false]
222  } else {
223    // first set prefix to the dir, and path to the base
224    prefix = pathModule.dirname(pp)
225    pp = pathModule.basename(pp)
226
227    do {
228      if (Buffer.byteLength(pp) <= pathSize &&
229          Buffer.byteLength(prefix) <= prefixSize) {
230        // both fit!
231        ret = [pp, prefix, false]
232      } else if (Buffer.byteLength(pp) > pathSize &&
233          Buffer.byteLength(prefix) <= prefixSize) {
234        // prefix fits in prefix, but path doesn't fit in path
235        ret = [pp.slice(0, pathSize - 1), prefix, true]
236      } else {
237        // make path take a bit from prefix
238        pp = pathModule.join(pathModule.basename(prefix), pp)
239        prefix = pathModule.dirname(prefix)
240      }
241    } while (prefix !== root && !ret)
242
243    // at this point, found no resolution, just truncate
244    if (!ret) {
245      ret = [p.slice(0, pathSize - 1), '', true]
246    }
247  }
248  return ret
249}
250
251const decString = (buf, off, size) =>
252  buf.slice(off, off + size).toString('utf8').replace(/\0.*/, '')
253
254const decDate = (buf, off, size) =>
255  numToDate(decNumber(buf, off, size))
256
257const numToDate = num => num === null ? null : new Date(num * 1000)
258
259const decNumber = (buf, off, size) =>
260  buf[off] & 0x80 ? large.parse(buf.slice(off, off + size))
261  : decSmallNumber(buf, off, size)
262
263const nanNull = value => isNaN(value) ? null : value
264
265const decSmallNumber = (buf, off, size) =>
266  nanNull(parseInt(
267    buf.slice(off, off + size)
268      .toString('utf8').replace(/\0.*$/, '').trim(), 8))
269
270// the maximum encodable as a null-terminated octal, by field size
271const MAXNUM = {
272  12: 0o77777777777,
273  8: 0o7777777,
274}
275
276const encNumber = (buf, off, size, number) =>
277  number === null ? false :
278  number > MAXNUM[size] || number < 0
279    ? (large.encode(number, buf.slice(off, off + size)), true)
280    : (encSmallNumber(buf, off, size, number), false)
281
282const encSmallNumber = (buf, off, size, number) =>
283  buf.write(octalString(number, size), off, size, 'ascii')
284
285const octalString = (number, size) =>
286  padOctal(Math.floor(number).toString(8), size)
287
288const padOctal = (string, size) =>
289  (string.length === size - 1 ? string
290  : new Array(size - string.length - 1).join('0') + string + ' ') + '\0'
291
292const encDate = (buf, off, size, date) =>
293  date === null ? false :
294  encNumber(buf, off, size, date.getTime() / 1000)
295
296// enough to fill the longest string we've got
297const NULLS = new Array(156).join('\0')
298// pad with nulls, return true if it's longer or non-ascii
299const encString = (buf, off, size, string) =>
300  string === null ? false :
301  (buf.write(string + NULLS, off, size, 'utf8'),
302  string.length !== Buffer.byteLength(string) || string.length > size)
303
304module.exports = Header
305