| | class GitPacks { |
| | private const MAX_READ = 16777216; |
| | - private const MAX_OBJECT_SIZE = 10485760; |
| | - |
| | - private string $objectsPath; |
| | - private array $packFiles; |
| | - private ?string $lastPack = null; |
| | - |
| | - private array $fileHandles = []; |
| | - private array $fanoutCache = []; |
| | - private array $shaBucketCache = []; |
| | - private array $offsetBucketCache = []; |
| | - |
| | - public function __construct( string $objectsPath ) { |
| | - $this->objectsPath = $objectsPath; |
| | - $this->packFiles = glob( "{$this->objectsPath}/pack/*.idx" ) ?: []; |
| | - } |
| | - |
| | - public function __destruct() { |
| | - foreach( $this->fileHandles as $handle ) { |
| | - if( is_resource( $handle ) ) { |
| | - fclose( $handle ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - public function read( string $sha ): ?string { |
| | - $info = $this->findPackInfo( $sha ); |
| | - |
| | - if( $info['offset'] === -1 ) { |
| | - return null; |
| | - } |
| | - |
| | - $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | - |
| | - if( $size > self::MAX_OBJECT_SIZE ) { |
| | - error_log( "GitPacks: Object $sha too large ({$size} bytes, limit " . |
| | - self::MAX_OBJECT_SIZE . |
| | - "), skipping to prevent memory exhaustion" |
| | - ); |
| | - |
| | - return null; |
| | - } |
| | - |
| | - $handle = $this->getHandle( $info['file'] ); |
| | - |
| | - return $handle |
| | - ? $this->readPackEntry( $handle, $info['offset'], $size ) |
| | - : null; |
| | - } |
| | - |
| | - public function getSize( string $sha ): ?int { |
| | - $info = $this->findPackInfo( $sha ); |
| | - |
| | - if( $info['offset'] === -1 ) { |
| | - return null; |
| | - } |
| | - |
| | - return $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | - } |
| | - |
| | - private function findPackInfo( string $sha ): array { |
| | - if( !ctype_xdigit( $sha ) || strlen( $sha ) !== 40 ) { |
| | - return ['offset' => -1]; |
| | - } |
| | - |
| | - $binarySha = hex2bin( $sha ); |
| | - |
| | - if( $this->lastPack ) { |
| | - $offset = $this->findInIdx( $this->lastPack, $binarySha ); |
| | - |
| | - if( $offset !== -1 ) { |
| | - return $this->makeResult( $this->lastPack, $offset ); |
| | - } |
| | - } |
| | - |
| | - foreach( $this->packFiles as $indexFile ) { |
| | - if( $indexFile === $this->lastPack ) { |
| | - continue; |
| | - } |
| | - |
| | - $offset = $this->findInIdx( $indexFile, $binarySha ); |
| | - |
| | - if( $offset !== -1 ) { |
| | - $this->lastPack = $indexFile; |
| | - |
| | - return $this->makeResult( $indexFile, $offset ); |
| | - } |
| | - } |
| | - |
| | - return ['offset' => -1]; |
| | - } |
| | - |
| | - private function makeResult( string $indexPath, int $offset ): array { |
| | - return [ |
| | - 'file' => str_replace( '.idx', '.pack', $indexPath ), |
| | - 'offset' => $offset |
| | - ]; |
| | - } |
| | - |
| | - private function findInIdx( string $indexFile, string $binarySha ): int { |
| | - $fileHandle = $this->getHandle( $indexFile ); |
| | - |
| | - if( !$fileHandle ) { |
| | - return -1; |
| | - } |
| | - |
| | - if( !isset( $this->fanoutCache[$indexFile] ) ) { |
| | - fseek( $fileHandle, 0 ); |
| | - |
| | - if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) { |
| | - $this->fanoutCache[$indexFile] = array_values( |
| | - unpack( 'N*', fread( $fileHandle, 1024 ) ) |
| | - ); |
| | - } else { |
| | - return -1; |
| | - } |
| | - } |
| | - |
| | - $fanout = $this->fanoutCache[$indexFile]; |
| | - |
| | - $firstByte = ord( $binarySha[0] ); |
| | - $start = $firstByte === 0 ? 0 : $fanout[$firstByte - 1]; |
| | - $end = $fanout[$firstByte]; |
| | - |
| | - if( $end <= $start ) { |
| | - return -1; |
| | - } |
| | - |
| | - $cacheKey = "$indexFile:$firstByte"; |
| | - |
| | - if( !isset( $this->shaBucketCache[$cacheKey] ) ) { |
| | - $count = $end - $start; |
| | - fseek( $fileHandle, 1032 + ($start * 20) ); |
| | - $this->shaBucketCache[$cacheKey] = fread( $fileHandle, $count * 20 ); |
| | - |
| | - fseek( |
| | - $fileHandle, |
| | - 1032 + ($fanout[255] * 24) + ($start * 4) |
| | - ); |
| | - $this->offsetBucketCache[$cacheKey] = fread( $fileHandle, $count * 4 ); |
| | - } |
| | - |
| | - $shaBlock = $this->shaBucketCache[$cacheKey]; |
| | - $count = strlen( $shaBlock ) / 20; |
| | - $low = 0; |
| | - $high = $count - 1; |
| | - $foundIdx = -1; |
| | - |
| | - while( $low <= $high ) { |
| | - $mid = ($low + $high) >> 1; |
| | - $compare = substr( $shaBlock, $mid * 20, 20 ); |
| | - |
| | - if( $compare < $binarySha ) { |
| | - $low = $mid + 1; |
| | - } elseif( $compare > $binarySha ) { |
| | - $high = $mid - 1; |
| | - } else { |
| | - $foundIdx = $mid; |
| | - break; |
| | - } |
| | - } |
| | - |
| | - if( $foundIdx === -1 ) { |
| | - return -1; |
| | - } |
| | - |
| | - $offsetData = substr( |
| | - $this->offsetBucketCache[$cacheKey], |
| | - $foundIdx * 4, |
| | - 4 |
| | - ); |
| | - $offset = unpack( 'N', $offsetData )[1]; |
| | - |
| | - if( $offset & 0x80000000 ) { |
| | - $packTotal = $fanout[255]; |
| | - $pos64 = 1032 + ($packTotal * 28) + |
| | - (($offset & 0x7FFFFFFF) * 8); |
| | - fseek( $fileHandle, $pos64 ); |
| | - $offset = unpack( 'J', fread( $fileHandle, 8 ) )[1]; |
| | - } |
| | - |
| | - return (int)$offset; |
| | - } |
| | - |
| | - private function readPackEntry( $fileHandle, int $offset, int $expectedSize ): string { |
| | - fseek( $fileHandle, $offset ); |
| | - |
| | - $header = $this->readVarInt( $fileHandle ); |
| | - $type = ($header['byte'] >> 4) & 7; |
| | - |
| | - if( $type === 6 ) { |
| | - return $this->handleOfsDelta( $fileHandle, $offset, $expectedSize ); |
| | - } |
| | - |
| | - if( $type === 7 ) { |
| | - return $this->handleRefDelta( $fileHandle, $expectedSize ); |
| | - } |
| | - |
| | - return $this->decompressWithLimit( $fileHandle, $expectedSize ); |
| | - } |
| | - |
| | - private function decompressWithLimit( $fileHandle, int $maxSize ): string { |
| | - $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - if( $inflator === false ) { |
| | - return ''; |
| | - } |
| | - |
| | - $result = ''; |
| | - $totalRead = 0; |
| | - |
| | - while( !feof( $fileHandle ) && $totalRead < $maxSize ) { |
| | - $chunk = fread( $fileHandle, min( 8192, $maxSize - $totalRead ) ); |
| | - |
| | - if( $chunk === false || $chunk === '' ) { |
| | - break; |
| | - } |
| | - |
| | - $data = @inflate_add( $inflator, $chunk ); |
| | - |
| | - if( $data !== false ) { |
| | - $result .= $data; |
| | - $totalRead = strlen( $result ); |
| | - |
| | - if( $totalRead > $maxSize ) { |
| | - error_log( "GitPacks: Decompressed size exceeded limit, truncating" ); |
| | - break; |
| | - } |
| | - } |
| | - |
| | - if( $data === false || |
| | - inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { |
| | - break; |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function extractPackedSize( string $packPath, int $offset ): int { |
| | - $fileHandle = $this->getHandle( $packPath ); |
| | - |
| | - if( !$fileHandle ) { |
| | - return 0; |
| | - } |
| | - |
| | - fseek( $fileHandle, $offset ); |
| | - |
| | - $header = $this->readVarInt( $fileHandle ); |
| | - $size = $header['value']; |
| | - $type = ($header['byte'] >> 4) & 7; |
| | - |
| | - if( $type === 6 || $type === 7 ) { |
| | - return $this->readDeltaTargetSize( $fileHandle, $type ); |
| | - } |
| | - |
| | - return $size; |
| | - } |
| | - |
| | - private function handleOfsDelta( $fileHandle, int $offset, int $expectedSize ): string { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - $negative = $byte & 127; |
| | - |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - $negative = (($negative + 1) << 7) | ($byte & 127); |
| | - } |
| | - |
| | - $currentPos = ftell( $fileHandle ); |
| | - |
| | - $baseOffset = $offset - $negative; |
| | - $baseHandle = $fileHandle; |
| | - fseek( $baseHandle, $baseOffset ); |
| | - $baseHeader = $this->readVarInt( $baseHandle ); |
| | - $baseSize = $baseHeader['value']; |
| | - |
| | - if( $baseSize > self::MAX_OBJECT_SIZE ) { |
| | - error_log( "GitPacks: Base object too large ($baseSize bytes), cannot apply delta" ); |
| | - return ''; |
| | - } |
| | - |
| | - fseek( $fileHandle, $baseOffset ); |
| | - $base = $this->readPackEntry( $fileHandle, $baseOffset, $baseSize ); |
| | - |
| | - fseek( $fileHandle, $currentPos ); |
| | - |
| | - $remainingBytes = min( self::MAX_READ, $expectedSize * 2 ); |
| | - $compressed = fread( $fileHandle, $remainingBytes ); |
| | - $delta = @gzuncompress( $compressed ) ?: ''; |
| | - |
| | - return $this->applyDelta( $base, $delta ); |
| | - } |
| | - |
| | - private function handleRefDelta( $fileHandle, int $expectedSize ): string { |
| | - $baseSha = bin2hex( fread( $fileHandle, 20 ) ); |
| | - $baseSize = $this->getSize( $baseSha ); |
| | - |
| | - if( $baseSize === null || $baseSize > self::MAX_OBJECT_SIZE ) { |
| | - error_log( "GitPacks: Referenced base object $baseSha too large or not found" ); |
| | - return ''; |
| | - } |
| | - |
| | - $base = $this->read( $baseSha ) ?? ''; |
| | - |
| | - $remainingBytes = min( self::MAX_READ, $expectedSize * 2 ); |
| | - $compressed = fread( $fileHandle, $remainingBytes ); |
| | - $delta = @gzuncompress( $compressed ) ?: ''; |
| | - |
| | - return $this->applyDelta( $base, $delta ); |
| | - } |
| | - |
| | - private function applyDelta( string $base, string $delta ): string { |
| | - $position = 0; |
| | - $this->skipSize( $delta, $position ); |
| | - $this->skipSize( $delta, $position ); |
| | - |
| | - $output = ''; |
| | - $deltaLength = strlen( $delta ); |
| | - |
| | - while( $position < $deltaLength ) { |
| | - $opcode = ord( $delta[$position++] ); |
| | - |
| | - if( $opcode & 128 ) { |
| | - $offset = 0; |
| | - $length = 0; |
| | - |
| | - if( $opcode & 0x01 ) { |
| | - $offset |= ord( $delta[$position++] ); |
| | - } |
| | - if( $opcode & 0x02 ) { |
| | - $offset |= ord( $delta[$position++] ) << 8; |
| | - } |
| | - if( $opcode & 0x04 ) { |
| | - $offset |= ord( $delta[$position++] ) << 16; |
| | - } |
| | - if( $opcode & 0x08 ) { |
| | - $offset |= ord( $delta[$position++] ) << 24; |
| | - } |
| | - |
| | - if( $opcode & 0x10 ) { |
| | - $length |= ord( $delta[$position++] ); |
| | - } |
| | - if( $opcode & 0x20 ) { |
| | - $length |= ord( $delta[$position++] ) << 8; |
| | - } |
| | - if( $opcode & 0x40 ) { |
| | - $length |= ord( $delta[$position++] ) << 16; |
| | - } |
| | - |
| | - if( $length === 0 ) { |
| | - $length = 0x10000; |
| | - } |
| | - |
| | - $output .= substr( $base, $offset, $length ); |
| | - } else { |
| | - $length = $opcode & 127; |
| | - $output .= substr( $delta, $position, $length ); |
| | - $position += $length; |
| | - } |
| | - } |
| | - |
| | - return $output; |
| | - } |
| | - |
| | - private function readVarInt( $fileHandle ): array { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - $value = $byte & 15; |
| | - $shift = 4; |
| | - $first = $byte; |
| | - |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - $value |= (($byte & 127) << $shift); |
| | - $shift += 7; |
| | - } |
| | - |
| | - return ['value' => $value, 'byte' => $first]; |
| | - } |
| | - |
| | - private function readDeltaTargetSize( $fileHandle, int $type ): int { |
| | - if( $type === 6 ) { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $fileHandle, 1 ) ); |
| | - } |
| | - } else { |
| | - fseek( $fileHandle, 20, SEEK_CUR ); |
| | - } |
| | - |
| | - $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - if( $inflator === false ) { |
| | - return 0; |
| | - } |
| | - |
| | - $header = ''; |
| | - $attempts = 0; |
| | - $maxAttempts = 64; |
| | - |
| | - while( !feof( $fileHandle ) && strlen( $header ) < 32 && $attempts < $maxAttempts ) { |
| | - $chunk = fread( $fileHandle, 512 ); |
| | - |
| | - if( $chunk === false || $chunk === '' ) { |
| | - break; |
| | - } |
| | - |
| | - $output = @inflate_add( $inflator, $chunk, ZLIB_NO_FLUSH ); |
| | - |
| | - if( $output !== false ) { |
| | - $header .= $output; |
| | - } |
| | - |
| | - if( inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { |
| | - break; |
| | - } |
| | - |
| | - $attempts++; |
| | - } |
| | - |
| | - $position = 0; |
| | - |
| | - if( strlen( $header ) > 0 ) { |
| | - $this->skipSize( $header, $position ); |
| | - |
| | - return $this->readSize( $header, $position ); |
| | - } |
| | - |
| | - return 0; |
| | - } |
| | - |
| | - private function skipSize( string $data, int &$position ): void { |
| | - $length = strlen( $data ); |
| | - |
| | - while( $position < $length && (ord( $data[$position++] ) & 128) ) { |
| | - // Loop continues while MSB is 1 |
| | - } |
| | - } |
| | - |
| | - private function readSize( string $data, int &$position ): int { |
| | - $byte = ord( $data[$position++] ); |
| | - $value = $byte & 127; |
| | - $shift = 7; |
| | - |
| | - while( $byte & 128 ) { |
| | - $byte = ord( $data[$position++] ); |
| | - $value |= (($byte & 127) << $shift); |
| | - $shift += 7; |
| | - } |
| | - |
| | - return $value; |
| | - } |
| | - |
| | - private function getHandle( string $path ) { |
| | - if( !isset( $this->fileHandles[$path] ) ) { |
| | - $this->fileHandles[$path] = @fopen( $path, 'rb' ); |
| | - } |
| | - |
| | - return $this->fileHandles[$path]; |
| | - } |
| | -} |
| | - |
| | + private const MAX_RAM = 1048576; |
| | + |
| | + private string $objectsPath; |
| | + private array $packFiles; |
| | + private ?string $lastPack = null; |
| | + |
| | + private array $fileHandles = []; |
| | + private array $fanoutCache = []; |
| | + private array $shaBucketCache = []; |
| | + private array $offsetBucketCache = []; |
| | + |
| | + public function __construct( string $objectsPath ) { |
| | + $this->objectsPath = $objectsPath; |
| | + $this->packFiles = glob( "{$this->objectsPath}/pack/*.idx" ) ?: []; |
| | + } |
| | + |
| | + public function __destruct() { |
| | + foreach( $this->fileHandles as $handle ) { |
| | + if( is_resource( $handle ) ) { |
| | + fclose( $handle ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + public function read( string $sha ): ?string { |
| | + $info = $this->findPackInfo( $sha ); |
| | + |
| | + if( $info['offset'] === -1 ) { |
| | + return null; |
| | + } |
| | + |
| | + $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + |
| | + if( $size > self::MAX_RAM ) { |
| | + return null; |
| | + } |
| | + |
| | + $handle = $this->getHandle( $info['file'] ); |
| | + |
| | + return $handle |
| | + ? $this->readPackEntry( $handle, $info['offset'], $size ) |
| | + : null; |
| | + } |
| | + |
| | + public function stream( string $sha, callable $callback ): bool { |
| | + $info = $this->findPackInfo( $sha ); |
| | + |
| | + if( $info['offset'] === -1 ) { |
| | + return false; |
| | + } |
| | + |
| | + $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + $handle = $this->getHandle( $info['file'] ); |
| | + |
| | + if( !$handle ) { |
| | + return false; |
| | + } |
| | + |
| | + return $this->streamPackEntry( $handle, $info['offset'], $size, $callback ); |
| | + } |
| | + |
| | + public function getSize( string $sha ): ?int { |
| | + $info = $this->findPackInfo( $sha ); |
| | + |
| | + if( $info['offset'] === -1 ) { |
| | + return null; |
| | + } |
| | + |
| | + return $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + } |
| | + |
| | + private function findPackInfo( string $sha ): array { |
| | + if( !ctype_xdigit( $sha ) || strlen( $sha ) !== 40 ) { |
| | + return ['offset' => -1]; |
| | + } |
| | + |
| | + $binarySha = hex2bin( $sha ); |
| | + |
| | + if( $this->lastPack ) { |
| | + $offset = $this->findInIdx( $this->lastPack, $binarySha ); |
| | + |
| | + if( $offset !== -1 ) { |
| | + return $this->makeResult( $this->lastPack, $offset ); |
| | + } |
| | + } |
| | + |
| | + foreach( $this->packFiles as $indexFile ) { |
| | + if( $indexFile === $this->lastPack ) { |
| | + continue; |
| | + } |
| | + |
| | + $offset = $this->findInIdx( $indexFile, $binarySha ); |
| | + |
| | + if( $offset !== -1 ) { |
| | + $this->lastPack = $indexFile; |
| | + |
| | + return $this->makeResult( $indexFile, $offset ); |
| | + } |
| | + } |
| | + |
| | + return ['offset' => -1]; |
| | + } |
| | + |
| | + private function makeResult( string $indexPath, int $offset ): array { |
| | + return [ |
| | + 'file' => str_replace( '.idx', '.pack', $indexPath ), |
| | + 'offset' => $offset |
| | + ]; |
| | + } |
| | + |
| | + private function findInIdx( string $indexFile, string $binarySha ): int { |
| | + $fileHandle = $this->getHandle( $indexFile ); |
| | + |
| | + if( !$fileHandle ) { |
| | + return -1; |
| | + } |
| | + |
| | + if( !isset( $this->fanoutCache[$indexFile] ) ) { |
| | + fseek( $fileHandle, 0 ); |
| | + |
| | + if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) { |
| | + $this->fanoutCache[$indexFile] = array_values( |
| | + unpack( 'N*', fread( $fileHandle, 1024 ) ) |
| | + ); |
| | + } else { |
| | + return -1; |
| | + } |
| | + } |
| | + |
| | + $fanout = $this->fanoutCache[$indexFile]; |
| | + |
| | + $firstByte = ord( $binarySha[0] ); |
| | + $start = $firstByte === 0 ? 0 : $fanout[$firstByte - 1]; |
| | + $end = $fanout[$firstByte]; |
| | + |
| | + if( $end <= $start ) { |
| | + return -1; |
| | + } |
| | + |
| | + $cacheKey = "$indexFile:$firstByte"; |
| | + |
| | + if( !isset( $this->shaBucketCache[$cacheKey] ) ) { |
| | + $count = $end - $start; |
| | + fseek( $fileHandle, 1032 + ($start * 20) ); |
| | + $this->shaBucketCache[$cacheKey] = fread( $fileHandle, $count * 20 ); |
| | + |
| | + fseek( |
| | + $fileHandle, |
| | + 1032 + ($fanout[255] * 24) + ($start * 4) |
| | + ); |
| | + $this->offsetBucketCache[$cacheKey] = fread( $fileHandle, $count * 4 ); |
| | + } |
| | + |
| | + $shaBlock = $this->shaBucketCache[$cacheKey]; |
| | + $count = strlen( $shaBlock ) / 20; |
| | + $low = 0; |
| | + $high = $count - 1; |
| | + $foundIdx = -1; |
| | + |
| | + while( $low <= $high ) { |
| | + $mid = ($low + $high) >> 1; |
| | + $compare = substr( $shaBlock, $mid * 20, 20 ); |
| | + |
| | + if( $compare < $binarySha ) { |
| | + $low = $mid + 1; |
| | + } elseif( $compare > $binarySha ) { |
| | + $high = $mid - 1; |
| | + } else { |
| | + $foundIdx = $mid; |
| | + break; |
| | + } |
| | + } |
| | + |
| | + if( $foundIdx === -1 ) { |
| | + return -1; |
| | + } |
| | + |
| | + $offsetData = substr( |
| | + $this->offsetBucketCache[$cacheKey], |
| | + $foundIdx * 4, |
| | + 4 |
| | + ); |
| | + $offset = unpack( 'N', $offsetData )[1]; |
| | + |
| | + if( $offset & 0x80000000 ) { |
| | + $packTotal = $fanout[255]; |
| | + $pos64 = 1032 + ($packTotal * 28) + |
| | + (($offset & 0x7FFFFFFF) * 8); |
| | + fseek( $fileHandle, $pos64 ); |
| | + $offset = unpack( 'J', fread( $fileHandle, 8 ) )[1]; |
| | + } |
| | + |
| | + return (int)$offset; |
| | + } |
| | + |
| | + private function readPackEntry( $fileHandle, int $offset, int $expectedSize ): string { |
| | + fseek( $fileHandle, $offset ); |
| | + |
| | + $header = $this->readVarInt( $fileHandle ); |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + if( $type === 6 ) { |
| | + return $this->handleOfsDelta( $fileHandle, $offset, $expectedSize ); |
| | + } |
| | + |
| | + if( $type === 7 ) { |
| | + return $this->handleRefDelta( $fileHandle, $expectedSize ); |
| | + } |
| | + |
| | + return $this->decompressToString( $fileHandle, $expectedSize ); |
| | + } |
| | + |
| | + private function streamPackEntry( $fileHandle, int $offset, int $expectedSize, callable $callback ): bool { |
| | + fseek( $fileHandle, $offset ); |
| | + |
| | + $header = $this->readVarInt( $fileHandle ); |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + if( $type === 6 || $type === 7 ) { |
| | + return $this->streamDeltaObject( $fileHandle, $offset, $type, $expectedSize, $callback ); |
| | + } |
| | + |
| | + return $this->streamDecompression( $fileHandle, $callback ); |
| | + } |
| | + |
| | + private function streamDeltaObject( $fileHandle, int $offset, int $type, int $expectedSize, callable $callback ): bool { |
| | + fseek( $fileHandle, $offset ); |
| | + $this->readVarInt( $fileHandle ); |
| | + |
| | + if( $type === 6 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $negative = $byte & 127; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $negative = (($negative + 1) << 7) | ($byte & 127); |
| | + } |
| | + |
| | + $deltaPos = ftell( $fileHandle ); |
| | + $baseOffset = $offset - $negative; |
| | + |
| | + $base = ''; |
| | + $this->streamPackEntry( $fileHandle, $baseOffset, 0, function( $chunk ) use ( &$base ) { |
| | + $base .= $chunk; |
| | + } ); |
| | + |
| | + fseek( $fileHandle, $deltaPos ); |
| | + } else { |
| | + $baseSha = bin2hex( fread( $fileHandle, 20 ) ); |
| | + |
| | + $base = ''; |
| | + $streamed = $this->stream( $baseSha, function( $chunk ) use ( &$base ) { |
| | + $base .= $chunk; |
| | + } ); |
| | + |
| | + if( !$streamed ) { |
| | + return false; |
| | + } |
| | + } |
| | + |
| | + $compressed = fread( $fileHandle, self::MAX_READ ); |
| | + $delta = @gzuncompress( $compressed ) ?: ''; |
| | + |
| | + $result = $this->applyDelta( $base, $delta ); |
| | + |
| | + $chunkSize = 8192; |
| | + $length = strlen( $result ); |
| | + for( $i = 0; $i < $length; $i += $chunkSize ) { |
| | + $callback( substr( $result, $i, $chunkSize ) ); |
| | + } |
| | + |
| | + return true; |
| | + } |
| | + |
| | + private function streamDecompression( $fileHandle, callable $callback ): bool { |
| | + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + if( $inflator === false ) { |
| | + return false; |
| | + } |
| | + |
| | + while( !feof( $fileHandle ) ) { |
| | + $chunk = fread( $fileHandle, 8192 ); |
| | + |
| | + if( $chunk === false || $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $data = @inflate_add( $inflator, $chunk ); |
| | + |
| | + if( $data !== false && $data !== '' ) { |
| | + $callback( $data ); |
| | + } |
| | + |
| | + if( $data === false || |
| | + inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + } |
| | + |
| | + return true; |
| | + } |
| | + |
| | + /** |
| | + * Decompress to string (for small objects < 1MB) |
| | + */ |
| | + private function decompressToString( $fileHandle, int $maxSize ): string { |
| | + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + if( $inflator === false ) { |
| | + return ''; |
| | + } |
| | + |
| | + $result = ''; |
| | + |
| | + while( !feof( $fileHandle ) ) { |
| | + $chunk = fread( $fileHandle, 8192 ); |
| | + |
| | + if( $chunk === false || $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $data = @inflate_add( $inflator, $chunk ); |
| | + |
| | + if( $data !== false ) { |
| | + $result .= $data; |
| | + } |
| | + |
| | + if( $data === false || |
| | + inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function extractPackedSize( string $packPath, int $offset ): int { |
| | + $fileHandle = $this->getHandle( $packPath ); |
| | + |
| | + if( !$fileHandle ) { |
| | + return 0; |
| | + } |
| | + |
| | + fseek( $fileHandle, $offset ); |
| | + |
| | + $header = $this->readVarInt( $fileHandle ); |
| | + $size = $header['value']; |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + if( $type === 6 || $type === 7 ) { |
| | + return $this->readDeltaTargetSize( $fileHandle, $type ); |
| | + } |
| | + |
| | + return $size; |
| | + } |
| | + |
| | + private function handleOfsDelta( $fileHandle, int $offset, int $expectedSize ): string { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $negative = $byte & 127; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $negative = (($negative + 1) << 7) | ($byte & 127); |
| | + } |
| | + |
| | + $currentPos = ftell( $fileHandle ); |
| | + $baseOffset = $offset - $negative; |
| | + |
| | + fseek( $fileHandle, $baseOffset ); |
| | + $baseHeader = $this->readVarInt( $fileHandle ); |
| | + $baseSize = $baseHeader['value']; |
| | + |
| | + fseek( $fileHandle, $baseOffset ); |
| | + $base = $this->readPackEntry( $fileHandle, $baseOffset, $baseSize ); |
| | + |
| | + fseek( $fileHandle, $currentPos ); |
| | + |
| | + $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); |
| | + $compressed = fread( $fileHandle, $remainingBytes ); |
| | + $delta = @gzuncompress( $compressed ) ?: ''; |
| | + |
| | + return $this->applyDelta( $base, $delta ); |
| | + } |
| | + |
| | + private function handleRefDelta( $fileHandle, int $expectedSize ): string { |
| | + $baseSha = bin2hex( fread( $fileHandle, 20 ) ); |
| | + $base = $this->read( $baseSha ) ?? ''; |
| | + |
| | + $remainingBytes = min( self::MAX_READ, max( $expectedSize * 2, 1048576 ) ); |
| | + $compressed = fread( $fileHandle, $remainingBytes ); |
| | + $delta = @gzuncompress( $compressed ) ?: ''; |
| | + |
| | + return $this->applyDelta( $base, $delta ); |
| | + } |
| | + |
| | + private function applyDelta( string $base, string $delta ): string { |
| | + $position = 0; |
| | + $this->skipSize( $delta, $position ); |
| | + $this->skipSize( $delta, $position ); |
| | + |
| | + $output = ''; |
| | + $deltaLength = strlen( $delta ); |
| | + |
| | + while( $position < $deltaLength ) { |
| | + $opcode = ord( $delta[$position++] ); |
| | + |
| | + if( $opcode & 128 ) { |
| | + $offset = 0; |
| | + $length = 0; |
| | + |
| | + if( $opcode & 0x01 ) { |
| | + $offset |= ord( $delta[$position++] ); |
| | + } |
| | + if( $opcode & 0x02 ) { |
| | + $offset |= ord( $delta[$position++] ) << 8; |
| | + } |
| | + if( $opcode & 0x04 ) { |
| | + $offset |= ord( $delta[$position++] ) << 16; |
| | + } |
| | + if( $opcode & 0x08 ) { |
| | + $offset |= ord( $delta[$position++] ) << 24; |
| | + } |
| | + |
| | + if( $opcode & 0x10 ) { |
| | + $length |= ord( $delta[$position++] ); |
| | + } |
| | + if( $opcode & 0x20 ) { |
| | + $length |= ord( $delta[$position++] ) << 8; |
| | + } |
| | + if( $opcode & 0x40 ) { |
| | + $length |= ord( $delta[$position++] ) << 16; |
| | + } |
| | + |
| | + if( $length === 0 ) { |
| | + $length = 0x10000; |
| | + } |
| | + |
| | + $output .= substr( $base, $offset, $length ); |
| | + } else { |
| | + $length = $opcode & 127; |
| | + $output .= substr( $delta, $position, $length ); |
| | + $position += $length; |
| | + } |
| | + } |
| | + |
| | + return $output; |
| | + } |
| | + |
| | + private function readVarInt( $fileHandle ): array { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $value = $byte & 15; |
| | + $shift = 4; |
| | + $first = $byte; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + $value |= (($byte & 127) << $shift); |
| | + $shift += 7; |
| | + } |
| | + |
| | + return ['value' => $value, 'byte' => $first]; |
| | + } |
| | + |
| | + private function readDeltaTargetSize( $fileHandle, int $type ): int { |
| | + if( $type === 6 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $fileHandle, 1 ) ); |
| | + } |
| | + } else { |
| | + fseek( $fileHandle, 20, SEEK_CUR ); |
| | + } |
| | + |
| | + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + if( $inflator === false ) { |
| | + return 0; |
| | + } |
| | + |
| | + $header = ''; |
| | + $attempts = 0; |
| | + $maxAttempts = 64; |
| | + |
| | + while( !feof( $fileHandle ) && strlen( $header ) < 32 && $attempts < $maxAttempts ) { |
| | + $chunk = fread( $fileHandle, 512 ); |
| | + |
| | + if( $chunk === false || $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $output = @inflate_add( $inflator, $chunk, ZLIB_NO_FLUSH ); |
| | + |
| | + if( $output !== false ) { |
| | + $header .= $output; |
| | + } |
| | + |
| | + if( inflate_get_status( $inflator ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + |
| | + $attempts++; |
| | + } |
| | + |
| | + $position = 0; |
| | + |
| | + if( strlen( $header ) > 0 ) { |
| | + $this->skipSize( $header, $position ); |
| | + |
| | + return $this->readSize( $header, $position ); |
| | + } |
| | + |
| | + return 0; |
| | + } |
| | + |
| | + private function skipSize( string $data, int &$position ): void { |
| | + $length = strlen( $data ); |
| | + |
| | + while( $position < $length && (ord( $data[$position++] ) & 128) ) { |
| | + // Loop continues while MSB is 1 |
| | + } |
| | + } |
| | + |
| | + private function readSize( string $data, int &$position ): int { |
| | + $byte = ord( $data[$position++] ); |
| | + $value = $byte & 127; |
| | + $shift = 7; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( $data[$position++] ); |
| | + $value |= (($byte & 127) << $shift); |
| | + $shift += 7; |
| | + } |
| | + |
| | + return $value; |
| | + } |
| | + |
| | + private function getHandle( string $path ) { |
| | + if( !isset( $this->fileHandles[$path] ) ) { |
| | + $this->fileHandles[$path] = @fopen( $path, 'rb' ); |
| | + } |
| | + |
| | + return $this->fileHandles[$path]; |
| | + } |
| | +} |
| | |