| | if( $info['offset'] !== 0 ) { |
| | $handle = $this->getHandle( $info['file'] ); |
| | - if( $handle ) { |
| | - $result = $this->readPackEntry( |
| | - $handle, |
| | - $info['offset'], |
| | - $len, |
| | - $len |
| | - ); |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - public function read( string $sha ): string { |
| | - $info = $this->findPackInfo( $sha ); |
| | - $result = ''; |
| | - |
| | - if( $info['offset'] !== 0 ) { |
| | - $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | - if( $size <= self::MAX_RAM ) { |
| | - $handle = $this->getHandle( $info['file'] ); |
| | - if( $handle ) { |
| | - $result = $this->readPackEntry( |
| | - $handle, |
| | - $info['offset'], |
| | - $size |
| | - ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - public function stream( string $sha, callable $callback ): bool { |
| | - $info = $this->findPackInfo( $sha ); |
| | - $result = false; |
| | - |
| | - if( $info['offset'] !== 0 ) { |
| | - $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | - $handle = $this->getHandle( $info['file'] ); |
| | - if( $handle ) { |
| | - $result = $this->streamPackEntry( |
| | - $handle, |
| | - $info['offset'], |
| | - $size, |
| | - $callback |
| | - ); |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - public function getSize( string $sha ): int { |
| | - $info = $this->findPackInfo( $sha ); |
| | - $result = 0; |
| | - |
| | - if( $info['offset'] !== 0 ) { |
| | - $result = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function findPackInfo( string $sha ): array { |
| | - $result = [ 'offset' => 0, 'file' => '' ]; |
| | - $binarySha = hex2bin( $sha ); |
| | - |
| | - if( strlen( $sha ) === 40 && $binarySha !== false ) { |
| | - if( $this->lastPack !== '' ) { |
| | - $offset = $this->findInIdx( $this->lastPack, $binarySha ); |
| | - if( $offset !== 0 ) { |
| | - $result = [ |
| | - 'file' => str_replace( '.idx', '.pack', $this->lastPack ), |
| | - 'offset' => $offset |
| | - ]; |
| | - } |
| | - } |
| | - |
| | - if( $result['offset'] === 0 ) { |
| | - foreach( $this->packFiles as $indexFile ) { |
| | - if( $indexFile !== $this->lastPack ) { |
| | - $offset = $this->findInIdx( $indexFile, $binarySha ); |
| | - if( $offset !== 0 ) { |
| | - $this->lastPack = $indexFile; |
| | - $result = [ |
| | - 'file' => str_replace( '.idx', '.pack', $indexFile ), |
| | - 'offset' => $offset |
| | - ]; |
| | - break; |
| | - } |
| | - } |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function findInIdx( string $indexFile, string $binarySha ): int { |
| | - $handle = $this->getHandle( $indexFile ); |
| | - $result = 0; |
| | - |
| | - if( $handle ) { |
| | - if( !isset( $this->fanoutCache[$indexFile] ) ) { |
| | - fseek( $handle, 0 ); |
| | - $head = fread( $handle, 8 ); |
| | - if( $head === "\377tOc\0\0\0\2" ) { |
| | - $this->fanoutCache[$indexFile] = array_values( |
| | - unpack( 'N*', fread( $handle, 1024 ) ) |
| | - ); |
| | - } |
| | - } |
| | - |
| | - if( isset( $this->fanoutCache[$indexFile] ) ) { |
| | - $fanout = $this->fanoutCache[$indexFile]; |
| | - $byte = ord( $binarySha[0] ); |
| | - $start = $byte === 0 ? 0 : $fanout[$byte - 1]; |
| | - $end = $fanout[$byte]; |
| | - |
| | - if( $end > $start ) { |
| | - $result = $this->binarySearchIdx( |
| | - $indexFile, |
| | - $handle, |
| | - $start, |
| | - $end, |
| | - $binarySha, |
| | - $fanout[255] |
| | - ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function binarySearchIdx( |
| | - string $indexFile, |
| | - $handle, |
| | - int $start, |
| | - int $end, |
| | - string $binarySha, |
| | - int $total |
| | - ): int { |
| | - $key = "$indexFile:$start"; |
| | - $count = $end - $start; |
| | - $result = 0; |
| | - |
| | - if( !isset( $this->shaBucketCache[$key] ) ) { |
| | - fseek( $handle, 1032 + ($start * 20) ); |
| | - $this->shaBucketCache[$key] = fread( $handle, $count * 20 ); |
| | - fseek( $handle, 1032 + ($total * 24) + ($start * 4) ); |
| | - $this->offsetBucketCache[$key] = fread( $handle, $count * 4 ); |
| | - } |
| | - |
| | - $shaBlock = $this->shaBucketCache[$key]; |
| | - $low = 0; |
| | - $high = $count - 1; |
| | - $found = -1; |
| | - |
| | - while( $low <= $high ) { |
| | - $mid = ($low + $high) >> 1; |
| | - $cmp = substr( $shaBlock, $mid * 20, 20 ); |
| | - |
| | - if( $cmp < $binarySha ) { |
| | - $low = $mid + 1; |
| | - } elseif( $cmp > $binarySha ) { |
| | - $high = $mid - 1; |
| | - } else { |
| | - $found = $mid; |
| | - break; |
| | - } |
| | - } |
| | - |
| | - if( $found !== -1 ) { |
| | - $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 ); |
| | - $offset = unpack( 'N', $packed )[1]; |
| | - |
| | - if( $offset & 0x80000000 ) { |
| | - $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8); |
| | - fseek( $handle, $pos64 ); |
| | - $offset = unpack( 'J', fread( $handle, 8 ) )[1]; |
| | - } |
| | - $result = (int)$offset; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function readPackEntry( |
| | - $handle, |
| | - int $offset, |
| | - int $size, |
| | - int $cap = 0 |
| | - ): string { |
| | - fseek( $handle, $offset ); |
| | - $header = $this->readVarInt( $handle ); |
| | - $type = ($header['byte'] >> 4) & 7; |
| | - |
| | - return ($type === 6) |
| | - ? $this->handleOfsDelta( $handle, $offset, $size, $cap ) |
| | - : (($type === 7) |
| | - ? $this->handleRefDelta( $handle, $size, $cap ) |
| | - : $this->decompressToString( $handle, $cap )); |
| | - } |
| | - |
| | - private function streamPackEntry( |
| | - $handle, |
| | - int $offset, |
| | - int $size, |
| | - callable $callback |
| | - ): bool { |
| | - fseek( $handle, $offset ); |
| | - $header = $this->readVarInt( $handle ); |
| | - $type = ($header['byte'] >> 4) & 7; |
| | - |
| | - return ($type === 6 || $type === 7) |
| | - ? $this->streamDeltaObject( $handle, $offset, $type, $callback ) |
| | - : $this->streamDecompression( $handle, $callback ); |
| | - } |
| | - |
| | - private function streamDeltaObject( |
| | - $handle, |
| | - int $offset, |
| | - int $type, |
| | - callable $callback |
| | - ): bool { |
| | - fseek( $handle, $offset ); |
| | - $this->readVarInt( $handle ); |
| | - |
| | - if( $type === 6 ) { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $neg = $byte & 127; |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $neg = (($neg + 1) << 7) | ($byte & 127); |
| | - } |
| | - $deltaPos = ftell( $handle ); |
| | - $base = ''; |
| | - $this->streamPackEntry( |
| | - $handle, |
| | - $offset - $neg, |
| | - 0, |
| | - function( $c ) use ( &$base ) { $base .= $c; } |
| | - ); |
| | - fseek( $handle, $deltaPos ); |
| | - } else { |
| | - $baseSha = bin2hex( fread( $handle, 20 ) ); |
| | - $base = ''; |
| | - if( !$this->stream( $baseSha, function( $c ) use ( &$base ) { |
| | - $base .= $c; |
| | - } ) ) { |
| | - return false; |
| | - } |
| | - } |
| | - |
| | - return $this->applyDeltaStream( $handle, $base, $callback ); |
| | - } |
| | - |
| | - private function applyDeltaStream( |
| | - $handle, |
| | - string $base, |
| | - callable $callback |
| | - ): bool { |
| | - $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - if( !$infl ) { |
| | - return false; |
| | - } |
| | - |
| | - $state = 0; |
| | - $buffer = ''; |
| | - |
| | - while( !feof( $handle ) ) { |
| | - $chunk = fread( $handle, 8192 ); |
| | - if( $chunk === '' ) { break; } |
| | - $data = @inflate_add( $infl, $chunk ); |
| | - if( $data === false ) { break; } |
| | - $buffer .= $data; |
| | - |
| | - while( true ) { |
| | - $len = strlen( $buffer ); |
| | - if( $len === 0 ) { break; } |
| | - |
| | - if( $state < 2 ) { |
| | - $pos = 0; |
| | - while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; } |
| | - if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { break; } |
| | - $buffer = substr( $buffer, $pos + 1 ); |
| | - $state++; |
| | - continue; |
| | - } |
| | - |
| | - $op = ord( $buffer[0] ); |
| | - if( $op & 128 ) { |
| | - $need = 1; |
| | - if( $op & 0x01 ) { $need++; } |
| | - if( $op & 0x02 ) { $need++; } |
| | - if( $op & 0x04 ) { $need++; } |
| | - if( $op & 0x08 ) { $need++; } |
| | - if( $op & 0x10 ) { $need++; } |
| | - if( $op & 0x20 ) { $need++; } |
| | - if( $op & 0x40 ) { $need++; } |
| | - |
| | - if( $len < $need ) { break; } |
| | - |
| | - $off = 0; |
| | - $ln = 0; |
| | - $p = 1; |
| | - if( $op & 0x01 ) { $off |= ord( $buffer[$p++] ); } |
| | - if( $op & 0x02 ) { $off |= ord( $buffer[$p++] ) << 8; } |
| | - if( $op & 0x04 ) { $off |= ord( $buffer[$p++] ) << 16; } |
| | - if( $op & 0x08 ) { $off |= ord( $buffer[$p++] ) << 24; } |
| | - if( $op & 0x10 ) { $ln |= ord( $buffer[$p++] ); } |
| | - if( $op & 0x20 ) { $ln |= ord( $buffer[$p++] ) << 8; } |
| | - if( $op & 0x40 ) { $ln |= ord( $buffer[$p++] ) << 16; } |
| | - if( $ln === 0 ) { $ln = 0x10000; } |
| | - |
| | - $callback( substr( $base, $off, $ln ) ); |
| | - $buffer = substr( $buffer, $need ); |
| | - } else { |
| | - $ln = $op & 127; |
| | - if( $len < 1 + $ln ) { break; } |
| | - $callback( substr( $buffer, 1, $ln ) ); |
| | - $buffer = substr( $buffer, 1 + $ln ); |
| | - } |
| | - } |
| | - |
| | - if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) { break; } |
| | - } |
| | - return true; |
| | - } |
| | - |
| | - private function streamDecompression( $handle, callable $callback ): bool { |
| | - $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - if( !$infl ) { |
| | - return false; |
| | - } |
| | - |
| | - while( !feof( $handle ) ) { |
| | - $chunk = fread( $handle, 8192 ); |
| | - if( $chunk === '' ) { break; } |
| | - $data = @inflate_add( $infl, $chunk ); |
| | - if( $data !== false && $data !== '' ) { |
| | - $callback( $data ); |
| | - } |
| | - if( $data === false || |
| | - inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | - break; |
| | - } |
| | - } |
| | - return true; |
| | - } |
| | - |
| | - private function decompressToString( |
| | - $handle, |
| | - int $cap = 0 |
| | - ): string { |
| | - $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - $res = ''; |
| | - |
| | - if( $infl ) { |
| | - while( !feof( $handle ) ) { |
| | - $chunk = fread( $handle, 8192 ); |
| | - if( $chunk === '' ) { break; } |
| | - $data = @inflate_add( $infl, $chunk ); |
| | - if( $data !== false ) { $res .= $data; } |
| | - if( $cap > 0 && strlen( $res ) >= $cap ) { |
| | - $res = substr( $res, 0, $cap ); |
| | - break; |
| | - } |
| | - if( $data === false || |
| | - inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | - break; |
| | - } |
| | - } |
| | - } |
| | - return $res; |
| | - } |
| | - |
| | - private function extractPackedSize( string $packPath, int $offset ): int { |
| | - $handle = $this->getHandle( $packPath ); |
| | - $size = 0; |
| | - |
| | - if( $handle ) { |
| | - fseek( $handle, $offset ); |
| | - $header = $this->readVarInt( $handle ); |
| | - $size = $header['value']; |
| | - $type = ($header['byte'] >> 4) & 7; |
| | - |
| | - if( $type === 6 || $type === 7 ) { |
| | - $size = $this->readDeltaTargetSize( $handle, $type ); |
| | - } |
| | - } |
| | - return $size; |
| | - } |
| | - |
| | - private function handleOfsDelta( |
| | - $handle, |
| | - int $offset, |
| | - int $size, |
| | - int $cap |
| | - ): string { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $neg = $byte & 127; |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $neg = (($neg + 1) << 7) | ($byte & 127); |
| | - } |
| | - $cur = ftell( $handle ); |
| | - $base = $offset - $neg; |
| | - |
| | - fseek( $handle, $base ); |
| | - $bHead = $this->readVarInt( $handle ); |
| | - fseek( $handle, $base ); |
| | - $bData = $this->readPackEntry( $handle, $base, $bHead['value'], $cap ); |
| | - fseek( $handle, $cur ); |
| | - |
| | - $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
| | - $comp = fread( $handle, $rem ); |
| | - $delta = @gzuncompress( $comp ) ?: ''; |
| | - |
| | - return $this->applyDelta( $bData, $delta, $cap ); |
| | - } |
| | - |
| | - private function handleRefDelta( $handle, int $size, int $cap ): string { |
| | - $sha = bin2hex( fread( $handle, 20 ) ); |
| | - $bas = $cap > 0 ? $this->peek( $sha, $cap ) : $this->read( $sha ); |
| | - $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
| | - $cmp = fread( $handle, $rem ); |
| | - $del = @gzuncompress( $cmp ) ?: ''; |
| | - |
| | - return $this->applyDelta( $bas, $del, $cap ); |
| | - } |
| | - |
| | - private function applyDelta( string $base, string $delta, int $cap ): string { |
| | - $pos = 0; |
| | - $this->readSize( $delta, $pos ); |
| | - $this->readSize( $delta, $pos ); |
| | - |
| | - $out = ''; |
| | - $len = strlen( $delta ); |
| | - |
| | - while( $pos < $len ) { |
| | - if( $cap > 0 && strlen( $out ) >= $cap ) { break; } |
| | - $op = ord( $delta[$pos++] ); |
| | - |
| | - if( $op & 128 ) { |
| | - $off = 0; |
| | - $ln = 0; |
| | - if( $op & 0x01 ) { $off |= ord( $delta[$pos++] ); } |
| | - if( $op & 0x02 ) { $off |= ord( $delta[$pos++] ) << 8; } |
| | - if( $op & 0x04 ) { $off |= ord( $delta[$pos++] ) << 16; } |
| | - if( $op & 0x08 ) { $off |= ord( $delta[$pos++] ) << 24; } |
| | - if( $op & 0x10 ) { $ln |= ord( $delta[$pos++] ); } |
| | - if( $op & 0x20 ) { $ln |= ord( $delta[$pos++] ) << 8; } |
| | - if( $op & 0x40 ) { $ln |= ord( $delta[$pos++] ) << 16; } |
| | - if( $ln === 0 ) { $ln = 0x10000; } |
| | - $out .= substr( $base, $off, $ln ); |
| | - } else { |
| | - $ln = $op & 127; |
| | - $out .= substr( $delta, $pos, $ln ); |
| | - $pos += $ln; |
| | - } |
| | - } |
| | - return $out; |
| | - } |
| | - |
| | - private function readVarInt( $handle ): array { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $val = $byte & 15; |
| | - $shft = 4; |
| | - $fst = $byte; |
| | - |
| | - while( $byte & 128 ) { |
| | - $byte = ord( fread( $handle, 1 ) ); |
| | - $val |= (($byte & 127) << $shft); |
| | - $shft += 7; |
| | - } |
| | - return [ 'value' => $val, 'byte' => $fst ]; |
| | - } |
| | - |
| | - private function readDeltaTargetSize( $handle, int $type ): int { |
| | - if( $type === 6 ) { |
| | - $b = ord( fread( $handle, 1 ) ); |
| | - while( $b & 128 ) { $b = ord( fread( $handle, 1 ) ); } |
| | - } else { |
| | - fseek( $handle, 20, SEEK_CUR ); |
| | - } |
| | - |
| | - $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | - $head = ''; |
| | - $try = 0; |
| | - |
| | - if( $infl ) { |
| | - while( !feof( $handle ) && strlen( $head ) < 32 && $try < 64 ) { |
| | - $chunk = fread( $handle, 512 ); |
| | - if( $chunk === '' ) { break; } |
| | - $out = @inflate_add( $infl, $chunk, ZLIB_NO_FLUSH ); |
| | - if( $out !== false ) { $head .= $out; } |
| | - if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) { break; } |
| | - $try++; |
| | - } |
| | - } |
| | - |
| | - $pos = 0; |
| | - if( strlen( $head ) > 0 ) { |
| | - $this->readSize( $head, $pos ); |
| | - return $this->readSize( $head, $pos ); |
| | - } |
| | - return 0; |
| | - } |
| | - |
| | - private function readSize( string $data, int &$pos ): int { |
| | - $len = strlen( $data ); |
| | - $val = 0; |
| | - $shift = 0; |
| | - |
| | - while( $pos < $len ) { |
| | - $byte = ord( $data[$pos++] ); |
| | - $val |= ($byte & 0x7F) << $shift; |
| | - |
| | - if( !($byte & 0x80) ) { |
| | - break; |
| | - } |
| | - |
| | - $shift += 7; |
| | - } |
| | - |
| | - return $val; |
| | - } |
| | - |
| | - private function getHandle( string $path ) { |
| | - if( !isset( $this->fileHandles[$path] ) ) { |
| | - $this->fileHandles[$path] = @fopen( $path, 'rb' ); |
| | - } |
| | + |
| | + if( $handle ) { |
| | + $result = $this->readPackEntry( |
| | + $handle, |
| | + $info['offset'], |
| | + $len, |
| | + $len |
| | + ); |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + public function read( string $sha ): string { |
| | + $info = $this->findPackInfo( $sha ); |
| | + $result = ''; |
| | + |
| | + if( $info['offset'] !== 0 ) { |
| | + $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + |
| | + if( $size <= self::MAX_RAM ) { |
| | + $handle = $this->getHandle( $info['file'] ); |
| | + |
| | + if( $handle ) { |
| | + $result = $this->readPackEntry( |
| | + $handle, |
| | + $info['offset'], |
| | + $size |
| | + ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + public function stream( string $sha, callable $callback ): bool { |
| | + $info = $this->findPackInfo( $sha ); |
| | + $result = false; |
| | + |
| | + if( $info['offset'] !== 0 ) { |
| | + $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + $handle = $this->getHandle( $info['file'] ); |
| | + |
| | + if( $handle ) { |
| | + $result = $this->streamPackEntry( |
| | + $handle, |
| | + $info['offset'], |
| | + $size, |
| | + $callback |
| | + ); |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + public function getSize( string $sha ): int { |
| | + $info = $this->findPackInfo( $sha ); |
| | + $result = 0; |
| | + |
| | + if( $info['offset'] !== 0 ) { |
| | + $result = $this->extractPackedSize( $info['file'], $info['offset'] ); |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function findPackInfo( string $sha ): array { |
| | + $result = [ 'offset' => 0, 'file' => '' ]; |
| | + $binarySha = hex2bin( $sha ); |
| | + |
| | + if( strlen( $sha ) === 40 && $binarySha !== false ) { |
| | + if( $this->lastPack !== '' ) { |
| | + $offset = $this->findInIdx( $this->lastPack, $binarySha ); |
| | + |
| | + if( $offset !== 0 ) { |
| | + $result = [ |
| | + 'file' => str_replace( '.idx', '.pack', $this->lastPack ), |
| | + 'offset' => $offset |
| | + ]; |
| | + } |
| | + } |
| | + |
| | + if( $result['offset'] === 0 ) { |
| | + foreach( $this->packFiles as $indexFile ) { |
| | + if( $indexFile !== $this->lastPack ) { |
| | + $offset = $this->findInIdx( $indexFile, $binarySha ); |
| | + |
| | + if( $offset !== 0 ) { |
| | + $this->lastPack = $indexFile; |
| | + $result = [ |
| | + 'file' => str_replace( '.idx', '.pack', $indexFile ), |
| | + 'offset' => $offset |
| | + ]; |
| | + break; |
| | + } |
| | + } |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function findInIdx( string $indexFile, string $binarySha ): int { |
| | + $handle = $this->getHandle( $indexFile ); |
| | + $result = 0; |
| | + |
| | + if( $handle ) { |
| | + if( !isset( $this->fanoutCache[$indexFile] ) ) { |
| | + fseek( $handle, 0 ); |
| | + $head = fread( $handle, 8 ); |
| | + |
| | + if( $head === "\377tOc\0\0\0\2" ) { |
| | + $this->fanoutCache[$indexFile] = array_values( |
| | + unpack( 'N*', fread( $handle, 1024 ) ) |
| | + ); |
| | + } |
| | + } |
| | + |
| | + if( isset( $this->fanoutCache[$indexFile] ) ) { |
| | + $fanout = $this->fanoutCache[$indexFile]; |
| | + $byte = ord( $binarySha[0] ); |
| | + $start = $byte === 0 ? 0 : $fanout[$byte - 1]; |
| | + $end = $fanout[$byte]; |
| | + |
| | + if( $end > $start ) { |
| | + $result = $this->binarySearchIdx( |
| | + $indexFile, |
| | + $handle, |
| | + $start, |
| | + $end, |
| | + $binarySha, |
| | + $fanout[255] |
| | + ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function binarySearchIdx( |
| | + string $indexFile, |
| | + $handle, |
| | + int $start, |
| | + int $end, |
| | + string $binarySha, |
| | + int $total |
| | + ): int { |
| | + $key = "$indexFile:$start"; |
| | + $count = $end - $start; |
| | + $result = 0; |
| | + |
| | + if( !isset( $this->shaBucketCache[$key] ) ) { |
| | + fseek( $handle, 1032 + ($start * 20) ); |
| | + $this->shaBucketCache[$key] = fread( $handle, $count * 20 ); |
| | + |
| | + fseek( $handle, 1032 + ($total * 24) + ($start * 4) ); |
| | + $this->offsetBucketCache[$key] = fread( $handle, $count * 4 ); |
| | + } |
| | + |
| | + $shaBlock = $this->shaBucketCache[$key]; |
| | + $low = 0; |
| | + $high = $count - 1; |
| | + $found = -1; |
| | + |
| | + while( $low <= $high ) { |
| | + $mid = ($low + $high) >> 1; |
| | + $cmp = substr( $shaBlock, $mid * 20, 20 ); |
| | + |
| | + if( $cmp < $binarySha ) { |
| | + $low = $mid + 1; |
| | + } elseif( $cmp > $binarySha ) { |
| | + $high = $mid - 1; |
| | + } else { |
| | + $found = $mid; |
| | + break; |
| | + } |
| | + } |
| | + |
| | + if( $found !== -1 ) { |
| | + $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 ); |
| | + $offset = unpack( 'N', $packed )[1]; |
| | + |
| | + if( $offset & 0x80000000 ) { |
| | + $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8); |
| | + fseek( $handle, $pos64 ); |
| | + $offset = unpack( 'J', fread( $handle, 8 ) )[1]; |
| | + } |
| | + $result = (int)$offset; |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function readPackEntry( |
| | + $handle, |
| | + int $offset, |
| | + int $size, |
| | + int $cap = 0 |
| | + ): string { |
| | + fseek( $handle, $offset ); |
| | + $header = $this->readVarInt( $handle ); |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + return ($type === 6) |
| | + ? $this->handleOfsDelta( $handle, $offset, $size, $cap ) |
| | + : (($type === 7) |
| | + ? $this->handleRefDelta( $handle, $size, $cap ) |
| | + : $this->decompressToString( $handle, $cap )); |
| | + } |
| | + |
| | + private function streamPackEntry( |
| | + $handle, |
| | + int $offset, |
| | + int $size, |
| | + callable $callback |
| | + ): bool { |
| | + fseek( $handle, $offset ); |
| | + $header = $this->readVarInt( $handle ); |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + return ($type === 6 || $type === 7) |
| | + ? $this->streamDeltaObject( $handle, $offset, $type, $callback ) |
| | + : $this->streamDecompression( $handle, $callback ); |
| | + } |
| | + |
| | + private function streamDeltaObject( |
| | + $handle, |
| | + int $offset, |
| | + int $type, |
| | + callable $callback |
| | + ): bool { |
| | + fseek( $handle, $offset ); |
| | + $this->readVarInt( $handle ); |
| | + $result = false; |
| | + |
| | + if( $type === 6 ) { |
| | + $neg = $this->readOffsetDelta( $handle ); |
| | + $deltaPos = ftell( $handle ); |
| | + $base = ''; |
| | + |
| | + $this->streamPackEntry( |
| | + $handle, |
| | + $offset - $neg, |
| | + 0, |
| | + function( $c ) use ( &$base ) { $base .= $c; } |
| | + ); |
| | + |
| | + fseek( $handle, $deltaPos ); |
| | + $result = $this->applyDeltaStream( $handle, $base, $callback ); |
| | + } else { |
| | + $baseSha = bin2hex( fread( $handle, 20 ) ); |
| | + $base = ''; |
| | + |
| | + if( $this->stream( $baseSha, function( $c ) use ( &$base ) { |
| | + $base .= $c; |
| | + } ) ) { |
| | + $result = $this->applyDeltaStream( $handle, $base, $callback ); |
| | + } |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function applyDeltaStream( |
| | + $handle, |
| | + string $base, |
| | + callable $callback |
| | + ): bool { |
| | + $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + $ok = false; |
| | + |
| | + if( $infl ) { |
| | + $state = 0; |
| | + $buffer = ''; |
| | + $ok = true; |
| | + |
| | + while( !feof( $handle ) ) { |
| | + $chunk = fread( $handle, 8192 ); |
| | + |
| | + if( $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $data = @inflate_add( $infl, $chunk ); |
| | + |
| | + if( $data === false ) { |
| | + $ok = false; |
| | + break; |
| | + } |
| | + |
| | + $buffer .= $data; |
| | + |
| | + while( true ) { |
| | + $len = strlen( $buffer ); |
| | + |
| | + if( $len === 0 ) { |
| | + break; |
| | + } |
| | + |
| | + if( $state < 2 ) { |
| | + $pos = 0; |
| | + while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; } |
| | + |
| | + if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { |
| | + break; |
| | + } |
| | + |
| | + $buffer = substr( $buffer, $pos + 1 ); |
| | + $state++; |
| | + continue; |
| | + } |
| | + |
| | + $op = ord( $buffer[0] ); |
| | + |
| | + if( $op & 128 ) { |
| | + $need = $this->getCopyInstructionSize( $op ); |
| | + |
| | + if( $len < 1 + $need ) { |
| | + break; |
| | + } |
| | + |
| | + $info = $this->parseCopyInstruction( $op, $buffer, 1 ); |
| | + |
| | + $callback( substr( $base, $info['off'], $info['len'] ) ); |
| | + $buffer = substr( $buffer, 1 + $need ); |
| | + } else { |
| | + $ln = $op & 127; |
| | + |
| | + if( $len < 1 + $ln ) { |
| | + break; |
| | + } |
| | + |
| | + $callback( substr( $buffer, 1, $ln ) ); |
| | + $buffer = substr( $buffer, 1 + $ln ); |
| | + } |
| | + } |
| | + |
| | + if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $ok; |
| | + } |
| | + |
| | + private function streamDecompression( $handle, callable $callback ): bool { |
| | + $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + |
| | + if( !$infl ) { |
| | + return false; |
| | + } |
| | + |
| | + while( !feof( $handle ) ) { |
| | + $chunk = fread( $handle, 8192 ); |
| | + |
| | + if( $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $data = @inflate_add( $infl, $chunk ); |
| | + |
| | + if( $data !== false && $data !== '' ) { |
| | + $callback( $data ); |
| | + } |
| | + |
| | + if( $data === false || |
| | + inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + } |
| | + |
| | + return true; |
| | + } |
| | + |
| | + private function decompressToString( |
| | + $handle, |
| | + int $cap = 0 |
| | + ): string { |
| | + $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + $res = ''; |
| | + |
| | + if( $infl ) { |
| | + while( !feof( $handle ) ) { |
| | + $chunk = fread( $handle, 8192 ); |
| | + |
| | + if( $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $data = @inflate_add( $infl, $chunk ); |
| | + |
| | + if( $data !== false ) { |
| | + $res .= $data; |
| | + } |
| | + |
| | + if( $cap > 0 && strlen( $res ) >= $cap ) { |
| | + $res = substr( $res, 0, $cap ); |
| | + break; |
| | + } |
| | + |
| | + if( $data === false || |
| | + inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $res; |
| | + } |
| | + |
| | + private function extractPackedSize( string $packPath, int $offset ): int { |
| | + $handle = $this->getHandle( $packPath ); |
| | + $size = 0; |
| | + |
| | + if( $handle ) { |
| | + fseek( $handle, $offset ); |
| | + $header = $this->readVarInt( $handle ); |
| | + $size = $header['value']; |
| | + $type = ($header['byte'] >> 4) & 7; |
| | + |
| | + if( $type === 6 || $type === 7 ) { |
| | + $size = $this->readDeltaTargetSize( $handle, $type ); |
| | + } |
| | + } |
| | + |
| | + return $size; |
| | + } |
| | + |
| | + private function handleOfsDelta( |
| | + $handle, |
| | + int $offset, |
| | + int $size, |
| | + int $cap |
| | + ): string { |
| | + $neg = $this->readOffsetDelta( $handle ); |
| | + $cur = ftell( $handle ); |
| | + $base = $offset - $neg; |
| | + |
| | + fseek( $handle, $base ); |
| | + $bHead = $this->readVarInt( $handle ); |
| | + |
| | + fseek( $handle, $base ); |
| | + $bData = $this->readPackEntry( $handle, $base, $bHead['value'], $cap ); |
| | + |
| | + fseek( $handle, $cur ); |
| | + $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
| | + $comp = fread( $handle, $rem ); |
| | + $delta = @gzuncompress( $comp ) ?: ''; |
| | + |
| | + return $this->applyDelta( $bData, $delta, $cap ); |
| | + } |
| | + |
| | + private function handleRefDelta( $handle, int $size, int $cap ): string { |
| | + $sha = bin2hex( fread( $handle, 20 ) ); |
| | + $bas = $cap > 0 ? $this->peek( $sha, $cap ) : $this->read( $sha ); |
| | + $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
| | + $cmp = fread( $handle, $rem ); |
| | + $del = @gzuncompress( $cmp ) ?: ''; |
| | + |
| | + return $this->applyDelta( $bas, $del, $cap ); |
| | + } |
| | + |
| | + private function applyDelta( string $base, string $delta, int $cap ): string { |
| | + $pos = 0; |
| | + $res = $this->readDeltaSize( $delta, $pos ); |
| | + $pos += $res['used']; |
| | + $res = $this->readDeltaSize( $delta, $pos ); |
| | + $pos += $res['used']; |
| | + |
| | + $out = ''; |
| | + $len = strlen( $delta ); |
| | + |
| | + while( $pos < $len ) { |
| | + if( $cap > 0 && strlen( $out ) >= $cap ) { |
| | + break; |
| | + } |
| | + |
| | + $op = ord( $delta[$pos++] ); |
| | + |
| | + if( $op & 128 ) { |
| | + $info = $this->parseCopyInstruction( $op, $delta, $pos ); |
| | + $out .= substr( $base, $info['off'], $info['len'] ); |
| | + $pos += $info['used']; |
| | + } else { |
| | + $ln = $op & 127; |
| | + $out .= substr( $delta, $pos, $ln ); |
| | + $pos += $ln; |
| | + } |
| | + } |
| | + |
| | + return $out; |
| | + } |
| | + |
| | + private function parseCopyInstruction( |
| | + int $op, |
| | + string $data, |
| | + int $pos |
| | + ): array { |
| | + $off = 0; |
| | + $len = 0; |
| | + $ptr = $pos; |
| | + |
| | + if( $op & 0x01 ) { $off |= ord( $data[$ptr++] ); } |
| | + if( $op & 0x02 ) { $off |= ord( $data[$ptr++] ) << 8; } |
| | + if( $op & 0x04 ) { $off |= ord( $data[$ptr++] ) << 16; } |
| | + if( $op & 0x08 ) { $off |= ord( $data[$ptr++] ) << 24; } |
| | + |
| | + if( $op & 0x10 ) { $len |= ord( $data[$ptr++] ); } |
| | + if( $op & 0x20 ) { $len |= ord( $data[$ptr++] ) << 8; } |
| | + if( $op & 0x40 ) { $len |= ord( $data[$ptr++] ) << 16; } |
| | + |
| | + return [ |
| | + 'off' => $off, |
| | + 'len' => $len ?: 0x10000, |
| | + 'used' => $ptr - $pos |
| | + ]; |
| | + } |
| | + |
| | + private function getCopyInstructionSize( int $op ): int { |
| | + $c = $op & 0x7F; |
| | + $c = $c - (( $c >> 1 ) & 0x55); |
| | + $c = (( $c >> 2 ) & 0x33) + ( $c & 0x33 ); |
| | + $c = (( $c >> 4 ) + $c) & 0x0F; |
| | + |
| | + return $c; |
| | + } |
| | + |
| | + private function readVarInt( $handle ): array { |
| | + $byte = ord( fread( $handle, 1 ) ); |
| | + $val = $byte & 15; |
| | + $shft = 4; |
| | + $fst = $byte; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $handle, 1 ) ); |
| | + $val |= (($byte & 127) << $shft); |
| | + $shft += 7; |
| | + } |
| | + |
| | + return [ 'value' => $val, 'byte' => $fst ]; |
| | + } |
| | + |
| | + private function readOffsetDelta( $handle ): int { |
| | + $byte = ord( fread( $handle, 1 ) ); |
| | + $neg = $byte & 127; |
| | + |
| | + while( $byte & 128 ) { |
| | + $byte = ord( fread( $handle, 1 ) ); |
| | + $neg = (($neg + 1) << 7) | ($byte & 127); |
| | + } |
| | + |
| | + return $neg; |
| | + } |
| | + |
| | + private function readDeltaTargetSize( $handle, int $type ): int { |
| | + if( $type === 6 ) { |
| | + $b = ord( fread( $handle, 1 ) ); |
| | + while( $b & 128 ) { $b = ord( fread( $handle, 1 ) ); } |
| | + } else { |
| | + fseek( $handle, 20, SEEK_CUR ); |
| | + } |
| | + |
| | + $infl = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + $head = ''; |
| | + $try = 0; |
| | + |
| | + if( $infl ) { |
| | + while( !feof( $handle ) && strlen( $head ) < 32 && $try < 64 ) { |
| | + $chunk = fread( $handle, 512 ); |
| | + |
| | + if( $chunk === '' ) { |
| | + break; |
| | + } |
| | + |
| | + $out = @inflate_add( $infl, $chunk, ZLIB_NO_FLUSH ); |
| | + |
| | + if( $out !== false ) { |
| | + $head .= $out; |
| | + } |
| | + |
| | + if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) { |
| | + break; |
| | + } |
| | + |
| | + $try++; |
| | + } |
| | + } |
| | + |
| | + $pos = 0; |
| | + |
| | + if( strlen( $head ) > 0 ) { |
| | + $res = $this->readDeltaSize( $head, $pos ); |
| | + $pos += $res['used']; |
| | + $res = $this->readDeltaSize( $head, $pos ); |
| | + |
| | + return $res['val']; |
| | + } |
| | + |
| | + return 0; |
| | + } |
| | + |
| | + private function readDeltaSize( string $data, int $pos ): array { |
| | + $len = strlen( $data ); |
| | + $val = 0; |
| | + $shift = 0; |
| | + $start = $pos; |
| | + |
| | + while( $pos < $len ) { |
| | + $byte = ord( $data[$pos++] ); |
| | + $val |= ($byte & 0x7F) << $shift; |
| | + |
| | + if( !($byte & 0x80) ) { |
| | + break; |
| | + } |
| | + |
| | + $shift += 7; |
| | + } |
| | + |
| | + return [ 'val' => $val, 'used' => $pos - $start ]; |
| | + } |
| | + |
| | + private function getHandle( string $path ) { |
| | + if( !isset( $this->fileHandles[$path] ) ) { |
| | + $this->fileHandles[$path] = @fopen( $path, 'rb' ); |
| | + } |
| | + |
| | return $this->fileHandles[$path]; |
| | } |