module.exports = function pack(format) { // discuss at: https://locutus.io/php/pack/ // original by: Tim de Koning (https://www.kingsquare.nl) // parts by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) // bugfixed by: Tim de Koning (https://www.kingsquare.nl) // note 1: Float encoding by: Jonas Raoni Soares Silva // note 1: Home: https://www.kingsquare.nl/blog/12-12-2009/13507444 // note 1: Feedback: phpjs-pack@kingsquare.nl // note 1: "machine dependent byte order and size" aren't // note 1: applicable for JavaScript; pack works as on a 32bit, // note 1: little endian machine. // example 1: pack('nvc*', 0x1234, 0x5678, 65, 66) // returns 1: '\u00124xVAB' // example 2: pack('H4', '2345') // returns 2: '#E' // example 3: pack('H*', 'D5') // returns 3: 'Õ' // example 4: pack('d', -100.876) // returns 4: "\u0000\u0000\u0000\u0000\u00008YÀ" // test: skip-1 let formatPointer = 0 let argumentPointer = 1 let result = '' let argument = '' let i = 0 let r = [] let instruction, quantifier, word, precisionBits, exponentBits, extraNullCount // vars used by float encoding let bias let minExp let maxExp let minUnnormExp let status let exp let len let bin let signal let n let intPart let floatPart let lastBit let rounded let j let k let tmpResult while (formatPointer < format.length) { instruction = format.charAt(formatPointer) quantifier = '' formatPointer++ while (formatPointer < format.length && format.charAt(formatPointer).match(/[\d*]/) !== null) { quantifier += format.charAt(formatPointer) formatPointer++ } if (quantifier === '') { quantifier = '1' } // Now pack variables: 'quantifier' times 'instruction' switch (instruction) { case 'a': case 'A': // NUL-padded string // SPACE-padded string if (typeof arguments[argumentPointer] === 'undefined') { throw new Error('Warning: pack() Type ' + instruction + ': not enough arguments') } else { argument = String(arguments[argumentPointer]) } if (quantifier === '*') { quantifier = argument.length } for (i = 0; i < quantifier; i++) { if (typeof argument[i] === 'undefined') { if (instruction === 'a') { result += String.fromCharCode(0) } else { result += ' ' } } else { result += argument[i] } } argumentPointer++ break case 'h': case 'H': // Hex string, low nibble first // Hex string, high nibble first if (typeof arguments[argumentPointer] === 'undefined') { throw new Error('Warning: pack() Type ' + instruction + ': not enough arguments') } else { argument = arguments[argumentPointer] } if (quantifier === '*') { quantifier = argument.length } if (quantifier > argument.length) { const msg = 'Warning: pack() Type ' + instruction + ': not enough characters in string' throw new Error(msg) } for (i = 0; i < quantifier; i += 2) { // Always get per 2 bytes... word = argument[i] if (i + 1 >= quantifier || typeof argument[i + 1] === 'undefined') { word += '0' } else { word += argument[i + 1] } // The fastest way to reverse? if (instruction === 'h') { word = word[1] + word[0] } result += String.fromCharCode(parseInt(word, 16)) } argumentPointer++ break case 'c': case 'C': // signed char // unsigned char // c and C is the same in pack if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { result += String.fromCharCode(arguments[argumentPointer]) argumentPointer++ } break case 's': case 'S': case 'v': // signed short (always 16 bit, machine byte order) // unsigned short (always 16 bit, machine byte order) // s and S is the same in pack if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { result += String.fromCharCode(arguments[argumentPointer] & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 8) & 0xff) argumentPointer++ } break case 'n': // unsigned short (always 16 bit, big endian byte order) if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { result += String.fromCharCode((arguments[argumentPointer] >> 8) & 0xff) result += String.fromCharCode(arguments[argumentPointer] & 0xff) argumentPointer++ } break case 'i': case 'I': case 'l': case 'L': case 'V': // signed integer (machine dependent size and byte order) // unsigned integer (machine dependent size and byte order) // signed long (always 32 bit, machine byte order) // unsigned long (always 32 bit, machine byte order) // unsigned long (always 32 bit, little endian byte order) if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { result += String.fromCharCode(arguments[argumentPointer] & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 8) & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 16) & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 24) & 0xff) argumentPointer++ } break case 'N': // unsigned long (always 32 bit, big endian byte order) if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { result += String.fromCharCode((arguments[argumentPointer] >> 24) & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 16) & 0xff) result += String.fromCharCode((arguments[argumentPointer] >> 8) & 0xff) result += String.fromCharCode(arguments[argumentPointer] & 0xff) argumentPointer++ } break case 'f': case 'd': // float (machine dependent size and representation) // double (machine dependent size and representation) // version based on IEEE754 precisionBits = 23 exponentBits = 8 if (instruction === 'd') { precisionBits = 52 exponentBits = 11 } if (quantifier === '*') { quantifier = arguments.length - argumentPointer } if (quantifier > arguments.length - argumentPointer) { throw new Error('Warning: pack() Type ' + instruction + ': too few arguments') } for (i = 0; i < quantifier; i++) { argument = arguments[argumentPointer] bias = Math.pow(2, exponentBits - 1) - 1 minExp = -bias + 1 maxExp = bias minUnnormExp = minExp - precisionBits status = isNaN((n = parseFloat(argument))) || n === -Infinity || n === +Infinity ? n : 0 exp = 0 len = 2 * bias + 1 + precisionBits + 3 bin = new Array(len) signal = (n = status !== 0 ? 0 : n) < 0 n = Math.abs(n) intPart = Math.floor(n) floatPart = n - intPart for (k = len; k; ) { bin[--k] = 0 } for (k = bias + 2; intPart && k; ) { bin[--k] = intPart % 2 intPart = Math.floor(intPart / 2) } for (k = bias + 1; floatPart > 0 && k; --floatPart) { bin[++k] = ((floatPart *= 2) >= 1) - 0 } for (k = -1; ++k < len && !bin[k]; ) {} // @todo: Make this more readable: const key = (lastBit = precisionBits - 1 + (k = (exp = bias + 1 - k) >= minExp && exp <= maxExp ? k + 1 : bias + 1 - (exp = minExp - 1))) + 1 if (bin[key]) { if (!(rounded = bin[lastBit])) { for (j = lastBit + 2; !rounded && j < len; rounded = bin[j++]) {} } for (j = lastBit + 1; rounded && --j >= 0; (bin[j] = !bin[j] - 0) && (rounded = 0)) {} } for (k = k - 2 < 0 ? -1 : k - 3; ++k < len && !bin[k]; ) {} if ((exp = bias + 1 - k) >= minExp && exp <= maxExp) { ++k } else { if (exp < minExp) { if (exp !== bias + 1 - len && exp < minUnnormExp) { // "encodeFloat::float underflow" } k = bias + 1 - (exp = minExp - 1) } } if (intPart || status !== 0) { exp = maxExp + 1 k = bias + 2 if (status === -Infinity) { signal = 1 } else if (isNaN(status)) { bin[k] = 1 } } n = Math.abs(exp + bias) tmpResult = '' for (j = exponentBits + 1; --j; ) { tmpResult = (n % 2) + tmpResult n = n >>= 1 } n = 0 j = 0 k = (tmpResult = (signal ? '1' : '0') + tmpResult + bin.slice(k, k + precisionBits).join('')).length r = [] for (; k; ) { n += (1 << j) * tmpResult.charAt(--k) if (j === 7) { r[r.length] = String.fromCharCode(n) n = 0 } j = (j + 1) % 8 } r[r.length] = n ? String.fromCharCode(n) : '' result += r.join('') argumentPointer++ } break case 'x': // NUL byte if (quantifier === '*') { throw new Error("Warning: pack(): Type x: '*' ignored") } for (i = 0; i < quantifier; i++) { result += String.fromCharCode(0) } break case 'X': // Back up one byte if (quantifier === '*') { throw new Error("Warning: pack(): Type X: '*' ignored") } for (i = 0; i < quantifier; i++) { if (result.length === 0) { throw new Error('Warning: pack(): Type X:' + ' outside of string') } else { result = result.substring(0, result.length - 1) } } break case '@': // NUL-fill to absolute position if (quantifier === '*') { throw new Error("Warning: pack(): Type X: '*' ignored") } if (quantifier > result.length) { extraNullCount = quantifier - result.length for (i = 0; i < extraNullCount; i++) { result += String.fromCharCode(0) } } if (quantifier < result.length) { result = result.substring(0, quantifier) } break default: throw new Error('Warning: pack() Type ' + instruction + ': unknown format code') } } if (argumentPointer < arguments.length) { const msg2 = 'Warning: pack(): ' + (arguments.length - argumentPointer) + ' arguments unused' throw new Error(msg2) } return result }