| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-20 17:00:48 GMT-0800 |
| Commit | d9fb2528783253665cb0661dca4de03190ff608d |
| Parent | 2bee52a |
| +<?php | ||
| +class BufferedFileReader { | ||
| + private mixed $handle; | ||
| + private bool $temporary; | ||
| + | ||
| + private function __construct( mixed $handle, bool $temporary ) { | ||
| + $this->handle = $handle; | ||
| + $this->temporary = $temporary; | ||
| + } | ||
| + | ||
| + public static function open( string $path ): self { | ||
| + return new self( fopen( $path, 'rb' ), false ); | ||
| + } | ||
| + | ||
| + public static function createTemp(): self { | ||
| + return new self( tmpfile(), true ); | ||
| + } | ||
| + | ||
| + public function __destruct() { | ||
| + if( $this->isOpen() ) { | ||
| + fclose( $this->handle ); | ||
| + } | ||
| + } | ||
| + | ||
| + public function isOpen(): bool { | ||
| + return is_resource( $this->handle ); | ||
| + } | ||
| + | ||
| + public function read( int $length ): string { | ||
| + return $this->isOpen() && !feof( $this->handle ) | ||
| + ? (string)fread( $this->handle, $length ) | ||
| + : ''; | ||
| + } | ||
| + | ||
| + public function write( string $data ): bool { | ||
| + return $this->temporary && | ||
| + $this->isOpen() && | ||
| + fwrite( $this->handle, $data ) !== false; | ||
| + } | ||
| + | ||
| + public function seek( int $offset, int $whence = SEEK_SET ): bool { | ||
| + return $this->isOpen() && | ||
| + fseek( $this->handle, $offset, $whence ) === 0; | ||
| + } | ||
| + | ||
| + public function tell(): int { | ||
| + return $this->isOpen() | ||
| + ? (int)ftell( $this->handle ) | ||
| + : 0; | ||
| + } | ||
| + | ||
| + public function eof(): bool { | ||
| + return $this->isOpen() ? feof( $this->handle ) : true; | ||
| + } | ||
| + | ||
| + public function rewind(): void { | ||
| + if( $this->isOpen() ) { | ||
| + rewind( $this->handle ); | ||
| + } | ||
| + } | ||
| +} | ||
| require_once __DIR__ . '/GitRefs.php'; | ||
| require_once __DIR__ . '/GitPacks.php'; | ||
| - | ||
| -class Git { | ||
| - private const MAX_READ = 1048576; | ||
| - | ||
| - private string $repoPath; | ||
| - private string $objPath; | ||
| - private GitRefs $refs; | ||
| - private GitPacks $packs; | ||
| - | ||
| - public function __construct( string $repoPath ) { | ||
| - $this->setRepository( $repoPath ); | ||
| - } | ||
| - | ||
| - public function setRepository( string $repoPath ): void { | ||
| - $this->repoPath = rtrim( $repoPath, '/' ); | ||
| - $this->objPath = $this->repoPath . '/objects'; | ||
| - $this->refs = new GitRefs( $this->repoPath ); | ||
| - $this->packs = new GitPacks( $this->objPath ); | ||
| - } | ||
| - | ||
| - public function resolve( string $reference ): string { | ||
| - return $this->refs->resolve( $reference ); | ||
| - } | ||
| - | ||
| - public function getMainBranch(): array { | ||
| - return $this->refs->getMainBranch(); | ||
| - } | ||
| - | ||
| - public function eachBranch( callable $callback ): void { | ||
| - $this->refs->scanRefs( 'refs/heads', $callback ); | ||
| - } | ||
| - | ||
| - public function eachTag( callable $callback ): void { | ||
| - $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use ( | ||
| - $callback | ||
| - ) { | ||
| - $data = $this->read( $sha ); | ||
| - $tag = $this->parseTagData( $name, $sha, $data ); | ||
| - | ||
| - $callback( $tag ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - public function walk( | ||
| - string $refOrSha, | ||
| - callable $callback, | ||
| - string $path = '' | ||
| - ): void { | ||
| - $sha = $this->resolve( $refOrSha ); | ||
| - $treeSha = ''; | ||
| - | ||
| - if( $sha !== '' ) { | ||
| - $treeSha = $this->getTreeSha( $sha ); | ||
| - } | ||
| - | ||
| - if( $path !== '' && $treeSha !== '' ) { | ||
| - $info = $this->resolvePath( $treeSha, $path ); | ||
| - $treeSha = $info['isDir'] ? $info['sha'] : ''; | ||
| - } | ||
| - | ||
| - if( $treeSha !== '' ) { | ||
| - $this->walkTree( $treeSha, $callback ); | ||
| - } | ||
| - } | ||
| - | ||
| - public function readFile( string $ref, string $path ): File { | ||
| - $sha = $this->resolve( $ref ); | ||
| - $tree = $sha !== '' ? $this->getTreeSha( $sha ) : ''; | ||
| - $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : []; | ||
| - $file = new MissingFile(); | ||
| - | ||
| - if( isset( $info['sha'] ) && !$info['isDir'] && $info['sha'] !== '' ) { | ||
| - $file = new File( | ||
| - basename( $path ), | ||
| - $info['sha'], | ||
| - $info['mode'], | ||
| - 0, | ||
| - $this->getObjectSize( $info['sha'] ), | ||
| - $this->peek( $info['sha'] ) | ||
| - ); | ||
| - } | ||
| - | ||
| - return $file; | ||
| - } | ||
| - | ||
| - public function getObjectSize( string $sha, string $path = '' ): int { | ||
| - $target = $sha; | ||
| - $result = 0; | ||
| - | ||
| - if( $path !== '' ) { | ||
| - $info = $this->resolvePath( | ||
| - $this->getTreeSha( $this->resolve( $sha ) ), | ||
| - $path | ||
| - ); | ||
| - $target = $info['sha'] ?? ''; | ||
| - } | ||
| - | ||
| - if( $target !== '' ) { | ||
| - $result = $this->packs->getSize( $target ); | ||
| - | ||
| - if( $result === 0 ) { | ||
| - $result = $this->getLooseObjectSize( $target ); | ||
| - } | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - public function stream( | ||
| - string $sha, | ||
| - callable $callback, | ||
| - string $path = '' | ||
| - ): void { | ||
| - $target = $sha; | ||
| - | ||
| - if( $path !== '' ) { | ||
| - $info = $this->resolvePath( | ||
| - $this->getTreeSha( $this->resolve( $sha ) ), | ||
| - $path | ||
| - ); | ||
| - $target = isset( $info['isDir'] ) && !$info['isDir'] | ||
| - ? $info['sha'] | ||
| - : ''; | ||
| - } | ||
| - | ||
| - if( $target !== '' ) { | ||
| - $this->slurp( $target, $callback ); | ||
| - } | ||
| - } | ||
| - | ||
| - public function peek( string $sha, int $length = 255 ): string { | ||
| - $size = $this->packs->getSize( $sha ); | ||
| - | ||
| - return $size === 0 | ||
| - ? $this->peekLooseObject( $sha, $length ) | ||
| - : $this->packs->peek( $sha, $length ); | ||
| - } | ||
| - | ||
| - public function read( string $sha ): string { | ||
| - $size = $this->getObjectSize( $sha ); | ||
| - $content = ''; | ||
| - | ||
| - if( $size > 0 && $size <= self::MAX_READ ) { | ||
| - $this->slurp( $sha, function( $chunk ) use ( &$content ) { | ||
| - $content .= $chunk; | ||
| - } ); | ||
| - } | ||
| - | ||
| - return $content; | ||
| - } | ||
| - | ||
| - public function history( | ||
| - string $ref, | ||
| - int $limit, | ||
| - callable $callback | ||
| - ): void { | ||
| - $sha = $this->resolve( $ref ); | ||
| - $count = 0; | ||
| - | ||
| - while( $sha !== '' && $count < $limit ) { | ||
| - $commit = $this->parseCommit( $sha ); | ||
| - | ||
| - if( $commit->sha === '' ) { | ||
| - $sha = ''; | ||
| - } | ||
| - | ||
| - if( $sha !== '' ) { | ||
| - $callback( $commit ); | ||
| - $sha = $commit->parentSha; | ||
| - $count++; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - public function streamRaw( string $subPath ): bool { | ||
| - $result = false; | ||
| - | ||
| - if( strpos( $subPath, '..' ) === false ) { | ||
| - $path = "{$this->repoPath}/$subPath"; | ||
| - | ||
| - if( is_file( $path ) ) { | ||
| - $real = realpath( $path ); | ||
| - $repo = realpath( $this->repoPath ); | ||
| - | ||
| - if( $real && strpos( $real, $repo ) === 0 ) { | ||
| - $result = $this->streamFileContent( $path ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function streamFileContent( string $path ): bool { | ||
| - $result = false; | ||
| - | ||
| - if( $path !== '' ) { | ||
| - header( 'X-Accel-Redirect: ' . $path ); | ||
| - header( 'Content-Type: application/octet-stream' ); | ||
| - | ||
| - $result = true; | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - public function eachRef( callable $callback ): void { | ||
| - $head = $this->resolve( 'HEAD' ); | ||
| - | ||
| - if( $head !== '' ) { | ||
| - $callback( 'HEAD', $head ); | ||
| - } | ||
| - | ||
| - $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) { | ||
| - $callback( "refs/heads/$n", $s ); | ||
| - } ); | ||
| - | ||
| - $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) { | ||
| - $callback( "refs/tags/$n", $s ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - public function generatePackfile( array $objs ): Generator { | ||
| - $ctx = hash_init( 'sha1' ); | ||
| - $head = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) ); | ||
| - | ||
| - hash_update( $ctx, $head ); | ||
| - yield $head; | ||
| - | ||
| - foreach( $objs as $sha => $type ) { | ||
| - $size = $this->getObjectSize( $sha ); | ||
| - $byte = $type << 4 | $size & 0x0f; | ||
| - $sz = $size >> 4; | ||
| - $hdr = ''; | ||
| - | ||
| - while( $sz > 0 ) { | ||
| - $hdr .= chr( $byte | 0x80 ); | ||
| - $byte = $sz & 0x7f; | ||
| - $sz >>= 7; | ||
| - } | ||
| - | ||
| - $hdr .= chr( $byte ); | ||
| - hash_update( $ctx, $hdr ); | ||
| - yield $hdr; | ||
| - | ||
| - $deflate = deflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| - | ||
| - foreach( $this->slurpChunks( $sha ) as $raw ) { | ||
| - $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH ); | ||
| - | ||
| - if( $compressed !== '' ) { | ||
| - hash_update( $ctx, $compressed ); | ||
| - yield $compressed; | ||
| - } | ||
| - } | ||
| - | ||
| - $final = deflate_add( $deflate, '', ZLIB_FINISH ); | ||
| - | ||
| - if( $final !== '' ) { | ||
| - hash_update( $ctx, $final ); | ||
| - yield $final; | ||
| - } | ||
| - } | ||
| - | ||
| - yield hash_final( $ctx, true ); | ||
| - } | ||
| - | ||
| - private function slurpChunks( string $sha ): Generator { | ||
| - $path = $this->getLoosePath( $sha ); | ||
| - | ||
| - if( is_file( $path ) ) { | ||
| - yield from $this->looseObjectChunks( $path ); | ||
| - } else { | ||
| - $any = false; | ||
| - | ||
| - foreach( $this->packs->streamGenerator( $sha ) as $chunk ) { | ||
| - $any = true; | ||
| - yield $chunk; | ||
| - } | ||
| - | ||
| - if( !$any ) { | ||
| - $data = $this->packs->read( $sha ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - yield $data; | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function looseObjectChunks( string $path ): Generator { | ||
| - $handle = fopen( $path, 'rb' ); | ||
| - $infl = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null; | ||
| - | ||
| - if( !$handle || !$infl ) { | ||
| - return; | ||
| - } | ||
| - | ||
| - $found = false; | ||
| - $buffer = ''; | ||
| - | ||
| - while( !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 16384 ); | ||
| - $inflated = inflate_add( $infl, $chunk ); | ||
| - | ||
| - if( $inflated === false ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - if( !$found ) { | ||
| - $buffer .= $inflated; | ||
| - $eos = strpos( $buffer, "\0" ); | ||
| - | ||
| - if( $eos !== false ) { | ||
| - $found = true; | ||
| - $body = substr( $buffer, $eos + 1 ); | ||
| - | ||
| - if( $body !== '' ) { | ||
| - yield $body; | ||
| - } | ||
| - | ||
| - $buffer = ''; | ||
| - } | ||
| - } elseif( $inflated !== '' ) { | ||
| - yield $inflated; | ||
| - } | ||
| - } | ||
| - | ||
| - fclose( $handle ); | ||
| - } | ||
| - | ||
| - private function streamCompressedObject( string $sha, $ctx ): Generator { | ||
| - $stream = CompressionStream::createDeflater(); | ||
| - $buffer = ''; | ||
| - | ||
| - $this->slurp( $sha, function( $chunk ) use ( | ||
| - $stream, | ||
| - $ctx, | ||
| - &$buffer | ||
| - ) { | ||
| - $compressed = $stream->pump( $chunk ); | ||
| - | ||
| - if( $compressed !== '' ) { | ||
| - hash_update( $ctx, $compressed ); | ||
| - $buffer .= $compressed; | ||
| - } | ||
| - } ); | ||
| - | ||
| - $final = $stream->finish(); | ||
| - | ||
| - if( $final !== '' ) { | ||
| - hash_update( $ctx, $final ); | ||
| - $buffer .= $final; | ||
| - } | ||
| - | ||
| - $pos = 0; | ||
| - $len = strlen( $buffer ); | ||
| - | ||
| - while( $pos < $len ) { | ||
| - $chunk = substr( $buffer, $pos, 32768 ); | ||
| - | ||
| - yield $chunk; | ||
| - $pos += 32768; | ||
| - } | ||
| - } | ||
| - | ||
| - private function getTreeSha( string $commitOrTreeSha ): string { | ||
| - $data = $this->read( $commitOrTreeSha ); | ||
| - $sha = $commitOrTreeSha; | ||
| - | ||
| - if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) { | ||
| - $sha = $this->getTreeSha( $matches[1] ); | ||
| - } | ||
| - | ||
| - if( $sha === $commitOrTreeSha && | ||
| - preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { | ||
| - $sha = $matches[1]; | ||
| - } | ||
| - | ||
| - return $sha; | ||
| - } | ||
| - | ||
| - private function resolvePath( string $treeSha, string $path ): array { | ||
| - $parts = explode( '/', trim( $path, '/' ) ); | ||
| - $sha = $treeSha; | ||
| - $mode = '40000'; | ||
| - | ||
| - foreach( $parts as $part ) { | ||
| - $entry = [ 'sha' => '', 'mode' => '' ]; | ||
| - | ||
| - if( $part !== '' && $sha !== '' ) { | ||
| - $entry = $this->findTreeEntry( $sha, $part ); | ||
| - } | ||
| - | ||
| - $sha = $entry['sha']; | ||
| - $mode = $entry['mode']; | ||
| - } | ||
| - | ||
| - return [ | ||
| - 'sha' => $sha, | ||
| - 'mode' => $mode, | ||
| - 'isDir' => $mode === '40000' || $mode === '040000' | ||
| - ]; | ||
| - } | ||
| - | ||
| - private function findTreeEntry( string $treeSha, string $name ): array { | ||
| - $data = $this->read( $treeSha ); | ||
| - $pos = 0; | ||
| - $len = strlen( $data ); | ||
| - $entry = [ 'sha' => '', 'mode' => '' ]; | ||
| - | ||
| - while( $pos < $len ) { | ||
| - $space = strpos( $data, ' ', $pos ); | ||
| - $eos = strpos( $data, "\0", $space ); | ||
| - | ||
| - if( $space === false || $eos === false ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) { | ||
| - $entry = [ | ||
| - 'sha' => bin2hex( substr( $data, $eos + 1, 20 ) ), | ||
| - 'mode' => substr( $data, $pos, $space - $pos ) | ||
| - ]; | ||
| - break; | ||
| - } | ||
| - | ||
| - $pos = $eos + 21; | ||
| - } | ||
| - | ||
| - return $entry; | ||
| - } | ||
| - | ||
| - private function parseTagData( | ||
| - string $name, | ||
| - string $sha, | ||
| - string $data | ||
| - ): Tag { | ||
| - $isAnn = strncmp( $data, 'object ', 7 ) === 0; | ||
| - $pattern = $isAnn | ||
| - ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' | ||
| - : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'; | ||
| - $id = $this->parseIdentity( $data, $pattern ); | ||
| - $target = $isAnn | ||
| - ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha ) | ||
| - : $sha; | ||
| - | ||
| - return new Tag( | ||
| - $name, | ||
| - $sha, | ||
| - $target, | ||
| - $id['timestamp'], | ||
| - $this->extractMessage( $data ), | ||
| - $id['name'] | ||
| - ); | ||
| - } | ||
| - | ||
| - private function extractPattern( | ||
| - string $data, | ||
| - string $pattern, | ||
| - int $group, | ||
| - string $default = '' | ||
| - ): string { | ||
| - return preg_match( $pattern, $data, $matches ) | ||
| - ? $matches[$group] | ||
| - : $default; | ||
| - } | ||
| - | ||
| - private function parseIdentity( string $data, string $pattern ): array { | ||
| - $found = preg_match( $pattern, $data, $matches ); | ||
| - | ||
| - return [ | ||
| - 'name' => $found ? trim( $matches[1] ) : 'Unknown', | ||
| - 'email' => $found ? $matches[2] : '', | ||
| - 'timestamp' => $found ? (int)$matches[3] : 0 | ||
| - ]; | ||
| - } | ||
| - | ||
| - private function extractMessage( string $data ): string { | ||
| - $pos = strpos( $data, "\n\n" ); | ||
| - | ||
| - return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : ''; | ||
| - } | ||
| - | ||
| - private function slurp( string $sha, callable $callback ): void { | ||
| - $path = $this->getLoosePath( $sha ); | ||
| - | ||
| - if( is_file( $path ) ) { | ||
| - $this->slurpLooseObject( $path, $callback ); | ||
| - } else { | ||
| - $this->slurpPackedObject( $sha, $callback ); | ||
| - } | ||
| - } | ||
| - | ||
| - private function slurpLooseObject( string $path, callable $callback ): void { | ||
| - $this->iterateInflated( | ||
| - $path, | ||
| - function( $chunk ) use ( $callback ) { | ||
| - if( $chunk !== '' ) { | ||
| - $callback( $chunk ); | ||
| - } | ||
| - return true; | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - private function slurpPackedObject( string $sha, callable $callback ): void { | ||
| - $streamed = $this->packs->stream( $sha, $callback ); | ||
| - | ||
| - if( !$streamed ) { | ||
| - $data = $this->packs->read( $sha ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $callback( $data ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function iterateInflated( | ||
| - string $path, | ||
| - callable $processor | ||
| - ): void { | ||
| - $handle = fopen( $path, 'rb' ); | ||
| - $infl = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null; | ||
| - $found = false; | ||
| - $buffer = ''; | ||
| - | ||
| - if( $handle && $infl ) { | ||
| - while( !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 16384 ); | ||
| - $inflated = inflate_add( $infl, $chunk ); | ||
| - | ||
| - if( $inflated === false ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - if( !$found ) { | ||
| - $buffer .= $inflated; | ||
| - $eos = strpos( $buffer, "\0" ); | ||
| - | ||
| - if( $eos !== false ) { | ||
| - $found = true; | ||
| - $body = substr( $buffer, $eos + 1 ); | ||
| - $head = substr( $buffer, 0, $eos ); | ||
| - | ||
| - if( $processor( $body, $head ) === false ) { | ||
| - break; | ||
| - } | ||
| - } | ||
| - } elseif( $processor( $inflated, null ) === false ) { | ||
| - break; | ||
| - } | ||
| - } | ||
| - | ||
| - fclose( $handle ); | ||
| - } | ||
| - } | ||
| - | ||
| - private function peekLooseObject( string $sha, int $length ): string { | ||
| - $path = $this->getLoosePath( $sha ); | ||
| - $buf = ''; | ||
| - | ||
| - if( is_file( $path ) ) { | ||
| - $this->iterateInflated( | ||
| - $path, | ||
| - function( $chunk ) use ( $length, &$buf ) { | ||
| - $buf .= $chunk; | ||
| - return strlen( $buf ) < $length; | ||
| - } | ||
| - ); | ||
| - } | ||
| - | ||
| - return substr( $buf, 0, $length ); | ||
| - } | ||
| - | ||
| - private function parseCommit( string $sha ): object { | ||
| - $data = $this->read( $sha ); | ||
| - $result = (object)[ 'sha' => '' ]; | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $id = $this->parseIdentity( | ||
| - $data, | ||
| - '/^author (.*) <(.*)> (\d+)/m' | ||
| - ); | ||
| - | ||
| - $result = (object)[ | ||
| - 'sha' => $sha, | ||
| - 'message' => $this->extractMessage( $data ), | ||
| - 'author' => $id['name'], | ||
| - 'email' => $id['email'], | ||
| - 'date' => $id['timestamp'], | ||
| - 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 ) | ||
| - ]; | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function walkTree( string $sha, callable $callback ): void { | ||
| - $data = $this->read( $sha ); | ||
| - $tree = $data; | ||
| - | ||
| - if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) { | ||
| - $tree = $this->read( $m[1] ); | ||
| - } | ||
| - | ||
| - if( $tree !== '' && $this->isTreeData( $tree ) ) { | ||
| - $this->processTree( $tree, $callback ); | ||
| - } | ||
| - } | ||
| - | ||
| - private function processTree( string $data, callable $callback ): void { | ||
| - $pos = 0; | ||
| - $len = strlen( $data ); | ||
| - | ||
| - while( $pos < $len ) { | ||
| - $space = strpos( $data, ' ', $pos ); | ||
| - $eos = strpos( $data, "\0", $space ); | ||
| - $entry = null; | ||
| - | ||
| - if( $space !== false && $eos !== false && $eos + 21 <= $len ) { | ||
| - $mode = substr( $data, $pos, $space - $pos ); | ||
| - $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); | ||
| - $dir = $mode === '40000' || $mode === '040000'; | ||
| - $isSub = $mode === '160000'; | ||
| - | ||
| - $entry = [ | ||
| - 'file' => new File( | ||
| - substr( $data, $space + 1, $eos - $space - 1 ), | ||
| - $sha, | ||
| - $mode, | ||
| - 0, | ||
| - $dir || $isSub ? 0 : $this->getObjectSize( $sha ), | ||
| - $dir || $isSub ? '' : $this->peek( $sha ) | ||
| - ), | ||
| - 'nextPosition' => $eos + 21 | ||
| - ]; | ||
| - } | ||
| - | ||
| - if( $entry === null ) { | ||
| - break; | ||
| - } | ||
| - | ||
| - $callback( $entry['file'] ); | ||
| - $pos = $entry['nextPosition']; | ||
| - } | ||
| - } | ||
| - | ||
| - private function isTreeData( string $data ): bool { | ||
| - $len = strlen( $data ); | ||
| - $patt = '/^(40000|100644|100755|120000|160000) /'; | ||
| - $match = $len >= 25 && preg_match( $patt, $data ); | ||
| - $eos = $match ? strpos( $data, "\0" ) : false; | ||
| - | ||
| - return $match && $eos !== false && $eos + 21 <= $len; | ||
| - } | ||
| - | ||
| - private function getLoosePath( string $sha ): string { | ||
| - return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" . | ||
| - substr( $sha, 2 ); | ||
| - } | ||
| - | ||
| - private function getLooseObjectSize( string $sha ): int { | ||
| - $path = $this->getLoosePath( $sha ); | ||
| - $size = 0; | ||
| - | ||
| - if( is_file( $path ) ) { | ||
| - $this->iterateInflated( | ||
| - $path, | ||
| - function( $c, $head ) use ( &$size ) { | ||
| - if( $head !== null ) { | ||
| - $parts = explode( ' ', $head ); | ||
| - $size = isset( $parts[1] ) ? (int)$parts[1] : 0; | ||
| - } | ||
| +require_once __DIR__ . '/BufferedFileReader.php'; | ||
| + | ||
| +class Git { | ||
| + private const MAX_READ = 1048576; | ||
| + | ||
| + private string $repoPath; | ||
| + private string $objPath; | ||
| + private GitRefs $refs; | ||
| + private GitPacks $packs; | ||
| + | ||
| + public function __construct( string $repoPath ) { | ||
| + $this->setRepository( $repoPath ); | ||
| + } | ||
| + | ||
| + public function setRepository( string $repoPath ): void { | ||
| + $this->repoPath = rtrim( $repoPath, '/' ); | ||
| + $this->objPath = $this->repoPath . '/objects'; | ||
| + $this->refs = new GitRefs( $this->repoPath ); | ||
| + $this->packs = new GitPacks( $this->objPath ); | ||
| + } | ||
| + | ||
| + public function resolve( string $reference ): string { | ||
| + return $this->refs->resolve( $reference ); | ||
| + } | ||
| + | ||
| + public function getMainBranch(): array { | ||
| + return $this->refs->getMainBranch(); | ||
| + } | ||
| + | ||
| + public function eachBranch( callable $callback ): void { | ||
| + $this->refs->scanRefs( 'refs/heads', $callback ); | ||
| + } | ||
| + | ||
| + public function eachTag( callable $callback ): void { | ||
| + $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use ( | ||
| + $callback | ||
| + ) { | ||
| + $data = $this->read( $sha ); | ||
| + $tag = $this->parseTagData( $name, $sha, $data ); | ||
| + | ||
| + $callback( $tag ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + public function walk( | ||
| + string $refOrSha, | ||
| + callable $callback, | ||
| + string $path = '' | ||
| + ): void { | ||
| + $sha = $this->resolve( $refOrSha ); | ||
| + $treeSha = ''; | ||
| + | ||
| + if( $sha !== '' ) { | ||
| + $treeSha = $this->getTreeSha( $sha ); | ||
| + } | ||
| + | ||
| + if( $path !== '' && $treeSha !== '' ) { | ||
| + $info = $this->resolvePath( $treeSha, $path ); | ||
| + $treeSha = $info['isDir'] ? $info['sha'] : ''; | ||
| + } | ||
| + | ||
| + if( $treeSha !== '' ) { | ||
| + $this->walkTree( $treeSha, $callback ); | ||
| + } | ||
| + } | ||
| + | ||
| + public function readFile( string $ref, string $path ): File { | ||
| + $sha = $this->resolve( $ref ); | ||
| + $tree = $sha !== '' ? $this->getTreeSha( $sha ) : ''; | ||
| + $info = $tree !== '' ? $this->resolvePath( $tree, $path ) : []; | ||
| + $file = new MissingFile(); | ||
| + | ||
| + if( isset( $info['sha'] ) && !$info['isDir'] && $info['sha'] !== '' ) { | ||
| + $file = new File( | ||
| + basename( $path ), | ||
| + $info['sha'], | ||
| + $info['mode'], | ||
| + 0, | ||
| + $this->getObjectSize( $info['sha'] ), | ||
| + $this->peek( $info['sha'] ) | ||
| + ); | ||
| + } | ||
| + | ||
| + return $file; | ||
| + } | ||
| + | ||
| + public function getObjectSize( string $sha, string $path = '' ): int { | ||
| + $target = $sha; | ||
| + $result = 0; | ||
| + | ||
| + if( $path !== '' ) { | ||
| + $info = $this->resolvePath( | ||
| + $this->getTreeSha( $this->resolve( $sha ) ), | ||
| + $path | ||
| + ); | ||
| + $target = $info['sha'] ?? ''; | ||
| + } | ||
| + | ||
| + if( $target !== '' ) { | ||
| + $result = $this->packs->getSize( $target ); | ||
| + | ||
| + if( $result === 0 ) { | ||
| + $result = $this->getLooseObjectSize( $target ); | ||
| + } | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + public function stream( | ||
| + string $sha, | ||
| + callable $callback, | ||
| + string $path = '' | ||
| + ): void { | ||
| + $target = $sha; | ||
| + | ||
| + if( $path !== '' ) { | ||
| + $info = $this->resolvePath( | ||
| + $this->getTreeSha( $this->resolve( $sha ) ), | ||
| + $path | ||
| + ); | ||
| + $target = isset( $info['isDir'] ) && !$info['isDir'] | ||
| + ? $info['sha'] | ||
| + : ''; | ||
| + } | ||
| + | ||
| + if( $target !== '' ) { | ||
| + $this->slurp( $target, $callback ); | ||
| + } | ||
| + } | ||
| + | ||
| + public function peek( string $sha, int $length = 255 ): string { | ||
| + $size = $this->packs->getSize( $sha ); | ||
| + | ||
| + return $size === 0 | ||
| + ? $this->peekLooseObject( $sha, $length ) | ||
| + : $this->packs->peek( $sha, $length ); | ||
| + } | ||
| + | ||
| + public function read( string $sha ): string { | ||
| + $size = $this->getObjectSize( $sha ); | ||
| + $content = ''; | ||
| + | ||
| + if( $size > 0 && $size <= self::MAX_READ ) { | ||
| + $this->slurp( $sha, function( $chunk ) use ( &$content ) { | ||
| + $content .= $chunk; | ||
| + } ); | ||
| + } | ||
| + | ||
| + return $content; | ||
| + } | ||
| + | ||
| + public function history( | ||
| + string $ref, | ||
| + int $limit, | ||
| + callable $callback | ||
| + ): void { | ||
| + $sha = $this->resolve( $ref ); | ||
| + $count = 0; | ||
| + | ||
| + while( $sha !== '' && $count < $limit ) { | ||
| + $commit = $this->parseCommit( $sha ); | ||
| + | ||
| + if( $commit->sha === '' ) { | ||
| + $sha = ''; | ||
| + } | ||
| + | ||
| + if( $sha !== '' ) { | ||
| + $callback( $commit ); | ||
| + $sha = $commit->parentSha; | ||
| + $count++; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + public function streamRaw( string $subPath ): bool { | ||
| + $result = false; | ||
| + | ||
| + if( strpos( $subPath, '..' ) === false ) { | ||
| + $path = "{$this->repoPath}/$subPath"; | ||
| + | ||
| + if( is_file( $path ) ) { | ||
| + $real = realpath( $path ); | ||
| + $repo = realpath( $this->repoPath ); | ||
| + | ||
| + if( $real !== false && strpos( $real, $repo ) === 0 ) { | ||
| + $result = $this->streamFileContent( $path ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + private function streamFileContent( string $path ): bool { | ||
| + $result = false; | ||
| + | ||
| + if( $path !== '' ) { | ||
| + header( 'X-Accel-Redirect: ' . $path ); | ||
| + header( 'Content-Type: application/octet-stream' ); | ||
| + | ||
| + $result = true; | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + public function eachRef( callable $callback ): void { | ||
| + $head = $this->resolve( 'HEAD' ); | ||
| + | ||
| + if( $head !== '' ) { | ||
| + $callback( 'HEAD', $head ); | ||
| + } | ||
| + | ||
| + $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) { | ||
| + $callback( "refs/heads/$n", $s ); | ||
| + } ); | ||
| + | ||
| + $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) { | ||
| + $callback( "refs/tags/$n", $s ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + public function generatePackfile( array $objs ): Generator { | ||
| + $ctx = hash_init( 'sha1' ); | ||
| + $head = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) ); | ||
| + | ||
| + hash_update( $ctx, $head ); | ||
| + yield $head; | ||
| + | ||
| + foreach( $objs as $sha => $type ) { | ||
| + $size = $this->getObjectSize( $sha ); | ||
| + $byte = $type << 4 | $size & 0x0f; | ||
| + $sz = $size >> 4; | ||
| + $hdr = ''; | ||
| + | ||
| + while( $sz > 0 ) { | ||
| + $hdr .= chr( $byte | 0x80 ); | ||
| + $byte = $sz & 0x7f; | ||
| + $sz >>= 7; | ||
| + } | ||
| + | ||
| + $hdr .= chr( $byte ); | ||
| + hash_update( $ctx, $hdr ); | ||
| + yield $hdr; | ||
| + | ||
| + $deflate = deflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| + | ||
| + foreach( $this->slurpChunks( $sha ) as $raw ) { | ||
| + $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH ); | ||
| + | ||
| + if( $compressed !== '' ) { | ||
| + hash_update( $ctx, $compressed ); | ||
| + yield $compressed; | ||
| + } | ||
| + } | ||
| + | ||
| + $final = deflate_add( $deflate, '', ZLIB_FINISH ); | ||
| + | ||
| + if( $final !== '' ) { | ||
| + hash_update( $ctx, $final ); | ||
| + yield $final; | ||
| + } | ||
| + } | ||
| + | ||
| + yield hash_final( $ctx, true ); | ||
| + } | ||
| + | ||
| + private function slurpChunks( string $sha ): Generator { | ||
| + $path = $this->getLoosePath( $sha ); | ||
| + | ||
| + if( is_file( $path ) ) { | ||
| + yield from $this->looseObjectChunks( $path ); | ||
| + } else { | ||
| + $any = false; | ||
| + | ||
| + foreach( $this->packs->streamGenerator( $sha ) as $chunk ) { | ||
| + $any = true; | ||
| + yield $chunk; | ||
| + } | ||
| + | ||
| + if( !$any ) { | ||
| + $data = $this->packs->read( $sha ); | ||
| + | ||
| + if( $data !== '' ) { | ||
| + yield $data; | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function looseObjectChunks( string $path ): Generator { | ||
| + $reader = BufferedFileReader::open( $path ); | ||
| + $infl = $reader->isOpen() | ||
| + ? inflate_init( ZLIB_ENCODING_DEFLATE ) | ||
| + : false; | ||
| + | ||
| + if( $reader->isOpen() && $infl !== false ) { | ||
| + $found = false; | ||
| + $buffer = ''; | ||
| + | ||
| + while( !$reader->eof() ) { | ||
| + $chunk = $reader->read( 16384 ); | ||
| + $inflated = inflate_add( $infl, $chunk ); | ||
| + | ||
| + if( $inflated === false ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + if( !$found ) { | ||
| + $buffer .= $inflated; | ||
| + $eos = strpos( $buffer, "\0" ); | ||
| + | ||
| + if( $eos !== false ) { | ||
| + $found = true; | ||
| + $body = substr( $buffer, $eos + 1 ); | ||
| + | ||
| + if( $body !== '' ) { | ||
| + yield $body; | ||
| + } | ||
| + | ||
| + $buffer = ''; | ||
| + } | ||
| + } elseif( $inflated !== '' ) { | ||
| + yield $inflated; | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function streamCompressedObject( string $sha, $ctx ): Generator { | ||
| + $stream = CompressionStream::createDeflater(); | ||
| + $buffer = ''; | ||
| + | ||
| + $this->slurp( $sha, function( $chunk ) use ( | ||
| + $stream, | ||
| + $ctx, | ||
| + &$buffer | ||
| + ) { | ||
| + $compressed = $stream->pump( $chunk ); | ||
| + | ||
| + if( $compressed !== '' ) { | ||
| + hash_update( $ctx, $compressed ); | ||
| + $buffer .= $compressed; | ||
| + } | ||
| + } ); | ||
| + | ||
| + $final = $stream->finish(); | ||
| + | ||
| + if( $final !== '' ) { | ||
| + hash_update( $ctx, $final ); | ||
| + $buffer .= $final; | ||
| + } | ||
| + | ||
| + $pos = 0; | ||
| + $len = strlen( $buffer ); | ||
| + | ||
| + while( $pos < $len ) { | ||
| + $chunk = substr( $buffer, $pos, 32768 ); | ||
| + | ||
| + yield $chunk; | ||
| + $pos += 32768; | ||
| + } | ||
| + } | ||
| + | ||
| + private function getTreeSha( string $commitOrTreeSha ): string { | ||
| + $data = $this->read( $commitOrTreeSha ); | ||
| + $sha = $commitOrTreeSha; | ||
| + | ||
| + if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) { | ||
| + $sha = $this->getTreeSha( $matches[1] ); | ||
| + } | ||
| + | ||
| + if( $sha === $commitOrTreeSha && | ||
| + preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { | ||
| + $sha = $matches[1]; | ||
| + } | ||
| + | ||
| + return $sha; | ||
| + } | ||
| + | ||
| + private function resolvePath( string $treeSha, string $path ): array { | ||
| + $parts = explode( '/', trim( $path, '/' ) ); | ||
| + $sha = $treeSha; | ||
| + $mode = '40000'; | ||
| + | ||
| + foreach( $parts as $part ) { | ||
| + $entry = [ 'sha' => '', 'mode' => '' ]; | ||
| + | ||
| + if( $part !== '' && $sha !== '' ) { | ||
| + $entry = $this->findTreeEntry( $sha, $part ); | ||
| + } | ||
| + | ||
| + $sha = $entry['sha']; | ||
| + $mode = $entry['mode']; | ||
| + } | ||
| + | ||
| + return [ | ||
| + 'sha' => $sha, | ||
| + 'mode' => $mode, | ||
| + 'isDir' => $mode === '40000' || $mode === '040000' | ||
| + ]; | ||
| + } | ||
| + | ||
| + private function findTreeEntry( string $treeSha, string $name ): array { | ||
| + $data = $this->read( $treeSha ); | ||
| + $pos = 0; | ||
| + $len = strlen( $data ); | ||
| + $entry = [ 'sha' => '', 'mode' => '' ]; | ||
| + | ||
| + while( $pos < $len ) { | ||
| + $space = strpos( $data, ' ', $pos ); | ||
| + $eos = strpos( $data, "\0", $space ); | ||
| + | ||
| + if( $space === false || $eos === false ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) { | ||
| + $entry = [ | ||
| + 'sha' => bin2hex( substr( $data, $eos + 1, 20 ) ), | ||
| + 'mode' => substr( $data, $pos, $space - $pos ) | ||
| + ]; | ||
| + break; | ||
| + } | ||
| + | ||
| + $pos = $eos + 21; | ||
| + } | ||
| + | ||
| + return $entry; | ||
| + } | ||
| + | ||
| + private function parseTagData( | ||
| + string $name, | ||
| + string $sha, | ||
| + string $data | ||
| + ): Tag { | ||
| + $isAnn = strncmp( $data, 'object ', 7 ) === 0; | ||
| + $pattern = $isAnn | ||
| + ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' | ||
| + : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'; | ||
| + $id = $this->parseIdentity( $data, $pattern ); | ||
| + $target = $isAnn | ||
| + ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha ) | ||
| + : $sha; | ||
| + | ||
| + return new Tag( | ||
| + $name, | ||
| + $sha, | ||
| + $target, | ||
| + $id['timestamp'], | ||
| + $this->extractMessage( $data ), | ||
| + $id['name'] | ||
| + ); | ||
| + } | ||
| + | ||
| + private function extractPattern( | ||
| + string $data, | ||
| + string $pattern, | ||
| + int $group, | ||
| + string $default = '' | ||
| + ): string { | ||
| + return preg_match( $pattern, $data, $matches ) | ||
| + ? $matches[$group] | ||
| + : $default; | ||
| + } | ||
| + | ||
| + private function parseIdentity( string $data, string $pattern ): array { | ||
| + $found = preg_match( $pattern, $data, $matches ); | ||
| + | ||
| + return [ | ||
| + 'name' => $found ? trim( $matches[1] ) : 'Unknown', | ||
| + 'email' => $found ? $matches[2] : '', | ||
| + 'timestamp' => $found ? (int)$matches[3] : 0 | ||
| + ]; | ||
| + } | ||
| + | ||
| + private function extractMessage( string $data ): string { | ||
| + $pos = strpos( $data, "\n\n" ); | ||
| + | ||
| + return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : ''; | ||
| + } | ||
| + | ||
| + private function slurp( string $sha, callable $callback ): void { | ||
| + $path = $this->getLoosePath( $sha ); | ||
| + | ||
| + if( is_file( $path ) ) { | ||
| + $this->slurpLooseObject( $path, $callback ); | ||
| + } else { | ||
| + $this->slurpPackedObject( $sha, $callback ); | ||
| + } | ||
| + } | ||
| + | ||
| + private function slurpLooseObject( string $path, callable $callback ): void { | ||
| + $this->iterateInflated( | ||
| + $path, | ||
| + function( $chunk ) use ( $callback ) { | ||
| + if( $chunk !== '' ) { | ||
| + $callback( $chunk ); | ||
| + } | ||
| + | ||
| + return true; | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + private function slurpPackedObject( string $sha, callable $callback ): void { | ||
| + $streamed = $this->packs->stream( $sha, $callback ); | ||
| + | ||
| + if( !$streamed ) { | ||
| + $data = $this->packs->read( $sha ); | ||
| + | ||
| + if( $data !== '' ) { | ||
| + $callback( $data ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function iterateInflated( | ||
| + string $path, | ||
| + callable $processor | ||
| + ): void { | ||
| + $reader = BufferedFileReader::open( $path ); | ||
| + $infl = $reader->isOpen() | ||
| + ? inflate_init( ZLIB_ENCODING_DEFLATE ) | ||
| + : false; | ||
| + $found = false; | ||
| + $buffer = ''; | ||
| + | ||
| + if( $reader->isOpen() && $infl !== false ) { | ||
| + while( !$reader->eof() ) { | ||
| + $chunk = $reader->read( 16384 ); | ||
| + $inflated = inflate_add( $infl, $chunk ); | ||
| + | ||
| + if( $inflated === false ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + if( !$found ) { | ||
| + $buffer .= $inflated; | ||
| + $eos = strpos( $buffer, "\0" ); | ||
| + | ||
| + if( $eos !== false ) { | ||
| + $found = true; | ||
| + $body = substr( $buffer, $eos + 1 ); | ||
| + $head = substr( $buffer, 0, $eos ); | ||
| + | ||
| + if( $processor( $body, $head ) === false ) { | ||
| + break; | ||
| + } | ||
| + } | ||
| + } elseif( $processor( $inflated, '' ) === false ) { | ||
| + break; | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function peekLooseObject( string $sha, int $length ): string { | ||
| + $path = $this->getLoosePath( $sha ); | ||
| + $buf = ''; | ||
| + | ||
| + if( is_file( $path ) ) { | ||
| + $this->iterateInflated( | ||
| + $path, | ||
| + function( $chunk ) use ( $length, &$buf ) { | ||
| + $buf .= $chunk; | ||
| + | ||
| + return strlen( $buf ) < $length; | ||
| + } | ||
| + ); | ||
| + } | ||
| + | ||
| + return substr( $buf, 0, $length ); | ||
| + } | ||
| + | ||
| + private function parseCommit( string $sha ): object { | ||
| + $data = $this->read( $sha ); | ||
| + $result = (object)[ 'sha' => '' ]; | ||
| + | ||
| + if( $data !== '' ) { | ||
| + $id = $this->parseIdentity( | ||
| + $data, | ||
| + '/^author (.*) <(.*)> (\d+)/m' | ||
| + ); | ||
| + | ||
| + $result = (object)[ | ||
| + 'sha' => $sha, | ||
| + 'message' => $this->extractMessage( $data ), | ||
| + 'author' => $id['name'], | ||
| + 'email' => $id['email'], | ||
| + 'date' => $id['timestamp'], | ||
| + 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 ) | ||
| + ]; | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + private function walkTree( string $sha, callable $callback ): void { | ||
| + $data = $this->read( $sha ); | ||
| + $tree = $data; | ||
| + | ||
| + if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) { | ||
| + $tree = $this->read( $m[1] ); | ||
| + } | ||
| + | ||
| + if( $tree !== '' && $this->isTreeData( $tree ) ) { | ||
| + $this->processTree( $tree, $callback ); | ||
| + } | ||
| + } | ||
| + | ||
| + private function processTree( string $data, callable $callback ): void { | ||
| + $pos = 0; | ||
| + $len = strlen( $data ); | ||
| + | ||
| + while( $pos < $len ) { | ||
| + $space = strpos( $data, ' ', $pos ); | ||
| + $eos = strpos( $data, "\0", $space ); | ||
| + $entry = null; | ||
| + | ||
| + if( $space !== false && $eos !== false && $eos + 21 <= $len ) { | ||
| + $mode = substr( $data, $pos, $space - $pos ); | ||
| + $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); | ||
| + $dir = $mode === '40000' || $mode === '040000'; | ||
| + $isSub = $mode === '160000'; | ||
| + | ||
| + $entry = [ | ||
| + 'file' => new File( | ||
| + substr( $data, $space + 1, $eos - $space - 1 ), | ||
| + $sha, | ||
| + $mode, | ||
| + 0, | ||
| + $dir || $isSub ? 0 : $this->getObjectSize( $sha ), | ||
| + $dir || $isSub ? '' : $this->peek( $sha ) | ||
| + ), | ||
| + 'nextPosition' => $eos + 21 | ||
| + ]; | ||
| + } | ||
| + | ||
| + if( $entry === null ) { | ||
| + break; | ||
| + } | ||
| + | ||
| + $callback( $entry['file'] ); | ||
| + $pos = $entry['nextPosition']; | ||
| + } | ||
| + } | ||
| + | ||
| + private function isTreeData( string $data ): bool { | ||
| + $len = strlen( $data ); | ||
| + $patt = '/^(40000|100644|100755|120000|160000) /'; | ||
| + $match = $len >= 25 && preg_match( $patt, $data ); | ||
| + $eos = $match ? strpos( $data, "\0" ) : false; | ||
| + | ||
| + return $match && $eos !== false && $eos + 21 <= $len; | ||
| + } | ||
| + | ||
| + private function getLoosePath( string $sha ): string { | ||
| + return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" . | ||
| + substr( $sha, 2 ); | ||
| + } | ||
| + | ||
| + private function getLooseObjectSize( string $sha ): int { | ||
| + $path = $this->getLoosePath( $sha ); | ||
| + $size = 0; | ||
| + | ||
| + if( is_file( $path ) ) { | ||
| + $this->iterateInflated( | ||
| + $path, | ||
| + function( $c, $head ) use ( &$size ) { | ||
| + if( $head !== '' ) { | ||
| + $parts = explode( ' ', $head ); | ||
| + $size = isset( $parts[1] ) ? (int)$parts[1] : 0; | ||
| + } | ||
| + | ||
| return false; | ||
| } |
| } | ||
| } | ||
| + | ||
| $buffer = []; | ||
| } |
| public function stream( string $sha, callable $callback ): bool { | ||
| - return $this->streamInternal( $sha, $callback, 0 ); | ||
| - } | ||
| - | ||
| - public function streamGenerator( string $sha ): Generator { | ||
| - $info = $this->findPackInfo( $sha ); | ||
| - | ||
| - if( $info['offset'] !== 0 ) { | ||
| - $handle = $this->getHandle( $info['file'] ); | ||
| - | ||
| - if( $handle ) { | ||
| - yield from $this->streamPackEntryGenerator( | ||
| - $handle, | ||
| - $info['offset'], | ||
| - 0 | ||
| - ); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function streamInternal( | ||
| - string $sha, | ||
| - callable $callback, | ||
| - int $depth | ||
| - ): 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, | ||
| - $depth | ||
| - ); | ||
| - } | ||
| - } | ||
| - | ||
| - 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' => '' ]; | ||
| - | ||
| - if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) { | ||
| - $binarySha = hex2bin( $sha ); | ||
| - | ||
| - 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 ) { | ||
| - $count = count( $this->packFiles ); | ||
| - $idx = 0; | ||
| - $found = false; | ||
| - | ||
| - while( !$found && $idx < $count ) { | ||
| - $indexFile = $this->packFiles[$idx]; | ||
| - | ||
| - if( $indexFile !== $this->lastPack ) { | ||
| - $offset = $this->findInIdx( $indexFile, $binarySha ); | ||
| - | ||
| - if( $offset !== 0 ) { | ||
| - $this->lastPack = $indexFile; | ||
| - $result = [ | ||
| - 'file' => str_replace( '.idx', '.pack', $indexFile ), | ||
| - 'offset' => $offset | ||
| - ]; | ||
| - $found = true; | ||
| - } | ||
| - } | ||
| - | ||
| - $idx++; | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - 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( $found === -1 && $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; | ||
| - } | ||
| - } | ||
| - | ||
| - 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; | ||
| - $result = ''; | ||
| - | ||
| - if( $type === 6 ) { | ||
| - $result = $this->handleOfsDelta( $handle, $offset, $size, $cap ); | ||
| - } elseif( $type === 7 ) { | ||
| - $result = $this->handleRefDelta( $handle, $size, $cap ); | ||
| - } else { | ||
| - $result = $this->decompressToString( $handle, $cap ); | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function streamPackEntry( | ||
| - $handle, | ||
| - int $offset, | ||
| - int $size, | ||
| - callable $callback, | ||
| - int $depth = 0 | ||
| - ): bool { | ||
| - fseek( $handle, $offset ); | ||
| - $header = $this->readVarInt( $handle ); | ||
| - $type = ($header['byte'] >> 4) & 7; | ||
| - $result = false; | ||
| - | ||
| - if( $type === 6 || $type === 7 ) { | ||
| - $result = $this->streamDeltaObject( | ||
| - $handle, | ||
| - $offset, | ||
| - $type, | ||
| - $callback, | ||
| - $depth | ||
| - ); | ||
| - } else { | ||
| - $result = $this->streamDecompression( $handle, $callback ); | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function streamDeltaObject( | ||
| - $handle, | ||
| - int $offset, | ||
| - int $type, | ||
| - callable $callback, | ||
| - int $depth = 0 | ||
| - ): bool { | ||
| - $result = false; | ||
| - | ||
| - if( $depth < self::MAX_DEPTH ) { | ||
| - fseek( $handle, $offset ); | ||
| - $this->readVarInt( $handle ); | ||
| - | ||
| - if( $type === 6 ) { | ||
| - $neg = $this->readOffsetDelta( $handle ); | ||
| - $deltaPos = ftell( $handle ); | ||
| - $base = ''; | ||
| - $baseSize = $this->extractPackedSize( $handle, $offset - $neg ); | ||
| - | ||
| - if( $baseSize <= self::MAX_BASE_RAM ) { | ||
| - $this->streamPackEntry( | ||
| - $handle, | ||
| - $offset - $neg, | ||
| - 0, | ||
| - function( $c ) use ( &$base ) { | ||
| - $base .= $c; | ||
| - }, | ||
| - $depth + 1 | ||
| - ); | ||
| - | ||
| - fseek( $handle, $deltaPos ); | ||
| - $result = $this->applyDeltaStream( $handle, $base, $callback ); | ||
| - } else { | ||
| - error_log( | ||
| - "[GitPacks] ofs base too large for RAM path: $baseSize" | ||
| - ); | ||
| - } | ||
| - } else { | ||
| - $baseSha = bin2hex( fread( $handle, 20 ) ); | ||
| - $baseSize = $this->getSize( $baseSha ); | ||
| - | ||
| - if( $baseSize <= self::MAX_BASE_RAM ) { | ||
| - $base = ''; | ||
| - | ||
| - if( $this->streamInternal( | ||
| - $baseSha, | ||
| - function( $c ) use ( &$base ) { | ||
| - $base .= $c; | ||
| - }, | ||
| - $depth + 1 | ||
| - ) ) { | ||
| - $result = $this->applyDeltaStream( $handle, $base, $callback ); | ||
| - } | ||
| - } else { | ||
| - error_log( | ||
| - "[GitPacks] ref base too large: $baseSize (sha=$baseSha)" | ||
| - ); | ||
| - } | ||
| - } | ||
| - } else { | ||
| - error_log( "[GitPacks] delta depth limit exceeded at offset $offset" ); | ||
| - } | ||
| - | ||
| - return $result; | ||
| - } | ||
| - | ||
| - private function applyDeltaStream( | ||
| - $handle, | ||
| - string $base, | ||
| - callable $callback | ||
| - ): bool { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $state = 0; | ||
| - $buffer = ''; | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $buffer .= $data; | ||
| - $doneBuffer = false; | ||
| - | ||
| - while( !$doneBuffer ) { | ||
| - $len = strlen( $buffer ); | ||
| - | ||
| - if( $len === 0 ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - if( $state < 2 ) { | ||
| - $pos = 0; | ||
| - | ||
| - while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { | ||
| - $pos++; | ||
| - } | ||
| - | ||
| - if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $buffer = substr( $buffer, $pos + 1 ); | ||
| - $state++; | ||
| - } | ||
| - } else { | ||
| - $op = ord( $buffer[0] ); | ||
| - | ||
| - if( $op & 128 ) { | ||
| - $need = $this->getCopyInstructionSize( $op ); | ||
| - | ||
| - if( $len < 1 + $need ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $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 ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $callback( substr( $buffer, 1, $ln ) ); | ||
| - $buffer = substr( $buffer, 1 + $ln ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - private function streamPackEntryGenerator( | ||
| - $handle, | ||
| - int $offset, | ||
| - int $depth | ||
| - ): Generator { | ||
| - fseek( $handle, $offset ); | ||
| - $header = $this->readVarInt( $handle ); | ||
| - $type = ($header['byte'] >> 4) & 7; | ||
| - | ||
| - if( $type === 6 || $type === 7 ) { | ||
| - yield from $this->streamDeltaObjectGenerator( | ||
| - $handle, | ||
| - $offset, | ||
| - $type, | ||
| - $depth | ||
| - ); | ||
| - } else { | ||
| - yield from $this->streamDecompressionGenerator( $handle ); | ||
| - } | ||
| - } | ||
| - | ||
| - private function resolveBaseToTempFile( | ||
| - $packHandle, | ||
| - int $baseOffset, | ||
| - int $depth | ||
| - ) { | ||
| - $tmpHandle = tmpfile(); | ||
| - | ||
| - if( $tmpHandle ) { | ||
| - foreach( $this->streamPackEntryGenerator( | ||
| - $packHandle, | ||
| - $baseOffset, | ||
| - $depth + 1 | ||
| - ) as $chunk ) { | ||
| - fwrite( $tmpHandle, $chunk ); | ||
| - } | ||
| - | ||
| - rewind( $tmpHandle ); | ||
| - } else { | ||
| - error_log( | ||
| - "[GitPacks] tmpfile failed for ofs-delta base at $baseOffset" | ||
| - ); | ||
| - } | ||
| - | ||
| - return $tmpHandle; | ||
| - } | ||
| - | ||
| - private function streamDeltaObjectGenerator( | ||
| - $handle, | ||
| - int $offset, | ||
| - int $type, | ||
| - int $depth | ||
| - ): Generator { | ||
| - if( $depth < self::MAX_DEPTH ) { | ||
| - fseek( $handle, $offset ); | ||
| - $this->readVarInt( $handle ); | ||
| - | ||
| - if( $type === 6 ) { | ||
| - $neg = $this->readOffsetDelta( $handle ); | ||
| - $deltaPos = ftell( $handle ); | ||
| - $baseSize = $this->extractPackedSize( $handle, $offset - $neg ); | ||
| - | ||
| - if( $baseSize > self::MAX_BASE_RAM ) { | ||
| - $tmpHandle = $this->resolveBaseToTempFile( | ||
| - $handle, | ||
| - $offset - $neg, | ||
| - $depth | ||
| - ); | ||
| - | ||
| - if( $tmpHandle !== null ) { | ||
| - fseek( $handle, $deltaPos ); | ||
| - yield from $this->applyDeltaStreamFromFileGenerator( | ||
| - $handle, | ||
| - $tmpHandle | ||
| - ); | ||
| - | ||
| - fclose( $tmpHandle ); | ||
| - } | ||
| - } else { | ||
| - $base = ''; | ||
| - | ||
| - $this->streamPackEntry( | ||
| - $handle, | ||
| - $offset - $neg, | ||
| - 0, | ||
| - function( $c ) use ( &$base ) { | ||
| - $base .= $c; | ||
| - }, | ||
| - $depth + 1 | ||
| - ); | ||
| - | ||
| - fseek( $handle, $deltaPos ); | ||
| - yield from $this->applyDeltaStreamGenerator( $handle, $base ); | ||
| - } | ||
| - } else { | ||
| - $baseSha = bin2hex( fread( $handle, 20 ) ); | ||
| - $baseSize = $this->getSize( $baseSha ); | ||
| - | ||
| - if( $baseSize > self::MAX_BASE_RAM ) { | ||
| - $tmpHandle = tmpfile(); | ||
| - | ||
| - if( $tmpHandle ) { | ||
| - $written = $this->streamInternal( | ||
| - $baseSha, | ||
| - function( $c ) use ( $tmpHandle ) { | ||
| - fwrite( $tmpHandle, $c ); | ||
| - }, | ||
| - $depth + 1 | ||
| - ); | ||
| - | ||
| - if( $written ) { | ||
| - rewind( $tmpHandle ); | ||
| - yield from $this->applyDeltaStreamFromFileGenerator( | ||
| - $handle, | ||
| - $tmpHandle | ||
| - ); | ||
| - } | ||
| - | ||
| - fclose( $tmpHandle ); | ||
| - } else { | ||
| - error_log( | ||
| - "[GitPacks] tmpfile() failed for ref-delta (sha=$baseSha)" | ||
| - ); | ||
| - } | ||
| - } else { | ||
| - $base = ''; | ||
| - | ||
| - if( $this->streamInternal( | ||
| - $baseSha, | ||
| - function( $c ) use ( &$base ) { | ||
| - $base .= $c; | ||
| - }, | ||
| - $depth + 1 | ||
| - ) ) { | ||
| - yield from $this->applyDeltaStreamGenerator( $handle, $base ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } else { | ||
| - error_log( "[GitPacks] delta depth limit exceeded at offset $offset" ); | ||
| - } | ||
| - } | ||
| - | ||
| - private function applyDeltaStreamGenerator( | ||
| - $handle, | ||
| - string $base | ||
| - ): Generator { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $state = 0; | ||
| - $buffer = ''; | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $buffer .= $data; | ||
| - $doneBuffer = false; | ||
| - | ||
| - while( !$doneBuffer ) { | ||
| - $len = strlen( $buffer ); | ||
| - | ||
| - if( $len === 0 ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - if( $state < 2 ) { | ||
| - $pos = 0; | ||
| - | ||
| - while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { | ||
| - $pos++; | ||
| - } | ||
| - | ||
| - if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $buffer = substr( $buffer, $pos + 1 ); | ||
| - $state++; | ||
| - } | ||
| - } else { | ||
| - $op = ord( $buffer[0] ); | ||
| - | ||
| - if( $op & 128 ) { | ||
| - $need = $this->getCopyInstructionSize( $op ); | ||
| - | ||
| - if( $len < 1 + $need ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $info = $this->parseCopyInstruction( $op, $buffer, 1 ); | ||
| - | ||
| - yield substr( $base, $info['off'], $info['len'] ); | ||
| - $buffer = substr( $buffer, 1 + $need ); | ||
| - } | ||
| - } else { | ||
| - $ln = $op & 127; | ||
| - | ||
| - if( $len < 1 + $ln ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - yield substr( $buffer, 1, $ln ); | ||
| - $buffer = substr( $buffer, 1 + $ln ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function applyDeltaStreamFromFileGenerator( | ||
| - $deltaHandle, | ||
| - $baseHandle | ||
| - ): Generator { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $state = 0; | ||
| - $buffer = ''; | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $deltaHandle ) ) { | ||
| - $chunk = fread( $deltaHandle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $buffer .= $data; | ||
| - $doneBuffer = false; | ||
| - | ||
| - while( !$doneBuffer ) { | ||
| - $len = strlen( $buffer ); | ||
| - | ||
| - if( $len === 0 ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - if( $state < 2 ) { | ||
| - $pos = 0; | ||
| - | ||
| - while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { | ||
| - $pos++; | ||
| - } | ||
| - | ||
| - if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $buffer = substr( $buffer, $pos + 1 ); | ||
| - $state++; | ||
| - } | ||
| - } else { | ||
| - $op = ord( $buffer[0] ); | ||
| - | ||
| - if( $op & 128 ) { | ||
| - $need = $this->getCopyInstructionSize( $op ); | ||
| - | ||
| - if( $len < 1 + $need ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - $info = $this->parseCopyInstruction( $op, $buffer, 1 ); | ||
| - | ||
| - fseek( $baseHandle, $info['off'] ); | ||
| - $remaining = $info['len']; | ||
| - | ||
| - while( $remaining > 0 ) { | ||
| - $slice = fread( $baseHandle, min( 65536, $remaining ) ); | ||
| - | ||
| - if( $slice === false || $slice === '' ) { | ||
| - $remaining = 0; | ||
| - } else { | ||
| - yield $slice; | ||
| - $remaining -= strlen( $slice ); | ||
| - } | ||
| - } | ||
| - | ||
| - $buffer = substr( $buffer, 1 + $need ); | ||
| - } | ||
| - } else { | ||
| - $ln = $op & 127; | ||
| - | ||
| - if( $len < 1 + $ln ) { | ||
| - $doneBuffer = true; | ||
| - } | ||
| - | ||
| - if( !$doneBuffer ) { | ||
| - yield substr( $buffer, 1, $ln ); | ||
| - $buffer = substr( $buffer, 1 + $ln ); | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function streamDecompressionGenerator( $handle ): Generator { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - yield $data; | ||
| - } | ||
| - | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - private function streamDecompression( $handle, callable $callback ): bool { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $callback( $data ); | ||
| - } | ||
| - | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - | ||
| - return true; | ||
| - } | ||
| - | ||
| - private function decompressToString( | ||
| - $handle, | ||
| - int $cap = 0 | ||
| - ): string { | ||
| - $stream = CompressionStream::createInflater(); | ||
| - $res = ''; | ||
| - $done = false; | ||
| - | ||
| - while( !$done && !feof( $handle ) ) { | ||
| - $chunk = fread( $handle, 8192 ); | ||
| - $done = $chunk === false || $chunk === ''; | ||
| - | ||
| - if( !$done ) { | ||
| - $data = $stream->pump( $chunk ); | ||
| - | ||
| - if( $data !== '' ) { | ||
| - $res .= $data; | ||
| - } | ||
| - | ||
| - if( $cap > 0 && strlen( $res ) >= $cap ) { | ||
| - $res = substr( $res, 0, $cap ); | ||
| - $done = true; | ||
| - } | ||
| - | ||
| - if( !$done ) { | ||
| - $done = $stream->finished(); | ||
| - } | ||
| - } | ||
| - } | ||
| - | ||
| - return $res; | ||
| - } | ||
| - | ||
| - private function extractPackedSize( $packPathOrHandle, int $offset ): int { | ||
| - $handle = is_resource( $packPathOrHandle ) | ||
| - ? $packPathOrHandle | ||
| - : $this->getHandle( $packPathOrHandle ); | ||
| - $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 ); | ||
| - $done = false; | ||
| - | ||
| - while( !$done && $pos < $len ) { | ||
| - if( $cap > 0 && strlen( $out ) >= $cap ) { | ||
| - $done = true; | ||
| - } | ||
| - | ||
| - if( !$done ) { | ||
| - $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 === 0 ? 0x10000 : $len, | ||
| - '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; | ||
| + $result = false; | ||
| + | ||
| + foreach( $this->streamGenerator( $sha ) as $chunk ) { | ||
| + $callback( $chunk ); | ||
| + $result = true; | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + public function streamGenerator( string $sha ): Generator { | ||
| + yield from $this->streamShaGenerator( $sha, 0 ); | ||
| + } | ||
| + | ||
| + private function streamShaGenerator( string $sha, int $depth ): Generator { | ||
| + $info = $this->findPackInfo( $sha ); | ||
| + | ||
| + if( $info['offset'] !== 0 ) { | ||
| + $handle = $this->getHandle( $info['file'] ); | ||
| + | ||
| + if( $handle ) { | ||
| + yield from $this->streamPackEntryGenerator( | ||
| + $handle, | ||
| + $info['offset'], | ||
| + $depth | ||
| + ); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + 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' => '' ]; | ||
| + | ||
| + if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) { | ||
| + $binarySha = hex2bin( $sha ); | ||
| + | ||
| + 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 ) { | ||
| + $count = count( $this->packFiles ); | ||
| + $idx = 0; | ||
| + $found = false; | ||
| + | ||
| + while( !$found && $idx < $count ) { | ||
| + $indexFile = $this->packFiles[$idx]; | ||
| + | ||
| + if( $indexFile !== $this->lastPack ) { | ||
| + $offset = $this->findInIdx( $indexFile, $binarySha ); | ||
| + | ||
| + if( $offset !== 0 ) { | ||
| + $this->lastPack = $indexFile; | ||
| + $result = [ | ||
| + 'file' => str_replace( '.idx', '.pack', $indexFile ), | ||
| + 'offset' => $offset | ||
| + ]; | ||
| + $found = true; | ||
| + } | ||
| + } | ||
| + | ||
| + $idx++; | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + 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( $found === -1 && $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; | ||
| + } | ||
| + } | ||
| + | ||
| + 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; | ||
| + $result = ''; | ||
| + | ||
| + if( $type === 6 ) { | ||
| + $result = $this->handleOfsDelta( $handle, $offset, $size, $cap ); | ||
| + } elseif( $type === 7 ) { | ||
| + $result = $this->handleRefDelta( $handle, $size, $cap ); | ||
| + } else { | ||
| + $result = $this->decompressToString( $handle, $cap ); | ||
| + } | ||
| + | ||
| + return $result; | ||
| + } | ||
| + | ||
| + private function streamPackEntryGenerator( | ||
| + $handle, | ||
| + int $offset, | ||
| + int $depth | ||
| + ): Generator { | ||
| + fseek( $handle, $offset ); | ||
| + $header = $this->readVarInt( $handle ); | ||
| + $type = ($header['byte'] >> 4) & 7; | ||
| + | ||
| + if( $type === 6 || $type === 7 ) { | ||
| + yield from $this->streamDeltaObjectGenerator( | ||
| + $handle, | ||
| + $offset, | ||
| + $type, | ||
| + $depth | ||
| + ); | ||
| + } else { | ||
| + yield from $this->streamDecompressionGenerator( $handle ); | ||
| + } | ||
| + } | ||
| + | ||
| + private function resolveBaseToTempFile( | ||
| + $packHandle, | ||
| + int $baseOffset, | ||
| + int $depth | ||
| + ) { | ||
| + $tmpHandle = tmpfile(); | ||
| + | ||
| + if( $tmpHandle !== false ) { | ||
| + foreach( $this->streamPackEntryGenerator( | ||
| + $packHandle, | ||
| + $baseOffset, | ||
| + $depth + 1 | ||
| + ) as $chunk ) { | ||
| + fwrite( $tmpHandle, $chunk ); | ||
| + } | ||
| + | ||
| + rewind( $tmpHandle ); | ||
| + } else { | ||
| + error_log( | ||
| + "[GitPacks] tmpfile failed for ofs-delta base at $baseOffset" | ||
| + ); | ||
| + } | ||
| + | ||
| + return $tmpHandle; | ||
| + } | ||
| + | ||
| + private function streamDeltaObjectGenerator( | ||
| + $handle, | ||
| + int $offset, | ||
| + int $type, | ||
| + int $depth | ||
| + ): Generator { | ||
| + if( $depth < self::MAX_DEPTH ) { | ||
| + fseek( $handle, $offset ); | ||
| + $this->readVarInt( $handle ); | ||
| + | ||
| + if( $type === 6 ) { | ||
| + $neg = $this->readOffsetDelta( $handle ); | ||
| + $deltaPos = ftell( $handle ); | ||
| + $baseSize = $this->extractPackedSize( $handle, $offset - $neg ); | ||
| + | ||
| + if( $baseSize > self::MAX_BASE_RAM ) { | ||
| + $tmpHandle = $this->resolveBaseToTempFile( | ||
| + $handle, | ||
| + $offset - $neg, | ||
| + $depth | ||
| + ); | ||
| + | ||
| + if( $tmpHandle !== false ) { | ||
| + fseek( $handle, $deltaPos ); | ||
| + yield from $this->applyDeltaStreamGenerator( | ||
| + $handle, | ||
| + $tmpHandle | ||
| + ); | ||
| + | ||
| + fclose( $tmpHandle ); | ||
| + } | ||
| + } else { | ||
| + $base = ''; | ||
| + | ||
| + foreach( $this->streamPackEntryGenerator( | ||
| + $handle, | ||
| + $offset - $neg, | ||
| + $depth + 1 | ||
| + ) as $chunk ) { | ||
| + $base .= $chunk; | ||
| + } | ||
| + | ||
| + fseek( $handle, $deltaPos ); | ||
| + yield from $this->applyDeltaStreamGenerator( $handle, $base ); | ||
| + } | ||
| + } else { | ||
| + $baseSha = bin2hex( fread( $handle, 20 ) ); | ||
| + $baseSize = $this->getSize( $baseSha ); | ||
| + | ||
| + if( $baseSize > self::MAX_BASE_RAM ) { | ||
| + $tmpHandle = tmpfile(); | ||
| + | ||
| + if( $tmpHandle !== false ) { | ||
| + $written = false; | ||
| + | ||
| + foreach( $this->streamShaGenerator( | ||
| + $baseSha, | ||
| + $depth + 1 | ||
| + ) as $chunk ) { | ||
| + fwrite( $tmpHandle, $chunk ); | ||
| + $written = true; | ||
| + } | ||
| + | ||
| + if( $written ) { | ||
| + rewind( $tmpHandle ); | ||
| + yield from $this->applyDeltaStreamGenerator( | ||
| + $handle, | ||
| + $tmpHandle | ||
| + ); | ||
| + } | ||
| + | ||
| + fclose( $tmpHandle ); | ||
| + } else { | ||
| + error_log( | ||
| + "[GitPacks] tmpfile() failed for ref-delta (sha=$baseSha)" | ||
| + ); | ||
| + } | ||
| + } else { | ||
| + $base = ''; | ||
| + $written = false; | ||
| + | ||
| + foreach( $this->streamShaGenerator( | ||
| + $baseSha, | ||
| + $depth + 1 | ||
| + ) as $chunk ) { | ||
| + $base .= $chunk; | ||
| + $written = true; | ||
| + } | ||
| + | ||
| + if( $written ) { | ||
| + yield from $this->applyDeltaStreamGenerator( $handle, $base ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } else { | ||
| + error_log( "[GitPacks] delta depth limit exceeded at offset $offset" ); | ||
| + } | ||
| + } | ||
| + | ||
| + private function applyDeltaStreamGenerator( | ||
| + $handle, | ||
| + $base | ||
| + ): Generator { | ||
| + $stream = CompressionStream::createInflater(); | ||
| + $state = 0; | ||
| + $buffer = ''; | ||
| + $done = false; | ||
| + $isFile = is_resource( $base ); | ||
| + | ||
| + while( !$done && !feof( $handle ) ) { | ||
| + $chunk = fread( $handle, 8192 ); | ||
| + $done = $chunk === false || $chunk === ''; | ||
| + | ||
| + if( !$done ) { | ||
| + $data = $stream->pump( $chunk ); | ||
| + | ||
| + if( $data !== '' ) { | ||
| + $buffer .= $data; | ||
| + $doneBuffer = false; | ||
| + | ||
| + while( !$doneBuffer ) { | ||
| + $len = strlen( $buffer ); | ||
| + | ||
| + if( $len === 0 ) { | ||
| + $doneBuffer = true; | ||
| + } | ||
| + | ||
| + if( !$doneBuffer ) { | ||
| + if( $state < 2 ) { | ||
| + $pos = 0; | ||
| + | ||
| + while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { | ||
| + $pos++; | ||
| + } | ||
| + | ||
| + if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { | ||
| + $doneBuffer = true; | ||
| + } | ||
| + | ||
| + if( !$doneBuffer ) { | ||
| + $buffer = substr( $buffer, $pos + 1 ); | ||
| + $state++; | ||
| + } | ||
| + } else { | ||
| + $op = ord( $buffer[0] ); | ||
| + | ||
| + if( $op & 128 ) { | ||
| + $need = $this->getCopyInstructionSize( $op ); | ||
| + | ||
| + if( $len < 1 + $need ) { | ||
| + $doneBuffer = true; | ||
| + } | ||
| + | ||
| + if( !$doneBuffer ) { | ||
| + $info = $this->parseCopyInstruction( $op, $buffer, 1 ); | ||
| + | ||
| + if( $isFile ) { | ||
| + fseek( $base, $info['off'] ); | ||
| + $rem = $info['len']; | ||
| + | ||
| + while( $rem > 0 ) { | ||
| + $slc = fread( $base, min( 65536, $rem ) ); | ||
| + | ||
| + if( $slc === false || $slc === '' ) { | ||
| + $rem = 0; | ||
| + } else { | ||
| + yield $slc; | ||
| + $rem -= strlen( $slc ); | ||
| + } | ||
| + } | ||
| + } else { | ||
| + yield substr( $base, $info['off'], $info['len'] ); | ||
| + } | ||
| + | ||
| + $buffer = substr( $buffer, 1 + $need ); | ||
| + } | ||
| + } else { | ||
| + $ln = $op & 127; | ||
| + | ||
| + if( $len < 1 + $ln ) { | ||
| + $doneBuffer = true; | ||
| + } | ||
| + | ||
| + if( !$doneBuffer ) { | ||
| + yield substr( $buffer, 1, $ln ); | ||
| + $buffer = substr( $buffer, 1 + $ln ); | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + $done = $stream->finished(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function streamDecompressionGenerator( $handle ): Generator { | ||
| + $stream = CompressionStream::createInflater(); | ||
| + $done = false; | ||
| + | ||
| + while( !$done && !feof( $handle ) ) { | ||
| + $chunk = fread( $handle, 8192 ); | ||
| + $done = $chunk === false || $chunk === ''; | ||
| + | ||
| + if( !$done ) { | ||
| + $data = $stream->pump( $chunk ); | ||
| + | ||
| + if( $data !== '' ) { | ||
| + yield $data; | ||
| + } | ||
| + | ||
| + $done = $stream->finished(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + private function decompressToString( | ||
| + $handle, | ||
| + int $cap = 0 | ||
| + ): string { | ||
| + $stream = CompressionStream::createInflater(); | ||
| + $res = ''; | ||
| + $done = false; | ||
| + | ||
| + while( !$done && !feof( $handle ) ) { | ||
| + $chunk = fread( $handle, 8192 ); | ||
| + $done = $chunk === false || $chunk === ''; | ||
| + | ||
| + if( !$done ) { | ||
| + $data = $stream->pump( $chunk ); | ||
| + | ||
| + if( $data !== '' ) { | ||
| + $res .= $data; | ||
| + } | ||
| + | ||
| + if( $cap > 0 && strlen( $res ) >= $cap ) { | ||
| + $res = substr( $res, 0, $cap ); | ||
| + $done = true; | ||
| + } | ||
| + | ||
| + if( !$done ) { | ||
| + $done = $stream->finished(); | ||
| + } | ||
| + } | ||
| + } | ||
| + | ||
| + return $res; | ||
| + } | ||
| + | ||
| + private function extractPackedSize( $packPathOrHandle, int $offset ): int { | ||
| + $handle = is_resource( $packPathOrHandle ) | ||
| + ? $packPathOrHandle | ||
| + : $this->getHandle( $packPathOrHandle ); | ||
| + $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 ); | ||
| + $done = false; | ||
| + | ||
| + while( !$done && $pos < $len ) { | ||
| + if( $cap > 0 && strlen( $out ) >= $cap ) { | ||
| + $done = true; | ||
| + } | ||
| + | ||
| + if( !$done ) { | ||
| + $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 === 0 ? 0x10000 : $len, | ||
| + '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; |
| <?php | ||
| +require_once __DIR__ . '/BufferedFileReader.php'; | ||
| + | ||
| class GitRefs { | ||
| private string $repoPath; | ||
| $headFile = "{$this->repoPath}/HEAD"; | ||
| - if( $input === 'HEAD' && file_exists( $headFile ) ) { | ||
| - $head = trim( file_get_contents( $headFile ) ); | ||
| + if( $input === 'HEAD' && is_file( $headFile ) ) { | ||
| + $size = filesize( $headFile ); | ||
| + $head = ''; | ||
| + | ||
| + if( $size > 0 ) { | ||
| + $reader = BufferedFileReader::open( $headFile ); | ||
| + $head = trim( $reader->read( $size ) ); | ||
| + } | ||
| + | ||
| $result = strpos( $head, 'ref: ' ) === 0 | ||
| ? $this->resolve( substr( $head, 5 ) ) | ||
| $this->traverseDirectory( $path, $callback, $name ); | ||
| } elseif( is_file( $path ) ) { | ||
| - $sha = trim( file_get_contents( $path ) ); | ||
| + $size = filesize( $path ); | ||
| - if( preg_match( '/^[0-9a-f]{40}$/', $sha ) ) { | ||
| - $callback( $name, $sha ); | ||
| + if( $size > 0 ) { | ||
| + $reader = BufferedFileReader::open( $path ); | ||
| + $sha = trim( $reader->read( $size ) ); | ||
| + | ||
| + if( preg_match( '/^[0-9a-f]{40}$/', $sha ) ) { | ||
| + $callback( $name, $sha ); | ||
| + } | ||
| } | ||
| } | ||
| $path = "{$this->repoPath}/$ref"; | ||
| - if( file_exists( $path ) ) { | ||
| - $result = trim( file_get_contents( $path ) ); | ||
| + if( is_file( $path ) ) { | ||
| + $size = filesize( $path ); | ||
| + | ||
| + if( $size > 0 ) { | ||
| + $reader = BufferedFileReader::open( $path ); | ||
| + $result = trim( $reader->read( $size ) ); | ||
| + } | ||
| + | ||
| break; | ||
| } | ||
| } | ||
| if( $result === '' ) { | ||
| $packedPath = "{$this->repoPath}/packed-refs"; | ||
| - if( file_exists( $packedPath ) ) { | ||
| + if( is_file( $packedPath ) ) { | ||
| $result = $this->findInPackedRefs( $packedPath, $input ); | ||
| } | ||
| private function findInPackedRefs( string $path, string $input ): string { | ||
| $targets = [$input, "refs/heads/$input", "refs/tags/$input"]; | ||
| - $lines = file( $path ); | ||
| + $size = filesize( $path ); | ||
| + $lines = []; | ||
| $result = ''; | ||
| + | ||
| + if( $size > 0 ) { | ||
| + $reader = BufferedFileReader::open( $path ); | ||
| + $lines = explode( "\n", $reader->read( $size ) ); | ||
| + } | ||
| foreach( $lines as $line ) { | ||
| - if( $line[0] !== '#' && $line[0] !== '^' ) { | ||
| + if( $line !== '' && $line[0] !== '#' && $line[0] !== '^' ) { | ||
| $parts = explode( ' ', trim( $line ) ); | ||
| Delta | 1411 lines added, 1608 lines removed, 197-line decrease |
|---|