1'use strict' 2const Buffer = require('./buffer.js') 3const Header = require('./header.js') 4const path = require('path') 5 6class Pax { 7 constructor (obj, global) { 8 this.atime = obj.atime || null 9 this.charset = obj.charset || null 10 this.comment = obj.comment || null 11 this.ctime = obj.ctime || null 12 this.gid = obj.gid || null 13 this.gname = obj.gname || null 14 this.linkpath = obj.linkpath || null 15 this.mtime = obj.mtime || null 16 this.path = obj.path || null 17 this.size = obj.size || null 18 this.uid = obj.uid || null 19 this.uname = obj.uname || null 20 this.dev = obj.dev || null 21 this.ino = obj.ino || null 22 this.nlink = obj.nlink || null 23 this.global = global || false 24 } 25 26 encode () { 27 const body = this.encodeBody() 28 if (body === '') 29 return null 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 const v = this[field] instanceof Date ? this[field].getTime() / 1000 96 : this[field] 97 const s = ' ' + 98 (field === 'dev' || field === 'ino' || field === 'nlink' 99 ? 'SCHILY.' : '') + 100 field + '=' + v + '\n' 101 const byteLen = Buffer.byteLength(s) 102 // the digits includes the length of the digits in ascii base-10 103 // so if it's 9 characters, then adding 1 for the 9 makes it 10 104 // which makes it 11 chars. 105 let digits = Math.floor(Math.log(byteLen) / Math.log(10)) + 1 106 if (byteLen + digits >= Math.pow(10, digits)) 107 digits += 1 108 const len = digits + byteLen 109 return len + s 110 } 111} 112 113Pax.parse = (string, ex, g) => new Pax(merge(parseKV(string), ex), g) 114 115const merge = (a, b) => 116 b ? Object.keys(a).reduce((s, k) => (s[k] = a[k], s), b) : a 117 118const parseKV = string => 119 string 120 .replace(/\n$/, '') 121 .split('\n') 122 .reduce(parseKVLine, Object.create(null)) 123 124const parseKVLine = (set, line) => { 125 const n = parseInt(line, 10) 126 127 // XXX Values with \n in them will fail this. 128 // Refactor to not be a naive line-by-line parse. 129 if (n !== Buffer.byteLength(line) + 1) 130 return set 131 132 line = line.substr((n + ' ').length) 133 const kv = line.split('=') 134 const k = kv.shift().replace(/^SCHILY\.(dev|ino|nlink)/, '$1') 135 if (!k) 136 return set 137 138 const v = kv.join('=') 139 set[k] = /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) 140 ? new Date(v * 1000) 141 : /^[0-9]+$/.test(v) ? +v 142 : v 143 return set 144} 145 146module.exports = Pax 147