PHP's pack in JavaScript

How to use

You you can install via yarn add locutus and require this function via const pack = require('locutus/php/misc/pack').

It is important to use a bundler that supports tree-shaking so that you only ship the functions that you actually use to your browser, instead of all of Locutus, which is massive. Examples are: Parcel, webpack, or rollup.js. For server-side use this is typically less of a concern.

Examples

Please note that these examples are distilled from test cases that automatically verify our functions still work correctly. This could explain some quirky ones.

#codeexpected result
1pack('nvc*', 0x1234, 0x5678, 65, 66)'\u00124xVAB'
2pack('H4', '2345')'#E'
3pack('H*', 'D5')'Õ'
4pack('d', -100.876)"\u0000\u0000\u0000\u0000\u00008YÀ"

Notes

Here’s what our current JavaScript equivalent to PHP's pack looks like.

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
}

A community effort

Not unlike Wikipedia, Locutus is an ongoing community effort. Our philosophy follows The McDonald’s Theory. This means that we assimilate first iterations with imperfections, hoping for others to take issue with-and improve them. This unorthodox approach has worked very well to foster fun and fruitful collaboration, but please be reminded to use our creations at your own risk. THE SOFTWARE IS PROVIDED "AS IS" has never been more true than for Locutus.

Now go and: [ View on GitHub | Edit on GitHub | View Raw ]


« More PHP misc functions


Star