Dave Jarvis' Repositories

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

Reformats code, fixes error

AuthorDave Jarvis <email>
Date2026-02-09 12:15:09 GMT-0800
Commit18f217aa1a6eb5bfcad6416144ccb458ec1c928a
Parent7884e54
Git.php
class Git {
- private string $path;
- private string $objPath;
-
- public function __construct(string $repoPath) {
- $this->path = rtrim($repoPath, '/');
- $this->objPath = $this->path . '/objects';
- }
-
- public function getObjectSize(string $sha): int {
- $loose = "{$this->objPath}/" . substr($sha, 0, 2) . "/" . substr($sha, 2);
- if (file_exists($loose)) {
- $f = @fopen($loose, 'rb');
- if (!$f) return 0;
- $ctx = inflate_init(ZLIB_ENCODING_DEFLATE);
- $data = '';
- while (!feof($f)) {
- $chunk = fread($f, 128);
- $inflated = @inflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
- if ($inflated === false) break;
- $data .= $inflated;
- if (strpos($data, "\0") !== false) break;
- }
- fclose($f);
- $header = explode("\0", $data, 2)[0];
- $parts = explode(' ', $header);
- return isset($parts[1]) ? (int)$parts[1] : 0;
- }
- return $this->getPackedObjectSize($sha);
- }
-
-private function getPackedObjectSize(string $sha): int {
- $info = $this->getPackOffset($sha);
- if (!$info) return 0;
-
- $pf = @fopen($info['file'], 'rb');
- if (!$pf) return 0;
-
- fseek($pf, $info['offset']);
- $byte = ord(fread($pf, 1));
- $type = ($byte >> 4) & 7;
- $size = $byte & 15;
- $shift = 4;
-
- while ($byte & 128) {
- $byte = ord(fread($pf, 1));
- $size |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- // If the object is not a delta, the size we just read is the final size.
- if ($type !== 6 && $type !== 7) {
- fclose($pf);
- return $size;
- }
-
- // For Deltas (Type 6 or 7), the 'size' above is the size of the delta data,
- // not the resulting file. We must find the "Target Size" inside the delta header.
- if ($type === 6) { // OFS_DELTA: skip the negative offset
- $byte = ord(fread($pf, 1));
- while ($byte & 128) { $byte = ord(fread($pf, 1)); }
- } else { // REF_DELTA: skip the 20-byte base SHA
- fread($pf, 20);
- }
-
- // Inflate only the beginning of the delta data to get the header
- $ctx = inflate_init(ZLIB_ENCODING_DEFLATE);
- $headerData = '';
- while (!feof($pf)) {
- $chunk = fread($pf, 512);
- $inflated = @inflate_add($ctx, $chunk, ZLIB_NO_FLUSH);
- if ($inflated !== false) $headerData .= $inflated;
- if (strlen($headerData) >= 32) break; // We usually only need ~10 bytes
- }
- fclose($pf);
-
- if (strlen($headerData) === 0) return 0;
-
- $pos = 0;
- // 1. Skip Source Size (Base Object Size)
- $byte = ord($headerData[$pos++]);
- while ($byte & 128 && $pos < strlen($headerData)) {
- $byte = ord($headerData[$pos++]);
- }
-
- // 2. Read Target Size (The actual file size we want to display)
- if ($pos >= strlen($headerData)) return 0;
- $byte = ord($headerData[$pos++]);
- $targetSize = $byte & 127;
- $shift = 7;
- while ($byte & 128 && $pos < strlen($headerData)) {
- $byte = ord($headerData[$pos++]);
- $targetSize |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- return $targetSize;
- }
-
- public function getMainBranch(): ?array {
- $branches = [];
- $this->eachBranch(function($name, $sha) use (&$branches) { $branches[$name] = $sha; });
- foreach (['main', 'master', 'trunk', 'develop'] as $b) {
- if (isset($branches[$b])) return ['name' => $b, 'hash' => $branches[$b]];
- }
- if (!empty($branches)) {
- $f = array_key_first($branches);
- return ['name' => $f, 'hash' => $branches[$f]];
- }
- return null;
- }
-
- public function eachBranch(callable $callback): void { $this->scanRefs('refs/heads', $callback); }
- public function eachTag(callable $callback): void { $this->scanRefs('refs/tags', $callback); }
-
- public function walk(string $refOrSha, callable $callback): void {
- $sha = $this->resolve($refOrSha);
- if (!$sha) return;
- $data = $this->read($sha);
- if (!$data) return;
- if (preg_match('/^tree ([0-9a-f]{40})$/m', $data, $m)) {
- $data = $this->read($m[1]);
- if (!$data) return;
- } elseif (!$this->isTreeData($data)) return;
-
- $pos = 0;
- while ($pos < strlen($data)) {
- $space = strpos($data, ' ', $pos);
- $null = strpos($data, "\0", $space);
- if ($space === false || $null === false) break;
- $mode = substr($data, $pos, $space - $pos);
- $name = substr($data, $space + 1, $null - $space - 1);
- $entrySha = bin2hex(substr($data, $null + 1, 20));
-
- $isDir = ($mode === '40000' || $mode === '040000');
- $size = $isDir ? 0 : $this->getObjectSize($entrySha);
-
- $callback(new File($name, $entrySha, $mode, 0, $size));
-
- $pos = $null + 21;
- }
- }
-
- private function isTreeData(string $data): bool {
- if (strlen($data) < 25) return false;
- if (preg_match('/^(40000|100644|100755|120000) /', $data)) {
- $null = strpos($data, "\0");
- return ($null !== false && ($null + 21 <= strlen($data)));
- }
- return false;
- }
-
- public function history(string $refOrSha, int $limit, callable $callback): void {
- $currentSha = $this->resolve($refOrSha);
- $count = 0;
- while ($currentSha && $count < $limit) {
- $data = $this->read($currentSha);
- if (!$data) break;
- $message = (strpos($data, "\n\n") !== false) ? substr($data, strpos($data, "\n\n") + 2) : '';
- preg_match('/^author (.*) <(.*)> (\d+)/m', $data, $auth);
- $callback((object)['sha' => $currentSha, 'message' => trim($message), 'author' => $auth[1] ?? 'Unknown', 'email' => $auth[2] ?? '', 'date' => (int)($auth[3] ?? 0)]);
- $currentSha = preg_match('/^parent ([0-9a-f]{40})$/m', $data, $m) ? $m[1] : null;
- $count++;
- }
- }
-
- public function stream(string $sha, callable $callback): void {
- $data = $this->read($sha);
- if ($data) $callback($data);
- }
-
- private function resolve(string $input): ?string {
- if (preg_match('/^[0-9a-f]{40}$/', $input)) return $input;
- if ($input === 'HEAD' && file_exists($h = "{$this->path}/HEAD")) {
- $head = trim(file_get_contents($h));
- return (strpos($head, 'ref: ') === 0) ? $this->resolve(substr($head, 5)) : $head;
- }
- foreach ([$input, "refs/heads/$input", "refs/tags/$input"] as $p) {
- if (file_exists($f = "{$this->path}/$p")) return trim(file_get_contents($f));
- }
- if (file_exists($packed = "{$this->path}/packed-refs")) {
- foreach (file($packed) as $line) {
- if ($line[0] === '#' || $line[0] === '^') continue;
- $parts = explode(' ', trim($line));
- if (count($parts) >= 2 && ($parts[1] === $input || $parts[1] === "refs/heads/$input" || $parts[1] === "refs/tags/$input")) return $parts[0];
- }
- }
- return null;
- }
-
- private function read(string $sha): ?string {
- $loose = "{$this->objPath}/" . substr($sha, 0, 2) . "/" . substr($sha, 2);
- if (file_exists($loose)) {
- $inflated = @gzuncompress(file_get_contents($loose));
- return $inflated ? explode("\0", $inflated, 2)[1] : null;
- }
- return $this->fromPack($sha);
- }
-
- private function scanRefs(string $prefix, callable $callback): void {
- $dir = "{$this->path}/$prefix";
- if (is_dir($dir)) {
- foreach (array_diff(scandir($dir), ['.', '..']) as $f) {
- $callback($f, trim(file_get_contents("$dir/$f")));
- }
- }
- }
-
- private function fromPack(string $sha): ?string {
- $info = $this->getPackOffset($sha);
- if (!$info) return null;
- $pf = @fopen($info['file'], 'rb');
- if (!$pf) return null;
- $data = $this->readPackEntry($pf, $info['offset']);
- fclose($pf);
- return $data;
- }
-
- private function readPackEntry($pf, int $offset): ?string {
- fseek($pf, $offset);
- $byte = ord(fread($pf, 1));
- $type = ($byte >> 4) & 7;
- $size = $byte & 15;
- $shift = 4;
- while ($byte & 128) {
- $byte = ord(fread($pf, 1));
- $size |= (($byte & 127) << $shift);
- $shift += 7;
- }
-
- // Type 6: OBJ_OFS_DELTA
- if ($type === 6) {
- $byte = ord(fread($pf, 1));
- $negOffset = $byte & 127;
- while ($byte & 128) {
- $byte = ord(fread($pf, 1));
- $negOffset = (($negOffset + 1) << 7) | ($byte & 127);
- }
- $baseOffset = $offset - $negOffset;
- $base = $this->readPackEntry($pf, $baseOffset);
-
- fseek($pf, $offset);
- $b = ord(fread($pf, 1));
- while ($b & 128) { $b = ord(fread($pf, 1)); }
- $b = ord(fread($pf, 1));
- while ($b & 128) { $b = ord(fread($pf, 1)); }
-
- $delta = @gzuncompress(fread($pf, 16777216));
- return $this->applyDelta($base, $delta);
- }
-
- // Type 7: OBJ_REF_DELTA
- if ($type === 7) {
- $baseSha = bin2hex(fread($pf, 20));
- $base = $this->read($baseSha);
- $delta = @gzuncompress(fread($pf, 16777216));
- return $this->applyDelta($base, $delta);
- }
-
- return @gzuncompress(fread($pf, 16777216));
- }
-
- private function applyDelta(?string $base, ?string $delta): string {
- if (!$base || !$delta) return '';
- $pos = 0;
- // Skip Source Size
- $byte = ord($delta[$pos++]);
- while ($byte & 128) { $byte = ord($delta[$pos++]); }
- // Skip Target Size
- $byte = ord($delta[$pos++]);
- while ($byte & 128) { $byte = ord($delta[$pos++]); }
-
- $out = '';
- while ($pos < strlen($delta)) {
- $opcode = ord($delta[$pos++]);
- if ($opcode & 128) { // Copy
- $off = 0; $len = 0;
- if ($opcode & 1) $off |= ord($delta[$pos++]);
- if ($opcode & 2) $off |= ord($delta[$pos++]) << 8;
- if ($opcode & 4) $off |= ord($delta[$pos++]) << 16;
- if ($opcode & 8) $off |= ord($delta[$pos++]) << 24;
- if ($opcode & 16) $len |= ord($delta[$pos++]);
- if ($opcode & 32) $len |= ord($delta[$pos++]) << 8;
- if ($opcode & 64) $len |= ord($delta[$pos++]) << 16;
- if ($len === 0) $len = 0x10000;
- $out .= substr($base, $off, $len);
- } else { // Insert
- $len = $opcode & 127;
- $out .= substr($delta, $pos, $len);
- $pos += $len;
- }
- }
- return $out;
- }
-
- private function getPackOffset(string $sha): ?array {
- $packs = glob("{$this->objPath}/pack/*.idx");
- if (!$packs) return null;
-
- $binSha = hex2bin($sha);
- $firstByte = ord($binSha[0]);
-
- foreach ($packs as $idxFile) {
- $f = @fopen($idxFile, 'rb');
- if (!$f) continue;
-
- $sig = fread($f, 4);
- $ver = unpack('N', fread($f, 4))[1];
- if ($sig !== "\377tOc" || $ver !== 2) { fclose($f); continue; }
-
- $fanoutOffset = 8;
- if ($firstByte > 0) {
- fseek($f, $fanoutOffset + (($firstByte - 1) * 4));
- $start = unpack('N', fread($f, 4))[1];
- } else {
- $start = 0;
- }
- fseek($f, $fanoutOffset + ($firstByte * 4));
- $end = unpack('N', fread($f, 4))[1];
-
- if ($end <= $start) { fclose($f); continue; }
-
- fseek($f, $fanoutOffset + (255 * 4));
- $totalObjects = unpack('N', fread($f, 4))[1];
-
- $shaTableOffset = 8 + 1024;
- fseek($f, $shaTableOffset + ($start * 20));
-
- $foundIdx = -1;
- for ($i = $start; $i < $end; $i++) {
- if (fread($f, 20) === $binSha) { $foundIdx = $i; break; }
- }
-
- if ($foundIdx === -1) { fclose($f); continue; }
-
- $crcOffset = $shaTableOffset + ($totalObjects * 20);
- $offsetTableOffset = $crcOffset + ($totalObjects * 4);
-
- fseek($f, $offsetTableOffset + ($foundIdx * 4));
- $offset32 = unpack('N', fread($f, 4))[1];
-
- if ($offset32 & 0x80000000) {
- $largeOffsetIdx = $offset32 & 0x7FFFFFFF;
- $largeOffsetTablePos = $offsetTableOffset + ($totalObjects * 4);
- fseek($f, $largeOffsetTablePos + ($largeOffsetIdx * 8));
- $data = unpack('J', fread($f, 8));
- $offset = $data[1];
- } else {
- $offset = $offset32;
- }
-
- fclose($f);
- return ['file' => str_replace('.idx', '.pack', $idxFile), 'offset' => $offset];
- }
- return null;
+ private const CHUNK_SIZE = 128;
+ private const MAX_READ = 16777216;
+ private const MODE_TREE = '40000';
+ private const MODE_TREE_A = '040000';
+
+ private string $path;
+ private string $objPath;
+
+ public function __construct( string $repoPath ) {
+ $this->path = rtrim( $repoPath, '/' );
+ $this->objPath = $this->path . '/objects';
+ }
+
+ public function getObjectSize( string $sha ): int {
+ $prefix = substr( $sha, 0, 2 );
+ $suffix = substr( $sha, 2 );
+ $loosePath = "{$this->objPath}/{$prefix}/{$suffix}";
+
+ $size = file_exists( $loosePath )
+ ? $this->getLooseObjectSize( $loosePath )
+ : $this->getPackedObjectSize( $sha );
+
+ return $size;
+ }
+
+ private function getLooseObjectSize( string $path ): int {
+ $size = 0;
+ $fileHandle = @fopen( $path, 'rb' );
+
+ if( $fileHandle ) {
+ $data = $this->decompressHeader( $fileHandle );
+ $header = explode( "\0", $data, 2 )[0];
+ $parts = explode( ' ', $header );
+ $size = isset( $parts[1] ) ? (int)$parts[1] : 0;
+ fclose( $fileHandle );
+ }
+
+ return $size;
+ }
+
+ private function decompressHeader( $fileHandle ): string {
+ $data = '';
+ $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );
+
+ while( !feof( $fileHandle ) ) {
+ $chunk = fread( $fileHandle, self::CHUNK_SIZE );
+ $inflated = @inflate_add( $inflateContext, $chunk, ZLIB_NO_FLUSH );
+
+ if( $inflated === false ) {
+ break;
+ }
+
+ $data .= $inflated;
+
+ if( strpos( $data, "\0" ) !== false ) {
+ break;
+ }
+ }
+
+ return $data;
+ }
+
+ private function getPackedObjectSize( string $sha ): int {
+ $info = $this->getPackOffset( $sha );
+
+ $size = ($info['offset'] !== -1)
+ ? $this->extractPackedSize( $info )
+ : 0;
+
+ return $size;
+ }
+
+ private function extractPackedSize( array $info ): int {
+ $targetSize = 0;
+ $packFile = @fopen( $info['file'], 'rb' );
+
+ if( $packFile ) {
+ fseek( $packFile, $info['offset'] );
+ $header = $this->readVarInt( $packFile );
+ $type = ($header['byte'] >> 4) & 7;
+
+ $targetSize = ($type === 6 || $type === 7)
+ ? $this->readDeltaTargetSize( $packFile, $type )
+ : $header['value'];
+
+ fclose( $packFile );
+ }
+
+ return $targetSize;
+ }
+
+ private function readVarInt( $fileHandle ): array {
+ $byte = ord( fread( $fileHandle, 1 ) );
+ $value = $byte & 15;
+ $shift = 4;
+ $firstByte = $byte;
+
+ while( $byte & 128 ) {
+ $byte = ord( fread( $fileHandle, 1 ) );
+ $value |= (($byte & 127) << $shift);
+ $shift += 7;
+ }
+
+ return ['value' => $value, 'byte' => $firstByte];
+ }
+
+ private function readDeltaTargetSize( $fileHandle, int $type ): int {
+ $dummy = ($type === 6)
+ ? $this->skipOffsetDelta( $fileHandle )
+ : fread( $fileHandle, 20 );
+
+ $inflateContext = inflate_init( ZLIB_ENCODING_DEFLATE );
+ $headerData = '';
+
+ while( !feof( $fileHandle ) && strlen( $headerData ) < 32 ) {
+ $inflated = @inflate_add(
+ $inflateContext,
+ fread( $fileHandle, 512 ),
+ ZLIB_NO_FLUSH
+ );
+
+ if( $inflated !== false ) {
+ $headerData .= $inflated;
+ }
+ }
+
+ $result = 0;
+ $position = 0;
+
+ if( strlen( $headerData ) > 0 ) {
+ $this->skipSize( $headerData, $position );
+ $result = $this->readSize( $headerData, $position );
+ }
+
+ return $result;
+ }
+
+ public function getMainBranch(): array {
+ $result = ['name' => '', 'hash' => ''];
+ $branches = [];
+ $this->eachBranch( function( $name, $sha ) use( &$branches ) {
+ $branches[$name] = $sha;
+ } );
+
+ foreach( ['main', 'master', 'trunk', 'develop'] as $branch ) {
+ if( isset( $branches[$branch] ) ) {
+ $result = ['name' => $branch, 'hash' => $branches[$branch]];
+ break;
+ }
+ }
+
+ if( $result['name'] === '' ) {
+ $firstKey = array_key_first( $branches );
+
+ if( $firstKey !== null ) {
+ $result = ['name' => $firstKey, 'hash' => $branches[$firstKey]];
+ }
+ }
+
+ return $result;
+ }
+
+ public function eachBranch( callable $callback ): void {
+ $this->scanRefs( 'refs/heads', $callback );
+ }
+
+ public function eachTag( callable $callback ): void {
+ $this->scanRefs( 'refs/tags', $callback );
+ }
+
+ public function walk( string $refOrSha, callable $callback ): void {
+ $sha = $this->resolve( $refOrSha );
+ $data = ($sha !== '') ? $this->read( $sha ) : '';
+
+ if( preg_match( '/^tree ([0-9a-f]{40})$/m', $data, $matches ) ) {
+ $data = $this->read( $matches[1] );
+ }
+
+ if( $this->isTreeData( $data ) ) {
+ $this->processTree( $data, $callback );
+ }
+ }
+
+ private function processTree( string $data, callable $callback ): void {
+ $position = 0;
+
+ while( $position < strlen( $data ) ) {
+ $spacePos = strpos( $data, ' ', $position );
+ $nullPos = strpos( $data, "\0", $spacePos );
+
+ if( $spacePos === false || $nullPos === false ) {
+ break;
+ }
+
+ $mode = substr( $data, $position, $spacePos - $position );
+ $name = substr( $data, $spacePos + 1, $nullPos - $spacePos - 1 );
+ $entrySha = bin2hex( substr( $data, $nullPos + 1, 20 ) );
+
+ $isDir = ($mode === self::MODE_TREE || $mode === self::MODE_TREE_A);
+ $size = $isDir ? 0 : $this->getObjectSize( $entrySha );
+
+ $callback( new File( $name, $entrySha, $mode, 0, $size ) );
+ $position = $nullPos + 21;
+ }
+ }
+
+ private function isTreeData( string $data ): bool {
+ $result = false;
+ $pattern = '/^(40000|100644|100755|120000) /';
+
+ if( strlen( $data ) >= 25 && preg_match( $pattern, $data ) ) {
+ $nullPos = strpos( $data, "\0" );
+ $result = ($nullPos !== false && ($nullPos + 21 <= strlen( $data )));
+ }
+
+ return $result;
+ }
+
+ public function history( string $ref, int $limit, callable $cb ): void {
+ $currentSha = $this->resolve( $ref );
+ $count = 0;
+
+ while( $currentSha !== '' && $count < $limit ) {
+ $data = $this->read( $currentSha );
+
+ if( $data === '' ) {
+ break;
+ }
+
+ $pos = strpos( $data, "\n\n" );
+ $message = ($pos !== false) ? substr( $data, $pos + 2 ) : '';
+ preg_match( '/^author (.*) <(.*)> (\d+)/m', $data, $m );
+
+ $cb( (object)[
+ 'sha' => $currentSha,
+ 'message' => trim( $message ),
+ 'author' => $m[1] ?? 'Unknown',
+ 'email' => $m[2] ?? '',
+ 'date' => (int)($m[3] ?? 0)
+ ] );
+
+ $currentSha = preg_match( '/^parent ([0-9a-f]{40})$/m', $data, $ms )
+ ? $ms[1] : '';
+ $count++;
+ }
+ }
+
+ public function stream( string $sha, callable $callback ): void {
+ $data = $this->read( $sha );
+
+ if( $data !== '' ) {
+ $callback( $data );
+ }
+ }
+
+ public function resolve( string $input ): string {
+ $result = '';
+
+ if( preg_match( '/^[0-9a-f]{40}$/', $input ) ) {
+ $result = $input;
+ } elseif( $input === 'HEAD' &&
+ file_exists( $headFile = "{$this->path}/HEAD" ) ) {
+ $head = trim( file_get_contents( $headFile ) );
+ $result = (strpos( $head, 'ref: ' ) === 0)
+ ? $this->resolve( substr( $head, 5 ) ) : $head;
+ } else {
+ $result = $this->resolveRef( $input );
+ }
+
+ return $result;
+ }
+
+ private function resolveRef( string $input ): string {
+ $found = '';
+ $refPaths = [$input, "refs/heads/$input", "refs/tags/$input"];
+
+ foreach( $refPaths as $path ) {
+ if( file_exists( $filePath = "{$this->path}/$path" ) ) {
+ $found = trim( file_get_contents( $filePath ) );
+ break;
+ }
+ }
+
+ if( $found === '' &&
+ file_exists( $packed = "{$this->path}/packed-refs" ) ) {
+ $found = $this->findInPackedRefs( $packed, $input );
+ }
+
+ return $found;
+ }
+
+ private function findInPackedRefs( string $path, string $input ): string {
+ $result = '';
+ $targets = [$input, "refs/heads/$input", "refs/tags/$input"];
+
+ foreach( file( $path ) as $line ) {
+ if( $line[0] === '#' || $line[0] === '^' ) {
+ continue;
+ }
+
+ $parts = explode( ' ', trim( $line ) );
+
+ if( count( $parts ) >= 2 && in_array( $parts[1], $targets ) ) {
+ $result = $parts[0];
+ break;
+ }
+ }
+
+ return $result;
+ }
+
+ public function read( string $sha ): string {
+ $result = '';
+ $prefix = substr( $sha, 0, 2 );
+ $suffix = substr( $sha, 2 );
+ $loose = "{$this->objPath}/{$prefix}/{$suffix}";
+
+ if( file_exists( $loose ) ) {
+ $raw = file_get_contents( $loose );
+ $inflated = $raw ? @gzuncompress( $raw ) : false;
+ $result = $inflated ? explode( "\0", $inflated, 2 )[1] : '';
+ } else {
+ $result = $this->fromPack( $sha );
+ }
+
+ return $result;
+ }
+
+ private function fromPack( string $sha ): string {
+ $info = $this->getPackOffset( $sha );
+ $result = '';
+
+ if( $info['offset'] !== -1 ) {
+ $packFile = @fopen( $info['file'], 'rb' );
+
+ if( $packFile ) {
+ $result = $this->readPackEntry( $packFile, $info['offset'] );
+ fclose( $packFile );
+ }
+ }
+
+ return $result;
+ }
+
+ private function getPackOffset( string $sha ): array {
+ $result = ['file' => '', 'offset' => -1];
+
+ if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
+ $binSha = hex2bin( $sha );
+ $packs = glob( "{$this->objPath}/pack/*.idx" );
+
+ foreach( (array)$packs as $idxFile ) {
+ $offset = $this->findInPack( $idxFile, $binSha );
+
+ if( $offset !== -1 ) {
+ $result = [
+ 'file' => str_replace( '.idx', '.pack', $idxFile ),
+ 'offset' => $offset
+ ];
+ break;
+ }
+ }
+ }
+
+ return $result;
+ }
+
+ private function findInPack( string $idxFile, string $binSha ): int {
+ $offset = -1;
+ $fileHandle = @fopen( $idxFile, 'rb' );
+
+ if( $fileHandle ) {
+ $range = $this->getFanoutRange( $fileHandle, ord( $binSha[0] ) );
+
+ if( $range['end'] > $range['start'] ) {
+ $total = $this->getTotalObjects( $fileHandle );
+ $foundIdx = $this->searchShaTable(
+ $fileHandle,
+ $range['start'],
+ $range['end'],
+ $binSha
+ );
+
+ if( $foundIdx !== -1 ) {
+ $offset = $this->getOffsetFromTable(
+ $fileHandle,
+ $foundIdx,
+ $total
+ );
+ }
+ }
+
+ fclose( $fileHandle );
+ }
+
+ return $offset;
+ }
+
+ private function getFanoutRange( $fileHandle, int $firstByte ): array {
+ $range = ['start' => 0, 'end' => 0];
+ fseek( $fileHandle, 0 );
+
+ if( fread( $fileHandle, 8 ) === "\377tOc\0\0\0\2" ) {
+ fseek( $fileHandle, 8 + ($firstByte * 4) );
+ $range['end'] = unpack( 'N', fread( $fileHandle, 4 ) )[1];
+
+ if( $firstByte > 0 ) {
+ fseek( $fileHandle, 8 + (($firstByte - 1) * 4) );
+ $range['start'] = unpack( 'N', fread( $fileHandle, 4 ) )[1];
+ }
+ }
+
+ return $range;
+ }
+
+ private function getTotalObjects( $fileHandle ): int {
+ fseek( $fileHandle, 1032 );
+
+ return unpack( 'N', fread( $fileHandle, 4 ) )[1];
+ }
+
+ private function searchShaTable(
+ $fileHandle,
+ int $start,
+ int $end,
+ string $binSha
+ ): int {
+ $result = -1;
+ fseek( $fileHandle, 1032 + ($start * 20) );
+
+ for( $i = $start; $i < $end; $i++ ) {
+ if( fread( $fileHandle, 20 ) === $binSha ) {
+ $result = $i;
+ break;
+ }
+ }
+
+ return $result;
+ }
+
+ private function getOffsetFromTable( $fileHandle, int $idx, int $total ): int {
+ $pos = 1032 + ($total * 20) + ($total * 4) + ($idx * 4);
+ fseek( $fileHandle, $pos );
+ $offset = unpack( 'N', fread( $fileHandle, 4 ) )[1];
+
+ if( $offset & 0x80000000 ) {
+ $base = 1032 + ($total * 24) + ($total * 4);
+ fseek( $fileHandle, $base + (($offset & 0x7FFFFFFF) * 8) );
+ $offset = unpack( 'J', fread( $fileHandle, 8 ) )[1];
+ }
+
+ return $offset;
+ }
+
+ private function readPackEntry( $fileHandle, int $offset ): string {
+ fseek( $fileHandle, $offset );
+ $header = $this->readVarInt( $fileHandle );
+ $type = ($header['byte'] >> 4) & 7;
+ $result = '';
+
+ if( $type === 6 ) {
+ $result = $this->handleOfsDelta( $fileHandle, $offset );
+ } elseif( $type === 7 ) {
+ $result = $this->handleRefDelta( $fileHandle );
+ } else {
+ $result = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';
+ }
+
+ return $result;
+ }
+
+ private function handleOfsDelta( $fileHandle, int $offset ): string {
+ $byte = ord( fread( $fileHandle, 1 ) );
+ $negOffset = $byte & 127;
+
+ while( $byte & 128 ) {
+ $byte = ord( fread( $fileHandle, 1 ) );
+ $negOffset = (($negOffset + 1) << 7) | ($byte & 127);
+ }
+
+ $base = $this->readPackEntry( $fileHandle, $offset - $negOffset );
+ $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';
+
+ return $this->applyDelta( $base, $delta );
+ }
+
+ private function handleRefDelta( $fileHandle ): string {
+ $base = $this->read( bin2hex( fread( $fileHandle, 20 ) ) );
+ $delta = @gzuncompress( fread( $fileHandle, self::MAX_READ ) ) ?: '';
+
+ return $this->applyDelta( $base, $delta );
+ }
+
+ private function applyDelta( string $base, string $delta ): string {
+ $out = '';
+
+ if( $base !== '' && $delta !== '' ) {
+ $position = 0;
+ $this->skipSize( $delta, $position );
+ $this->skipSize( $delta, $position );
+
+ while( $position < strlen( $delta ) ) {
+ $opcode = ord( $delta[$position++] );
+
+ if( $opcode & 128 ) {
+ $out .= $this->deltaCopy( $base, $delta, $position, $opcode );
+ } else {
+ $len = $opcode & 127;
+ $out .= substr( $delta, $position, $len );
+ $position += $len;
+ }
+ }
+ }
+
+ return $out;
+ }
+
+ private function deltaCopy(
+ string $base,
+ string $delta,
+ int &$position,
+ int $opcode
+ ): string {
+ $offset = 0;
+ $length = 0;
+
+ if( $opcode & 1 ) { $offset |= ord( $delta[$position++] ); }
+ if( $opcode & 2 ) { $offset |= ord( $delta[$position++] ) << 8; }
+ if( $opcode & 4 ) { $offset |= ord( $delta[$position++] ) << 16; }
+ if( $opcode & 8 ) { $offset |= ord( $delta[$position++] ) << 24; }
+ if( $opcode & 16 ) { $length |= ord( $delta[$position++] ); }
+ if( $opcode & 32 ) { $length |= ord( $delta[$position++] ) << 8; }
+ if( $opcode & 64 ) { $length |= ord( $delta[$position++] ) << 16; }
+
+ return substr( $base, $offset, ($length === 0 ? 0x10000 : $length) );
+ }
+
+ private function skipSize( string $data, int &$position ): void {
+ while( ord( $data[$position++] ) & 128 ) {
+ // Intentionally empty
+ }
+ }
+
+ 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 skipOffsetDelta( $fileHandle ): void {
+ $byte = ord( fread( $fileHandle, 1 ) );
+
+ while( $byte & 128 ) {
+ $byte = ord( fread( $fileHandle, 1 ) );
+ }
+ }
+
+ private function scanRefs( string $prefix, callable $callback ): void {
+ $directory = "{$this->path}/$prefix";
+
+ if( is_dir( $directory ) ) {
+ foreach( array_diff( scandir( $directory ), ['.', '..'] ) as $fileName ) {
+ $content = file_get_contents( "$directory/$fileName" );
+ $callback( $fileName, trim( $content ) );
+ }
+ }
}
}
Delta574 lines added, 354 lines removed, 220-line increase