1'use strict' 2const Header = require('./header.js') 3const path = require('path') 4 5class Pax { 6 constructor (obj, global) { 7 this.atime = obj.atime || null 8 this.charset = obj.charset || null 9 this.comment = obj.comment || null 10 this.ctime = obj.ctime || null 11 this.gid = obj.gid || null 12 this.gname = obj.gname || null 13 this.linkpath = obj.linkpath || null 14 this.mtime = obj.mtime || null 15 this.path = obj.path || null 16 this.size = obj.size || null 17 this.uid = obj.uid || null 18 this.uname = obj.uname || null 19 this.dev = obj.dev || null 20 this.ino = obj.ino || null 21 this.nlink = obj.nlink || null 22 this.global = global || false 23 } 24 25 encode () { 26 const body = this.encodeBody() 27 if (body === '') { 28 return null 29 } 30 31 const bodyLen = Buffer.byteLength(body) 32 // round up to 512 bytes 33 // add 512 for header 34 const bufLen = 512 * Math.ceil(1 + bodyLen / 512) 35 const buf = Buffer.allocUnsafe(bufLen) 36 37 // 0-fill the header section, it might not hit every field 38 for (let i = 0; i < 512; i++) { 39 buf[i] = 0 40 } 41 42 new Header({ 43 // XXX split the path 44 // then the path should be PaxHeader + basename, but less than 99, 45 // prepend with the dirname 46 path: ('PaxHeader/' + path.basename(this.path)).slice(0, 99), 47 mode: this.mode || 0o644, 48 uid: this.uid || null, 49 gid: this.gid || null, 50 size: bodyLen, 51 mtime: this.mtime || null, 52 type: this.global ? 'GlobalExtendedHeader' : 'ExtendedHeader', 53 linkpath: '', 54 uname: this.uname || '', 55 gname: this.gname || '', 56 devmaj: 0, 57 devmin: 0, 58 atime: this.atime || null, 59 ctime: this.ctime || null, 60 }).encode(buf) 61 62 buf.write(body, 512, bodyLen, 'utf8') 63 64 // null pad after the body 65 for (let i = bodyLen + 512; i < buf.length; i++) { 66 buf[i] = 0 67 } 68 69 return buf 70 } 71 72 encodeBody () { 73 return ( 74 this.encodeField('path') + 75 this.encodeField('ctime') + 76 this.encodeField('atime') + 77 this.encodeField('dev') + 78 this.encodeField('ino') + 79 this.encodeField('nlink') + 80 this.encodeField('charset') + 81 this.encodeField('comment') + 82 this.encodeField('gid') + 83 this.encodeField('gname') + 84 this.encodeField('linkpath') + 85 this.encodeField('mtime') + 86 this.encodeField('size') + 87 this.encodeField('uid') + 88 this.encodeField('uname') 89 ) 90 } 91 92 encodeField (field) { 93 if (this[field] === null || this[field] === undefined) { 94 return '' 95 } 96 const v = this[field] instanceof Date ? this[field].getTime() / 1000 97 : this[field] 98 const s = ' ' + 99 (field === 'dev' || field === 'ino' || field === 'nlink' 100 ? 'SCHILY.' : '') + 101 field + '=' + v + '\n' 102 const byteLen = Buffer.byteLength(s) 103 // the digits includes the length of the digits in ascii base-10 104 // so if it's 9 characters, then adding 1 for the 9 makes it 10 105 // which makes it 11 chars. 106 let digits = Math.floor(Math.log(byteLen) / Math.log(10)) + 1 107 if (byteLen + digits >= Math.pow(10, digits)) { 108 digits += 1 109 } 110 const len = digits + byteLen 111 return len + s 112 } 113} 114 115Pax.parse = (string, ex, g) => new Pax(merge(parseKV(string), ex), g) 116 117const merge = (a, b) => 118 b ? Object.keys(a).reduce((s, k) => (s[k] = a[k], s), b) : a 119 120const parseKV = string => 121 string 122 .replace(/\n$/, '') 123 .split('\n') 124 .reduce(parseKVLine, Object.create(null)) 125 126const parseKVLine = (set, line) => { 127 const n = parseInt(line, 10) 128 129 // XXX Values with \n in them will fail this. 130 // Refactor to not be a naive line-by-line parse. 131 if (n !== Buffer.byteLength(line) + 1) { 132 return set 133 } 134 135 line = line.slice((n + ' ').length) 136 const kv = line.split('=') 137 const k = kv.shift().replace(/^SCHILY\.(dev|ino|nlink)/, '$1') 138 if (!k) { 139 return set 140 } 141 142 const v = kv.join('=') 143 set[k] = /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) 144 ? new Date(v * 1000) 145 : /^[0-9]+$/.test(v) ? +v 146 : v 147 return set 148} 149 150module.exports = Pax 151