Skip to content

Commit

Permalink
Rewrite packer and unpacker of Float/Double: now with support of byte…
Browse files Browse the repository at this point in the history
… order selection
  • Loading branch information
wapmorgan committed Apr 10, 2017
1 parent 54a5b5b commit 3bedd16
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 28 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ With BinaryStream you can handle network packets, binary files, system protocols
## Features
* The library supports all major data types and allows both read and write the data.
* Supports both direct order of bytes (big endian) and reverse (little). You can switch between them while reading a file.
* Supports multiple dimensions of **numbers** (8, 16, 32 and 64).
* Supports multiple dimensions of **integers** (8, 16, 32 and 64) and also rare (24, 40, 48 and 56).
* Supports multiple dimensions of **fractional numbers** (32 and 64).
* You can read both individual bytes and individual bits.
* For ease of navigation through the file, you can specify BinaryStream remember some positions in the file, and later return to them again.
* If in the file stored similar groups of data (eg, titles or frames), you can save the settings group once, and then use only the name of the group to read all the data.
* If you plan to work with different file formats, you can save the entire configuration (which is the byte order and all the groups of fields).
* Unlike standard php functions, **BinaryStream allows you to work with fractional numbers written in both the direct order of bytes** (Big-Endian) **and the reverse one** (Little-Endian).

## Manual
### Simple usage
Expand Down
176 changes: 152 additions & 24 deletions src/BinaryStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,12 @@ class BinaryStream {
'short' => 'v',
'integer' => 'V',
'long' => 'P',
'float' => 'f',
'double' => 'd',
),
'big' => array(
'char' => 'C',
'short' => 'n',
'integer' => 'N',
'long' => 'J',
'float' => 'f',
'double' => 'd',
),
);
protected $labels = array(
Expand All @@ -40,10 +36,6 @@ class BinaryStream {
32 => 'integer',
64 => 'long',
),
'float' => array(
32 => 'float',
64 => 'double',
),
'char' => array(
8 => 'char'
),
Expand Down Expand Up @@ -181,15 +173,19 @@ public function readFloat($sizeInBits = 32) {
}

if ($sizeInBits == 32 || $sizeInBits == 64) {
$bytes = $sizeInBits / 8;
$data = fread($this->fp, $bytes);
if ($data !== false)
$this->offset += $bytes;
else
$this->offset = ftell($this->fp);
$bytesCount = $sizeInBits / 8;
for ($i = 0; $i < $bytesCount; $i++) {
$bytes[$i] = fgetc($this->fp);
if ($bytes[$i] !== false)
$this->offset++;
else
$this->offset = ftell($this->fp);
}

// $value = unpack($this->types[$this->endian][$this->labels['float'][$sizeInBits]], $data);
// return $value[1];

$value = unpack($this->types[$this->endian][$this->labels['float'][$sizeInBits]], $data);
return $value[1];
return $this->unpackFloat($bytes);
}
}

Expand Down Expand Up @@ -318,17 +314,13 @@ public function readGroup($nameOrFieldsList) {

if ($field_size_in_bits == 32 || $field_size_in_bits == 64) {
$bytes = $field_size_in_bits / 8;
$data = null;
$data = array();
for ($i = 0; $i < $bytes; $i++) {
$data .= $cache[$offset];
$data[$i] = $cache[$offset];
$offset++;
}

$unpacked = unpack($this->types[$this->endian][$this->labels['float'][$field_size_in_bits]], $data);
// if ($unpacked[1] >> ($field_size_in_bits - 1) == 1)
// $group[$field_name] = -($unpacked[1] ^ bindec('1'.str_repeat('0', $field_size_in_bits - 1)));
// else
$group[$field_name] = $unpacked[1];
$group[$field_name] = $this->unpackFloat($data);
}
break;

Expand Down Expand Up @@ -528,7 +520,7 @@ public function writeFloat($float, $sizeInBits) {

if ($sizeInBits == 32 || $sizeInBits == 64) {
$bytes = $sizeInBits / 8;
$data = pack($this->types[$this->endian][$this->labels['float'][$sizeInBits]], $float);
$data = implode(null, $this->packFloat($float, $bytes));
if (fwrite($this->fp, $data)) {
$this->offset += $bytes;
} else {
Expand Down Expand Up @@ -556,4 +548,140 @@ public function writeString($string) {
else
$this->offset = ftell($this->fp);
}

/**
* Unpacks float (4 bytes) or double (8 bytes) from bytes. Takes into account current endianness settings.
* @param array $bytes Array of bytes. Should contain 4 or 8 elements.
*/
protected function unpackFloat(array $bytes) {
// own unpacker
$bytesCount = count($bytes);
// deal with endianness
if ($this->endian == self::LITTLE) $bytes = array_reverse($bytes);
// unpack exponent
$sign = (ord($bytes[0]) & 0x80) > 0;

if ($bytesCount == 4) // for 32 bit exponent size is 8 bits
$exponent = pow(2, ((ord($bytes[0]) & 0x7F) << 1) + ((ord($bytes[1]) & 0x80) >> 7) - 127);
else // for 64 bit exponent size is 11 bits
$exponent = pow(2, ((ord($bytes[0]) & 0x7F) << 4) + ((ord($bytes[1]) & 0xF0) >> 4) - 1023);

$fraction = 1.0;
$i = 1;

for ($b = 1; $b < $bytesCount; $b++) {
$byte = ord($bytes[$b]);

for ($j = 0; $j < 8; $j++) {
// skip first N bits of byte used for exponent
if ($b == 1) {
if (($bytesCount == 4 && $j == 0) || ($bytesCount == 8 && $j <= 3))
continue;
}

if ((($byte >> (7 - $j)) & 1) == 1) {
$fraction += pow(2, -$i);
}
$i++;
}
}
return ($sign ? -1 : 1) * $fraction * $exponent;
}

/**
* Packs float (4 bytes) or double (8 bytes) into bytes.
* @param float|double $float Float value
* @param int $sizeInBytes 4 or 8
*/
protected function packFloat($float, $sizeInBytes) {
// unpack exponent
$sign = $float < 0 ? true : false;
$float = abs($float);

if ($sizeInBytes == 4) { // for 32 bit exponent size is 8 bits
$exponentBits = 8;
$exponentBase = 127;
}
else { // for 64 bit exponent size is 11 bits
$exponentBits = 11;
$exponentBase = 1023;
}
$exponentRange = 2 << $exponentBits;

$decimal = floor($float);

if ($float > 1) {
for ($i = 0; $i < $exponentRange; $i++) {
if (pow(2, $i) > $decimal) {
$exponent = $exponentBase + ($i - 1);
break;
}
}
} else {
for ($i = 0; $i < $exponentRange; $i++) {
if (pow(2, -$i) > $decimal) {
$exponent = $exponentBase - $i + 1;
break;
}
}
}

if ($sizeInBytes == 4) {
$bytes = array(
(($sign ? 1 : 0) << 7) + (($exponent & 0xFE) >> 1),
($exponent & 0x01) << 7,
0,
0,
);
} else {
$bytes = array(
(($sign ? 1 : 0) << 7) + (($exponent & 0x7F0) >> 4),
($exponent & 0xF) << 4,
0,
0,
0,
0,
0,
0,
);
}

$fraction = ($float - pow(2, $exponent - $exponentBase)) / pow(2, $exponent - $exponentBase);

$i = 1;
for ($b = 1; $b < $sizeInBytes; $b++) {
for ($j = 0; $j < 8; $j++) {
// skip first N bits of byte used for exponent
if ($b == 1) {
if (($sizeInBytes == 4 && $j == 0) || ($sizeInBytes == 8 && $j <= 3))
continue;
}

if ($fraction > pow(2, -$i)) {
// var_dump($b.'['.$j.']');
$fraction -= pow(2, -$i);
// var_dump($fraction);
$bytes[$b] = (($bytes[$b] >> (7 - $j)) | 0x1) << (7 - $j);
}

$i++;
}
}

// add 1 to fraction. Don't know why, but this works fine
$bytes[$sizeInBytes - 1]++;
for ($b = ($sizeInBytes - 1); $b >= 1; $b--) {
if ($bytes[$b] > 255) {
$bytes[$b] = 0;
$bytes[$b-1]++;
}
}

// deal with endianness
if ($this->endian == self::LITTLE) $bytes = array_reverse($bytes);

// var_dump(implode(null, array_map(function ($val) { return str_pad(decbin($val), 8, '0', STR_PAD_LEFT).PHP_EOL; }, $bytes)));
// var_dump(implode(null, array_map(function ($val) { return dechex($val); }, $bytes)));
return array_map('chr', $bytes);
}
}
6 changes: 6 additions & 0 deletions tests/ReaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ public function testInteger() {

public function testFloat() {
$s = new BinaryStream($this->createStream(pack('fd', 123.789, 654321.789)));
// check machine byte order
if (pack('S', 1) == 0x0001) // BIG ENDIAN
$s->setEndian(BinaryStream::BIG);
else
$s->setEndian(BinaryStream::LITTLE);

$this->assertEquals(123.789, round($s->readFloat(32), 3));
$this->assertEquals(654321.789, round($s->readFloat(64), 3));

Expand Down
11 changes: 8 additions & 3 deletions tests/WriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@ public function testFloat() {
$s = new BinaryStream($file, BinaryStream::CREATE);
$s->setEndian(BinaryStream::BIG);
$s->writeFloat(123.789, 32);
$s->writeFloat(3523.12, 32);
$s->writeFloat(123.789, 64);
$s->writeFloat(654321.789, 64);

rewind($file);
$actual = unpack('fa/db', fread($file, 12));
// rewind($file);
$s->go(0);
$actual = $s->readGroup(array('f:a' => 32, 'f:b' => 32, 'f:c' => 64, 'f:d' => 64));
$this->assertEquals(123.789, round($actual['a'], 3));
$this->assertEquals(654321.789, $actual['b']);
$this->assertEquals(3523.12, round($actual['b'], 3));
$this->assertEquals(123.789, round($actual['c'], 3));
$this->assertEquals(654321.789, round($actual['d'], 3));
}

public function testChar() {
Expand Down

0 comments on commit 3bedd16

Please sign in to comment.