Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git

Optimizes handling of large binaries

AuthorDave Jarvis <email>
Date2026-02-11 23:50:14 GMT-0800
Commitfff610d11b54cf8e370b99c30c84a5f3480552bb
Parent8ed6561
File.php
class File {
+ private ?Git $git;
private string $name;
private string $sha;
private string $mode;
private int $timestamp;
private int $size;
private bool $isDir;
- public function __construct(string $name, string $sha, string $mode, int $timestamp = 0, int $size = 0) {
- $this->name = $name;
- $this->sha = $sha;
- $this->mode = $mode;
+ public function __construct( ?Git $git, string $name, string $sha, string $mode, int $timestamp = 0, int $size = 0 ) {
+ $this->git = $git;
+ $this->name = $name;
+ $this->sha = $sha;
+ $this->mode = $mode;
$this->timestamp = $timestamp;
- $this->size = $size;
- $this->isDir = ($mode === '40000' || $mode === '040000');
+ $this->size = $size;
+ $this->isDir = ( $mode === '40000' || $mode === '040000' );
}
- public function compare(File $other): int {
- if ($this->isDir !== $other->isDir) {
+ public function compare( File $other ): int {
+ if( $this->isDir !== $other->isDir ) {
return $this->isDir ? -1 : 1;
}
- return strcasecmp($this->name, $other->name);
+ return strcasecmp( $this->name, $other->name );
}
- public function render(FileRenderer $renderer): void {
+ public function render( FileRenderer $renderer ): void {
$renderer->renderFileItem(
$this->name,
private function getIconClass(): string {
- if ($this->isDir) return 'fa-folder';
+ if( $this->isDir ) return 'fa-folder';
- return match (true) {
- $this->isType('application/pdf') => 'fa-file-pdf',
- $this->isCategory(MediaTypeSniffer::CAT_ARCHIVE) => 'fa-file-archive',
- $this->isCategory(MediaTypeSniffer::CAT_IMAGE) => 'fa-file-image',
- $this->isCategory(MediaTypeSniffer::CAT_AUDIO) => 'fa-file-audio',
- $this->isCategory(MediaTypeSniffer::CAT_VIDEO) => 'fa-file-video',
- $this->isCategory(MediaTypeSniffer::CAT_TEXT) => 'fa-file-code',
- default => 'fa-file',
+ return match ( true ) {
+ $this->isType( 'application/pdf' ) => 'fa-file-pdf',
+ $this->isCategory( MediaTypeSniffer::CAT_ARCHIVE ) => 'fa-file-archive',
+ $this->isCategory( MediaTypeSniffer::CAT_IMAGE ) => 'fa-file-image',
+ $this->isCategory( MediaTypeSniffer::CAT_AUDIO ) => 'fa-file-audio',
+ $this->isCategory( MediaTypeSniffer::CAT_VIDEO ) => 'fa-file-video',
+ $this->isCategory( MediaTypeSniffer::CAT_TEXT ) => 'fa-file-code',
+ default => 'fa-file',
};
}
private function getFormattedSize(): string {
- if ($this->size <= 0) return '0 B';
+ if( $this->size <= 0 ) return '0 B';
$units = ['B', 'KB', 'MB', 'GB'];
- $i = (int)floor(log($this->size, 1024));
- return round($this->size / pow(1024, $i), 1) . ' ' . $units[$i];
+ $i = (int)floor( log( $this->size, 1024 ) );
+ return round( $this->size / pow( 1024, $i ), 1 ) . ' ' . $units[$i];
}
- public function isType(string $type): bool {
- return str_contains(MediaTypeSniffer::isMediaType($this->getSniffBuffer(), $this->name), $type);
+ public function isType( string $type ): bool {
+ return str_contains( MediaTypeSniffer::isMediaType( $this->getSniffBuffer(), $this->name ), $type );
}
- public function isCategory(string $category): bool {
- return MediaTypeSniffer::isCategory($this->getSniffBuffer(), $this->name) === $category;
+ public function isCategory( string $category ): bool {
+ return MediaTypeSniffer::isCategory( $this->getSniffBuffer(), $this->name ) === $category;
}
public function isBinary(): bool {
- return MediaTypeSniffer::isBinary($this->getSniffBuffer(), $this->name);
+ return MediaTypeSniffer::isBinary( $this->getSniffBuffer(), $this->name );
}
private function getSniffBuffer(): string {
- if ($this->isDir || !file_exists($this->name)) return '';
- $handle = @fopen($this->name, 'rb');
- if (!$handle) return '';
- $read = fread($handle, 12);
- fclose($handle);
- return ($read !== false) ? $read : '';
+ if( $this->isDir ) return '';
+
+ if( $this->git ) {
+ return $this->git->peek( $this->sha );
+ }
+
+ if( !file_exists( $this->name ) ) return '';
+ $handle = @fopen( $this->name, 'rb' );
+ if( !$handle ) return '';
+ $read = fread( $handle, 12 );
+ fclose( $handle );
+ return ( $read !== false ) ? $read : '';
}
}
git/Git.php
class Git {
- private const CHUNK_SIZE = 128;
+ private const CHUNK_SIZE = 128;
private const MAX_READ_SIZE = 1048576;
public function eachTag( callable $callback ): void {
- $this->refs->scanRefs( 'refs/tags', function($name, $sha) use ($callback) {
- $data = $this->read($sha);
+ $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use ( $callback ) {
+ $data = $this->read( $sha );
$targetSha = $sha;
$timestamp = 0;
$message = '';
$author = '';
- // Determine if Annotated Tag or Lightweight Tag
- if (strncmp($data, 'object ', 7) === 0) {
- // Annotated Tag
- if (preg_match('/^object ([0-9a-f]{40})$/m', $data, $m)) {
- $targetSha = $m[1];
+ if( strncmp( $data, 'object ', 7 ) === 0 ) {
+ if( preg_match( '/^object ([0-9a-f]{40})$/m', $data, $m ) ) {
+ $targetSha = $m[1];
}
- if (preg_match('/^tagger (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) {
- $author = trim($m[1]);
- $timestamp = (int)$m[2];
+ if( preg_match( '/^tagger (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m ) ) {
+ $author = trim( $m[1] );
+ $timestamp = (int)$m[2];
}
- $pos = strpos($data, "\n\n");
- if ($pos !== false) {
- $message = trim(substr($data, $pos + 2));
+ $pos = strpos( $data, "\n\n" );
+ if( $pos !== false ) {
+ $message = trim( substr( $data, $pos + 2 ) );
}
} else {
- // Lightweight Tag (points directly to commit)
- // We parse the commit data to get date/author
- if (preg_match('/^author (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m)) {
- $author = trim($m[1]);
- $timestamp = (int)$m[2];
+ if( preg_match( '/^author (.*) <.*> (\d+) [+\-]\d{4}$/m', $data, $m ) ) {
+ $author = trim( $m[1] );
+ $timestamp = (int)$m[2];
}
- $pos = strpos($data, "\n\n");
- if ($pos !== false) {
- $message = trim(substr($data, $pos + 2));
+ $pos = strpos( $data, "\n\n" );
+ if( $pos !== false ) {
+ $message = trim( substr( $data, $pos + 2 ) );
}
}
- $callback(new Tag(
+ $callback( new Tag(
$name,
$sha,
$targetSha,
$timestamp,
$message,
$author
- ));
- });
+ ) );
+ } );
}
return $this->getLooseObjectSize( $sha );
+ }
+
+ public function peek( string $sha, int $length = 12 ): string {
+ $size = $this->packs->getSize( $sha );
+
+ if( $size === null ) {
+ return $this->peekLooseObject( $sha, $length );
+ }
+
+ return $this->packs->peek( $sha, $length ) ?? '';
}
}
- // Try streaming from pack file first (supports large files)
if( method_exists( $this->packs, 'stream' ) ) {
$streamed = $this->packs->stream( $sha, $callback );
if( $streamed ) {
return;
}
}
- // Fallback to read method (limited size)
$data = $this->packs->read( $sha );
if( $data !== null && $data !== '' ) {
$callback( $data );
+ }
+ }
+
+ private function peekLooseObject( string $sha, int $length ): string {
+ $path = $this->getLoosePath( $sha );
+
+ if( !is_file( $path ) ) {
+ return '';
+ }
+
+ $fileHandle = @fopen( $path, 'rb' );
+
+ if( !$fileHandle ) {
+ return '';
+ }
+
+ $inflator = inflate_init( ZLIB_ENCODING_DEFLATE );
+ $headerFound = false;
+ $buffer = '';
+
+ while( !feof( $fileHandle ) && strlen( $buffer ) < $length ) {
+ $chunk = fread( $fileHandle, 128 );
+ $inflated = @inflate_add( $inflator, $chunk );
+
+ if( !$headerFound ) {
+ $raw = $inflated;
+ $nullPos = strpos( $raw, "\0" );
+
+ if( $nullPos !== false ) {
+ $headerFound = true;
+ $buffer .= substr( $raw, $nullPos + 1 );
+ }
+ } else {
+ $buffer .= $inflated;
+ }
}
+
+ fclose( $fileHandle );
+
+ return substr( $buffer, 0, $length );
}
$size = $isDirectory ? 0 : $this->getObjectSize( $sha );
- $callback( new File( $name, $sha, $mode, 0, $size ) );
+ $callback( new File( $this, $name, $sha, $mode, 0, $size ) );
$position = $nullPos + 21;
$nullPos = strpos( $data, "\0" );
- return $nullPos !== false && ($nullPos + 21 <= strlen( $data ));
+ return $nullPos !== false && ( $nullPos + 21 <= strlen( $data ) );
}
git/GitDiff.php
private const MAX_DIFF_SIZE = 1048576;
- public function __construct(Git $git) {
+ public function __construct( Git $git ) {
$this->git = $git;
}
- public function compare(string $commitHash) {
- $commitData = $this->git->read($commitHash);
+ public function compare( string $commitHash ) {
+ $commitData = $this->git->read( $commitHash );
$parentHash = '';
- if (preg_match('/^parent ([0-9a-f]{40})/m', $commitData, $matches)) {
+ if( preg_match( '/^parent ([0-9a-f]{40})/m', $commitData, $matches ) ) {
$parentHash = $matches[1];
}
- $newTree = $this->getTreeHash($commitHash);
- $oldTree = $parentHash ? $this->getTreeHash($parentHash) : null;
+ $newTree = $this->getTreeHash( $commitHash );
+ $oldTree = $parentHash ? $this->getTreeHash( $parentHash ) : null;
- return $this->diffTrees($oldTree, $newTree);
+ return $this->diffTrees( $oldTree, $newTree );
}
- private function getTreeHash($commitSha) {
- $data = $this->git->read($commitSha);
- if (preg_match('/^tree ([0-9a-f]{40})/m', $data, $matches)) {
+ private function getTreeHash( $commitSha ) {
+ $data = $this->git->read( $commitSha );
+ if( preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
return $matches[1];
}
return null;
}
- private function diffTrees($oldTreeSha, $newTreeSha, $path = '') {
+ private function diffTrees( $oldTreeSha, $newTreeSha, $path = '' ) {
$changes = [];
- if ($oldTreeSha === $newTreeSha) return [];
+ if( $oldTreeSha === $newTreeSha ) return [];
- $oldEntries = $oldTreeSha ? $this->parseTree($oldTreeSha) : [];
- $newEntries = $newTreeSha ? $this->parseTree($newTreeSha) : [];
+ $oldEntries = $oldTreeSha ? $this->parseTree( $oldTreeSha ) : [];
+ $newEntries = $newTreeSha ? $this->parseTree( $newTreeSha ) : [];
- $allNames = array_unique(array_merge(array_keys($oldEntries), array_keys($newEntries)));
- sort($allNames);
+ $allNames = array_unique( array_merge( array_keys( $oldEntries ), array_keys( $newEntries ) ) );
+ sort( $allNames );
- foreach ($allNames as $name) {
- $old = $oldEntries[$name] ?? null;
- $new = $newEntries[$name] ?? null;
+ foreach( $allNames as $name ) {
+ $old = $oldEntries[$name] ?? null;
+ $new = $newEntries[$name] ?? null;
$currentPath = $path ? "$path/$name" : $name;
- if (!$old) {
- if ($new['is_dir']) {
- $changes = array_merge($changes, $this->diffTrees(null, $new['sha'], $currentPath));
+ if( !$old ) {
+ if( $new['is_dir'] ) {
+ $changes = array_merge( $changes, $this->diffTrees( null, $new['sha'], $currentPath ) );
} else {
- $changes[] = $this->createChange('A', $currentPath, null, $new['sha']);
+ $changes[] = $this->createChange( 'A', $currentPath, null, $new['sha'] );
}
- } elseif (!$new) {
- if ($old['is_dir']) {
- $changes = array_merge($changes, $this->diffTrees($old['sha'], null, $currentPath));
+ } elseif( !$new ) {
+ if( $old['is_dir'] ) {
+ $changes = array_merge( $changes, $this->diffTrees( $old['sha'], null, $currentPath ) );
} else {
- $changes[] = $this->createChange('D', $currentPath, $old['sha'], null);
+ $changes[] = $this->createChange( 'D', $currentPath, $old['sha'], null );
}
- } elseif ($old['sha'] !== $new['sha']) {
- if ($old['is_dir'] && $new['is_dir']) {
- $changes = array_merge($changes, $this->diffTrees($old['sha'], $new['sha'], $currentPath));
- } elseif (!$old['is_dir'] && !$new['is_dir']) {
- $changes[] = $this->createChange('M', $currentPath, $old['sha'], $new['sha']);
+ } elseif( $old['sha'] !== $new['sha'] ) {
+ if( $old['is_dir'] && $new['is_dir'] ) {
+ $changes = array_merge( $changes, $this->diffTrees( $old['sha'], $new['sha'], $currentPath ) );
+ } elseif( !$old['is_dir'] && !$new['is_dir'] ) {
+ $changes[] = $this->createChange( 'M', $currentPath, $old['sha'], $new['sha'] );
}
}
}
return $changes;
}
- private function parseTree($sha) {
- $data = $this->git->read($sha);
+ private function parseTree( $sha ) {
+ $data = $this->git->read( $sha );
$entries = [];
- $len = strlen($data);
- $pos = 0;
+ $len = strlen( $data );
+ $pos = 0;
- while ($pos < $len) {
- $space = strpos($data, ' ', $pos);
- $null = strpos($data, "\0", $space);
+ while( $pos < $len ) {
+ $space = strpos( $data, ' ', $pos );
+ $null = strpos( $data, "\0", $space );
- if ($space === false || $null === false) break;
+ if( $space === false || $null === false ) break;
- $mode = substr($data, $pos, $space - $pos);
- $name = substr($data, $space + 1, $null - $space - 1);
- $hash = bin2hex(substr($data, $null + 1, 20));
+ $mode = substr( $data, $pos, $space - $pos );
+ $name = substr( $data, $space + 1, $null - $space - 1 );
+ $hash = bin2hex( substr( $data, $null + 1, 20 ) );
$entries[$name] = [
- 'mode' => $mode,
- 'sha' => $hash,
- 'is_dir' => ($mode === '40000' || $mode === '040000')
+ 'mode' => $mode,
+ 'sha' => $hash,
+ 'is_dir' => ( $mode === '40000' || $mode === '040000' )
];
$pos = $null + 21;
}
return $entries;
}
- private function createChange($type, $path, $oldSha, $newSha) {
- // Check file sizes before reading content to prevent OOM
- $oldSize = $oldSha ? $this->git->getObjectSize($oldSha) : 0;
- $newSize = $newSha ? $this->git->getObjectSize($newSha) : 0;
+ private function createChange( $type, $path, $oldSha, $newSha ) {
+ $oldSize = $oldSha ? $this->git->getObjectSize( $oldSha ) : 0;
+ $newSize = $newSha ? $this->git->getObjectSize( $newSha ) : 0;
- // If file is too large, skip diffing and treat as binary
- if ($oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE) {
+ if( $oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE ) {
return [
- 'type' => $type,
- 'path' => $path,
+ 'type' => $type,
+ 'path' => $path,
'is_binary' => true,
- 'hunks' => []
+ 'hunks' => []
];
}
- $oldContent = $oldSha ? $this->git->read($oldSha) : '';
- $newContent = $newSha ? $this->git->read($newSha) : '';
+ $oldContent = $oldSha ? $this->git->read( $oldSha ) : '';
+ $newContent = $newSha ? $this->git->read( $newSha ) : '';
$isBinary = false;
- if ($newSha) {
- $f = new VirtualDiffFile($path, $newContent);
- if ($f->isBinary()) $isBinary = true;
+ if( $newSha ) {
+ $f = new VirtualDiffFile( $path, $newContent );
+ if( $f->isBinary() ) $isBinary = true;
}
- if (!$isBinary && $oldSha) {
- $f = new VirtualDiffFile($path, $oldContent);
- if ($f->isBinary()) $isBinary = true;
+ if( !$isBinary && $oldSha ) {
+ $f = new VirtualDiffFile( $path, $oldContent );
+ if( $f->isBinary() ) $isBinary = true;
}
$diff = null;
- if (!$isBinary) {
- $diff = $this->calculateDiff($oldContent, $newContent);
+ if( !$isBinary ) {
+ $diff = $this->calculateDiff( $oldContent, $newContent );
}
return [
- 'type' => $type,
- 'path' => $path,
+ 'type' => $type,
+ 'path' => $path,
'is_binary' => $isBinary,
- 'hunks' => $diff
+ 'hunks' => $diff
];
}
- private function calculateDiff($old, $new) {
- // Normalize line endings
- $old = str_replace("\r\n", "\n", $old);
- $new = str_replace("\r\n", "\n", $new);
+ private function calculateDiff( $old, $new ) {
+ $old = str_replace( "\r\n", "\n", $old );
+ $new = str_replace( "\r\n", "\n", $new );
- $oldLines = explode("\n", $old);
- $newLines = explode("\n", $new);
+ $oldLines = explode( "\n", $old );
+ $newLines = explode( "\n", $new );
- $m = count($oldLines);
- $n = count($newLines);
+ $m = count( $oldLines );
+ $n = count( $newLines );
- // LCS Algorithm Optimization: Trim matching start/end
$start = 0;
- while ($start < $m && $start < $n && $oldLines[$start] === $newLines[$start]) {
+ while( $start < $m && $start < $n && $oldLines[$start] === $newLines[$start] ) {
$start++;
}
$end = 0;
- while ($m - $end > $start && $n - $end > $start && $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end]) {
+ while( $m - $end > $start && $n - $end > $start && $oldLines[$m - 1 - $end] === $newLines[$n - 1 - $end] ) {
$end++;
}
- $oldSlice = array_slice($oldLines, $start, $m - $start - $end);
- $newSlice = array_slice($newLines, $start, $n - $start - $end);
+ $oldSlice = array_slice( $oldLines, $start, $m - $start - $end );
+ $newSlice = array_slice( $newLines, $start, $n - $start - $end );
- $cntOld = count($oldSlice);
- $cntNew = count($newSlice);
+ $cntOld = count( $oldSlice );
+ $cntNew = count( $newSlice );
- if (($cntOld * $cntNew) > 500000) {
- return [['t' => 'gap']];
+ if( ( $cntOld * $cntNew ) > 500000 ) {
+ return [['t' => 'gap']];
}
- $ops = $this->computeLCS($oldSlice, $newSlice);
+ $ops = $this->computeLCS( $oldSlice, $newSlice );
$groupedOps = [];
- $bufferDel = [];
- $bufferAdd = [];
+ $bufferDel = [];
+ $bufferAdd = [];
- foreach ($ops as $op) {
- if ($op['t'] === ' ') {
- foreach ($bufferDel as $o) $groupedOps[] = $o;
- foreach ($bufferAdd as $o) $groupedOps[] = $o;
- $bufferDel = [];
- $bufferAdd = [];
- $groupedOps[] = $op;
- } elseif ($op['t'] === '-') {
- $bufferDel[] = $op;
- } elseif ($op['t'] === '+') {
- $bufferAdd[] = $op;
- }
+ foreach( $ops as $op ) {
+ if( $op['t'] === ' ' ) {
+ foreach( $bufferDel as $o ) $groupedOps[] = $o;
+ foreach( $bufferAdd as $o ) $groupedOps[] = $o;
+ $bufferDel = [];
+ $bufferAdd = [];
+ $groupedOps[] = $op;
+ } elseif( $op['t'] === '-' ) {
+ $bufferDel[] = $op;
+ } elseif( $op['t'] === '+' ) {
+ $bufferAdd[] = $op;
+ }
}
- foreach ($bufferDel as $o) $groupedOps[] = $o;
- foreach ($bufferAdd as $o) $groupedOps[] = $o;
+ foreach( $bufferDel as $o ) $groupedOps[] = $o;
+ foreach( $bufferAdd as $o ) $groupedOps[] = $o;
$ops = $groupedOps;
- // Generate Stream with Context
$stream = [];
- // Prefix context
- for ($i = 0; $i < $start; $i++) {
- $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $i + 1, 'nn' => $i + 1];
+ for( $i = 0; $i < $start; $i++ ) {
+ $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $i + 1, 'nn' => $i + 1];
}
$currO = $start + 1;
$currN = $start + 1;
- foreach ($ops as $op) {
- if ($op['t'] === ' ') {
- $stream[] = ['t' => ' ', 'l' => $op['l'], 'no' => $currO++, 'nn' => $currN++];
- } elseif ($op['t'] === '-') {
- $stream[] = ['t' => '-', 'l' => $op['l'], 'no' => $currO++, 'nn' => null];
- } elseif ($op['t'] === '+') {
- $stream[] = ['t' => '+', 'l' => $op['l'], 'no' => null, 'nn' => $currN++];
- }
+ foreach( $ops as $op ) {
+ if( $op['t'] === ' ' ) {
+ $stream[] = ['t' => ' ', 'l' => $op['l'], 'no' => $currO++, 'nn' => $currN++];
+ } elseif( $op['t'] === '-' ) {
+ $stream[] = ['t' => '-', 'l' => $op['l'], 'no' => $currO++, 'nn' => null];
+ } elseif( $op['t'] === '+' ) {
+ $stream[] = ['t' => '+', 'l' => $op['l'], 'no' => null, 'nn' => $currN++];
+ }
}
- // Suffix context
- for ($i = $m - $end; $i < $m; $i++) {
- $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $currO++, 'nn' => $currN++];
+ for( $i = $m - $end; $i < $m; $i++ ) {
+ $stream[] = ['t' => ' ', 'l' => $oldLines[$i], 'no' => $currO++, 'nn' => $currN++];
}
- // Filter to Hunks
- $finalLines = [];
+ $finalLines = [];
$lastVisibleIndex = -1;
- $streamLen = count($stream);
- $contextLines = 3;
+ $streamLen = count( $stream );
+ $contextLines = 3;
- for ($i = 0; $i < $streamLen; $i++) {
- $show = false;
+ for( $i = 0; $i < $streamLen; $i++ ) {
+ $show = false;
- if ($stream[$i]['t'] !== ' ') {
+ if( $stream[$i]['t'] !== ' ' ) {
+ $show = true;
+ } else {
+ for( $j = 1; $j <= $contextLines; $j++ ) {
+ if( ( $i + $j ) < $streamLen && $stream[$i + $j]['t'] !== ' ' ) {
$show = true;
- } else {
- // Check ahead
- for ($j = 1; $j <= $contextLines; $j++) {
- if (($i + $j) < $streamLen && $stream[$i + $j]['t'] !== ' ') {
- $show = true;
- break;
- }
- }
- // Check behind
- if (!$show) {
- for ($j = 1; $j <= $contextLines; $j++) {
- if (($i - $j) >= 0 && $stream[$i - $j]['t'] !== ' ') {
- $show = true;
- break;
- }
- }
+ break;
+ }
+ }
+ if( !$show ) {
+ for( $j = 1; $j <= $contextLines; $j++ ) {
+ if( ( $i - $j ) >= 0 && $stream[$i - $j]['t'] !== ' ' ) {
+ $show = true;
+ break;
}
+ }
}
+ }
- if ($show) {
- if ($lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1) {
- $finalLines[] = ['t' => 'gap'];
- }
- $finalLines[] = $stream[$i];
- $lastVisibleIndex = $i;
+ if( $show ) {
+ if( $lastVisibleIndex !== -1 && $i > $lastVisibleIndex + 1 ) {
+ $finalLines[] = ['t' => 'gap'];
}
+ $finalLines[] = $stream[$i];
+ $lastVisibleIndex = $i;
+ }
}
return $finalLines;
}
- private function computeLCS($old, $new) {
- $m = count($old);
- $n = count($new);
- $c = array_fill(0, $m + 1, array_fill(0, $n + 1, 0));
+ private function computeLCS( $old, $new ) {
+ $m = count( $old );
+ $n = count( $new );
+ $c = array_fill( 0, $m + 1, array_fill( 0, $n + 1, 0 ) );
- for ($i = 1; $i <= $m; $i++) {
- for ($j = 1; $j <= $n; $j++) {
- if ($old[$i-1] === $new[$j-1]) {
- $c[$i][$j] = $c[$i-1][$j-1] + 1;
+ for( $i = 1; $i <= $m; $i++ ) {
+ for( $j = 1; $j <= $n; $j++ ) {
+ if( $old[$i - 1] === $new[$j - 1] ) {
+ $c[$i][$j] = $c[$i - 1][$j - 1] + 1;
} else {
- $c[$i][$j] = max($c[$i][$j-1], $c[$i-1][$j]);
+ $c[$i][$j] = max( $c[$i][$j - 1], $c[$i - 1][$j] );
}
}
}
$diff = [];
- $i = $m; $j = $n;
- while ($i > 0 || $j > 0) {
- if ($i > 0 && $j > 0 && $old[$i-1] === $new[$j-1]) {
- array_unshift($diff, ['t' => ' ', 'l' => $old[$i-1]]);
- $i--; $j--;
- } elseif ($j > 0 && ($i === 0 || $c[$i][$j-1] >= $c[$i-1][$j])) {
- array_unshift($diff, ['t' => '+', 'l' => $new[$j-1]]);
+ $i = $m;
+ $j = $n;
+ while( $i > 0 || $j > 0 ) {
+ if( $i > 0 && $j > 0 && $old[$i - 1] === $new[$j - 1] ) {
+ array_unshift( $diff, ['t' => ' ', 'l' => $old[$i - 1]] );
+ $i--;
$j--;
- } elseif ($i > 0 && ($j === 0 || $c[$i][$j-1] < $c[$i-1][$j])) {
- array_unshift($diff, ['t' => '-', 'l' => $old[$i-1]]);
+ } elseif( $j > 0 && ( $i === 0 || $c[$i][$j - 1] >= $c[$i - 1][$j] ) ) {
+ array_unshift( $diff, ['t' => '+', 'l' => $new[$j - 1]] );
+ $j--;
+ } elseif( $i > 0 && ( $j === 0 || $c[$i][$j - 1] < $c[$i - 1][$j] ) ) {
+ array_unshift( $diff, ['t' => '-', 'l' => $old[$i - 1]] );
$i--;
}
private $vName;
- public function __construct($name, $content) {
- parent::__construct($name, '', '100644', 0, strlen($content));
- $this->vName = $name;
+ public function __construct( $name, $content ) {
+ parent::__construct( null, $name, '', '100644', 0, strlen( $content ) );
+ $this->vName = $name;
$this->content = $content;
}
public function isBinary(): bool {
- $buffer = substr($this->content, 0, 12);
- return MediaTypeSniffer::isBinary($buffer, $this->vName);
+ $buffer = substr( $this->content, 0, 12 );
+ return MediaTypeSniffer::isBinary( $buffer, $this->vName );
}
}
git/GitPacks.php
}
- 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++] );
+ public function peek( string $sha, int $len = 12 ): ?string {
+ $info = $this->findPackInfo( $sha );
+
+ if( $info['offset'] === -1 ) {
+ return null;
+ }
+
+ $handle = $this->getHandle( $info['file'] );
+
+ if( !$handle ) {
+ return null;
+ }
+
+ return $this->readPackEntry( $handle, $info['offset'], $len, $len );
+ }
+
+ 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, int $cap = 0 ): string {
+ fseek( $fileHandle, $offset );
+
+ $header = $this->readVarInt( $fileHandle );
+ $type = ($header['byte'] >> 4) & 7;
+
+ if( $type === 6 ) {
+ return $this->handleOfsDelta( $fileHandle, $offset, $expectedSize, $cap );
+ }
+
+ if( $type === 7 ) {
+ return $this->handleRefDelta( $fileHandle, $expectedSize, $cap );
+ }
+
+ return $this->decompressToString( $fileHandle, $expectedSize, $cap );
+ }
+
+ 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;
+ }
+
+ private function decompressToString( $fileHandle, int $maxSize, int $cap = 0 ): 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( $cap > 0 && strlen( $result ) >= $cap ) {
+ return substr( $result, 0, $cap );
+ }
+
+ 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, int $cap = 0 ): 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, $cap );
+ }
+
+ private function handleRefDelta( $fileHandle, int $expectedSize, int $cap = 0 ): 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, $cap );
+ }
+
+ private function applyDelta( string $base, string $delta, int $cap = 0 ): string {
+ $position = 0;
+ $this->skipSize( $delta, $position );
+ $this->skipSize( $delta, $position );
+
+ $output = '';
+ $deltaLength = strlen( $delta );
+
+ while( $position < $deltaLength ) {
+ if( $cap > 0 && strlen( $output ) >= $cap ) {
+ break;
+ }
+
+ $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) ) {
+ }
+ }
+
+ 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;
Delta798 lines added, 735 lines removed, 63-line increase