PHP's pack in JavaScript

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
}
[ View on GitHub | Edit on GitHub | Source on GitHub ]

How to use

You you can install via npm install locutus and require it via require('locutus/php/misc/pack'). You could also require the misc module in full so that you could access misc.pack instead.

If you intend to target the browser, you can then use a module bundler such as Parcel, webpack, Browserify, or rollup.js. This can be important because Locutus allows modern JavaScript in the source files, meaning it may not work in all browsers without a build/transpile step. Locutus does transpile all functions to ES5 before publishing to npm.

A community effort

Not unlike Wikipedia, Locutus is an ongoing community effort. Our philosophy follows The McDonald’s Theory. This means that we don't consider it to be a bad thing that many of our functions are first iterations, which may still have their fair share of issues. We hope that these flaws will inspire others to come up with better ideas.

This way of working also means that we don't offer any production guarantees, and recommend to use Locutus inspiration and learning purposes only.

Notes

  • Float encoding by: Jonas Raoni Soares Silva Home: https://www.kingsquare.nl/blog/12-12-2009/13507444 Feedback: phpjs-pack@kingsquare.nl “machine dependent byte order and size” aren’t applicable for JavaScript; pack works as on a 32bit, little endian machine.

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À"

« More PHP misc functions


Star