Skip to content

Commit 3e51582

Browse files
committed
Tar: add support for large and negative numbers
In 2001 the GNU tar introduced support for large and negative numbers (https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions) This is required to handle files bigger than 8G.
1 parent 460c205 commit 3e51582

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

src/Tar.php

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -553,8 +553,8 @@ protected function writeRawFileHeader($name, $uid, $gid, $perm, $size, $mtime, $
553553
$uid = sprintf("%6s ", decoct($uid));
554554
$gid = sprintf("%6s ", decoct($gid));
555555
$perm = sprintf("%6s ", decoct($perm));
556-
$size = sprintf("%11s ", decoct($size));
557-
$mtime = sprintf("%11s", decoct($mtime));
556+
$size = self::numberEncode($size, 12);
557+
$mtime = self::numberEncode($size, 12);
558558

559559
$data_first = pack("a100a8a8a8a12A12", $name, $perm, $uid, $gid, $size, $mtime);
560560
$data_last = pack("a1a100a6a2a32a32a8a8a155a12", $typeflag, '', 'ustar', '', '', '', '', '', $prefix, "");
@@ -614,8 +614,8 @@ protected function parseHeader($block)
614614
$return['perm'] = OctDec(trim($header['perm']));
615615
$return['uid'] = OctDec(trim($header['uid']));
616616
$return['gid'] = OctDec(trim($header['gid']));
617-
$return['size'] = OctDec(trim($header['size']));
618-
$return['mtime'] = OctDec(trim($header['mtime']));
617+
$return['size'] = self::numberDecode($header['size']);
618+
$return['mtime'] = self::numberDecode($header['mtime']);
619619
$return['typeflag'] = $header['typeflag'];
620620
$return['link'] = trim($header['link']);
621621
$return['uname'] = trim($header['uname']);
@@ -713,4 +713,63 @@ public function filetype($file)
713713
return Archive::COMPRESS_NONE;
714714
}
715715

716+
/**
717+
* Decodes numeric values according to the
718+
* https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
719+
* (basically with support for big numbers)
720+
*
721+
* @param string $field
722+
* $return int
723+
*/
724+
static public function numberDecode($field)
725+
{
726+
$firstByte = ord(substr($field, 0, 1));
727+
if ($firstByte === 255) {
728+
$value = -1 << (8 * strlen($field));
729+
$shift = 0;
730+
for ($i = strlen($field) - 1; $i >= 0; $i--) {
731+
$value += ord(substr($field, $i, 1)) << $shift;
732+
$shift += 8;
733+
}
734+
} elseif ($firstByte === 128) {
735+
$value = 0;
736+
$shift = 0;
737+
for ($i = strlen($field) - 1; $i > 0; $i--) {
738+
$value += ord(substr($field, $i, 1)) << $shift;
739+
$shift += 8;
740+
}
741+
} else {
742+
$value = octdec(trim($field));
743+
}
744+
return $value;
745+
}
746+
747+
/**
748+
* Encodes numeric values according to the
749+
* https://www.gnu.org/software/tar/manual/html_node/Extensions.html#Extensions
750+
* (basically with support for big numbers)
751+
*
752+
* @param int $value
753+
* @param int $length field length
754+
* @return string
755+
*/
756+
static public function numberEncode($value, $length)
757+
{
758+
// old implementations leave last byte empty
759+
// octal encoding encodes three bits per byte
760+
$maxValue = 1 << (($length - 1) * 3);
761+
if ($value < 0) {
762+
// PHP already stores integers as 2's complement
763+
$value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
764+
$encoded = str_repeat(chr(255), max(1, $length - PHP_INT_SIZE));
765+
$encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
766+
} elseif ($value >= $maxValue) {
767+
$value = pack(PHP_INT_SIZE === 8 ? 'J' : 'N', (int) $value);
768+
$encoded = chr(128) . str_repeat(chr(0), max(0, $length - PHP_INT_SIZE - 1));
769+
$encoded .= substr($value, max(0, PHP_INT_SIZE - $length + 1));
770+
} else {
771+
$encoded = sprintf("%" . ($length - 1) . "s ", decoct($value));
772+
}
773+
return $encoded;
774+
}
716775
}

tests/TarTestCase.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,32 @@ public function testSaveWithInvalidDestinationFile()
778778
$this->assertTrue(true); // succeed if no exception, yet
779779
}
780780

781+
public function testNumberEncodeDecode()
782+
{
783+
// 2^34 + 17 = 2^2 * 2^32 + 17
784+
$refValue = (1 << 34) + 17;
785+
$encoded = Tar::numberEncode($refValue, 12);
786+
$this->assertEquals(pack('CCnNN', 128, 0, 0, 1 << 2, 17), $encoded);
787+
$decoded = Tar::numberDecode($encoded);
788+
$this->assertEquals($refValue, $decoded);
789+
790+
$encoded = Tar::numberEncode($refValue, 7);
791+
$this->assertEquals(pack('CnN', 128, 1 << 2, 17), $encoded);
792+
$decoded = Tar::numberDecode($encoded);
793+
$this->assertEquals($refValue, $decoded);
794+
795+
$refValue = -1234;
796+
$encoded = Tar::numberEncode($refValue, 12);
797+
$this->assertEquals(pack('CCnNN', 0xFF, 0xFF, 0xFFFF, 0xFFFFFFFF, -1234), $encoded);
798+
$decoded = Tar::numberDecode($encoded);
799+
$this->assertEquals($refValue, $decoded);
800+
801+
$encoded = Tar::numberEncode($refValue, 3);
802+
$this->assertEquals(pack('Cn', 0xFF, -1234), $encoded);
803+
$decoded = Tar::numberDecode($encoded);
804+
$this->assertEquals($refValue, $decoded);
805+
}
806+
781807
/**
782808
* recursive rmdir()/unlink()
783809
*

0 commit comments

Comments
 (0)