| | 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 ); |
| | - $entry = [ 'sha' => '', 'mode' => '' ]; |
| | - |
| | - $this->parseTreeData( |
| | - $data, |
| | - function( $file, $n, $sha, $mode ) use ( $name, &$entry ) { |
| | - if( $file->isName( $name ) ) { |
| | - $entry = [ 'sha' => $sha, 'mode' => $mode ]; |
| | - |
| | - return false; |
| | - } |
| | - } |
| | - ); |
| | - |
| | - 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 { |
| | - $this->parseTreeData( |
| | - $data, |
| | - function( $file, $n, $s, $m ) use ( $callback ) { |
| | - $callback( $file ); |
| | - } |
| | - ); |
| | - } |
| | - |
| | - public function parseTreeData( string $data, callable $callback ): void { |
| | - $pos = 0; |
| | - $len = strlen( $data ); |
| | - |
| | - while( $pos < $len ) { |
| | - $space = strpos( $data, ' ', $pos ); |
| | - $eos = strpos( $data, "\0", $space ); |
| | - |
| | - if( $space === false || $eos === false || $eos + 21 > $len ) { |
| | - break; |
| | - } |
| | - |
| | - $mode = substr( $data, $pos, $space - $pos ); |
| | - $name = substr( $data, $space + 1, $eos - $space - 1 ); |
| | - $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); |
| | - $dir = $mode === '40000' || $mode === '040000'; |
| | - $isSub = $mode === '160000'; |
| | - |
| | - $file = new File( |
| | - $name, |
| | - $sha, |
| | - $mode, |
| | - 0, |
| | - $dir || $isSub ? 0 : $this->getObjectSize( $sha ), |
| | - $dir || $isSub ? '' : $this->peek( $sha ) |
| | - ); |
| | - |
| | - if( $callback( $file, $name, $sha, $mode ) === false ) { |
| | - break; |
| | - } |
| | - |
| | - $pos = $eos + 21; |
| | - } |
| | - } |
| | - |
| | - 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; |
| | - } |
| | - ); |
| | - } |
| | - |
| | - return $size; |
| | - } |
| | - |
| | - public function collectObjects( array $wants, array $haves = [] ): array { |
| | - $objs = $this->traverseObjects( $wants ); |
| | - $result = []; |
| | - |
| | - if( !empty( $haves ) ) { |
| | - $haveObjs = $this->traverseObjects( $haves ); |
| | - |
| | - foreach( $haveObjs as $sha => $type ) { |
| | - if( isset( $objs[$sha] ) ) { |
| | - unset( $objs[$sha] ); |
| | - } |
| | - } |
| | - } |
| | - |
| | - $result = $objs; |
| | - |
| | - return $result; |
| | - } |
| | - |
| | - private function traverseObjects( array $roots ): array { |
| | - $objs = []; |
| | - $queue = []; |
| | - |
| | - foreach( $roots as $sha ) { |
| | - $queue[] = [ 'sha' => $sha, 'type' => 0 ]; |
| | - } |
| | - |
| | - while( !empty( $queue ) ) { |
| | - $item = array_pop( $queue ); |
| | - $sha = $item['sha']; |
| | - $type = $item['type']; |
| | - |
| | - if( isset( $objs[$sha] ) ) { |
| | - continue; |
| | - } |
| | - |
| | - $data = ''; |
| | - |
| | - if( $type !== 3 ) { |
| | - $data = $this->read( $sha ); |
| | - |
| | - if( $type === 0 ) { |
| | - $type = $this->getObjectType( $data ); |
| | - } |
| | - } |
| | - |
| | - $objs[$sha] = $type; |
| | - |
| | - if( $type === 1 ) { |
| | - $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ); |
| | - |
| | - if( $hasTree ) { |
| | - $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; |
| | - } |
| | - |
| | - $hasParents = preg_match_all( |
| | - '/^parent ([0-9a-f]{40})/m', |
| | - $data, |
| | - $m |
| | - ); |
| | - |
| | - if( $hasParents ) { |
| | - foreach( $m[1] as $parentSha ) { |
| | - $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; |
| | - } |
| | - } |
| | - } elseif( $type === 2 ) { |
| | - $pos = 0; |
| | - $len = strlen( $data ); |
| | - |
| | - while( $pos < $len ) { |
| | - $space = strpos( $data, ' ', $pos ); |
| | - $eos = strpos( $data, "\0", $space ); |
| | - |
| | - if( $space === false || $eos === false ) { |
| | - break; |
| | - } |
| | - |
| | - $mode = substr( $data, $pos, $space - $pos ); |
| | - $hash = bin2hex( substr( $data, $eos + 1, 20 ) ); |
| | - |
| | - if( $mode !== '160000' ) { |
| | - $isDir = $mode === '40000' || $mode === '040000'; |
| | - $queue[] = [ 'sha' => $hash, 'type' => $isDir ? 2 : 3 ]; |
| | - } |
| | - |
| | - $pos = $eos + 21; |
| | - } |
| | - } elseif( $type === 4 ) { |
| | - $isTagTgt = preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ); |
| | - |
| | - if( $isTagTgt ) { |
| | - $nextType = 1; |
| | - |
| | - if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) { |
| | - $map = [ |
| | - 'commit' => 1, |
| | - 'tree' => 2, |
| | - 'blob' => 3, |
| | - 'tag' => 4 |
| | - ]; |
| | - $nextType = $map[$t[1]] ?? 1; |
| | - } |
| | - |
| | - $queue[] = [ 'sha' => $m[1], 'type' => $nextType ]; |
| | - } |
| | - } |
| | - } |
| | - |
| | - return $objs; |
| | - } |
| | - |
| | - private function getObjectType( string $data ): int { |
| | - $isTree = strpos( $data, "tree " ) === 0; |
| | - $isObj = strpos( $data, "object " ) === 0; |
| | - $result = 3; |
| | - |
| | - if( $isTree ) { |
| | - $result = 1; |
| | - } elseif( $isObj ) { |
| | - $result = 4; |
| | - } elseif( $this->isTreeData( $data ) ) { |
| | - $result = 2; |
| | - } |
| | - |
| | - return $result; |
| | - } |
| | -} |
| | - |
| | -class MissingFile extends File { |
| | - public function __construct() { |
| | - parent::__construct( '', '', '0', 0, 0, '' ); |
| | - } |
| | - |
| | - public function emitRawHeaders(): void { |
| | - header( "HTTP/1.1 404 Not Found" ); |
| | - exit; |
| | - } |
| | -} |
| | + foreach( $this->streamCompressed( $sha ) as $compressed ) { |
| | + hash_update( $ctx, $compressed ); |
| | + yield $compressed; |
| | + } |
| | + } |
| | + |
| | + yield hash_final( $ctx, true ); |
| | + } |
| | + |
| | + private function streamCompressed( string $sha ): Generator { |
| | + $yielded = false; |
| | + |
| | + foreach( $this->packs->streamRawCompressed( $sha ) as $chunk ) { |
| | + $yielded = true; |
| | + yield $chunk; |
| | + } |
| | + |
| | + if( !$yielded ) { |
| | + $deflate = deflate_init( ZLIB_ENCODING_DEFLATE ); |
| | + |
| | + foreach( $this->slurpChunks( $sha ) as $raw ) { |
| | + $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH ); |
| | + |
| | + if( $compressed !== '' ) { |
| | + yield $compressed; |
| | + } |
| | + } |
| | + |
| | + $final = deflate_add( $deflate, '', ZLIB_FINISH ); |
| | + |
| | + if( $final !== '' ) { |
| | + yield $final; |
| | + } |
| | + } |
| | + } |
| | + |
| | + 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 ); |
| | + $entry = [ 'sha' => '', 'mode' => '' ]; |
| | + |
| | + $this->parseTreeData( |
| | + $data, |
| | + function( $file, $n, $sha, $mode ) use ( $name, &$entry ) { |
| | + if( $file->isName( $name ) ) { |
| | + $entry = [ 'sha' => $sha, 'mode' => $mode ]; |
| | + |
| | + return false; |
| | + } |
| | + } |
| | + ); |
| | + |
| | + 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 { |
| | + $this->parseTreeData( |
| | + $data, |
| | + function( $file, $n, $s, $m ) use ( $callback ) { |
| | + $callback( $file ); |
| | + } |
| | + ); |
| | + } |
| | + |
| | + public function parseTreeData( string $data, callable $callback ): void { |
| | + $pos = 0; |
| | + $len = strlen( $data ); |
| | + |
| | + while( $pos < $len ) { |
| | + $space = strpos( $data, ' ', $pos ); |
| | + $eos = strpos( $data, "\0", $space ); |
| | + |
| | + if( $space === false || $eos === false || $eos + 21 > $len ) { |
| | + break; |
| | + } |
| | + |
| | + $mode = substr( $data, $pos, $space - $pos ); |
| | + $name = substr( $data, $space + 1, $eos - $space - 1 ); |
| | + $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); |
| | + $dir = $mode === '40000' || $mode === '040000'; |
| | + $isSub = $mode === '160000'; |
| | + |
| | + $file = new File( |
| | + $name, |
| | + $sha, |
| | + $mode, |
| | + 0, |
| | + $dir || $isSub ? 0 : $this->getObjectSize( $sha ), |
| | + $dir || $isSub ? '' : $this->peek( $sha ) |
| | + ); |
| | + |
| | + if( $callback( $file, $name, $sha, $mode ) === false ) { |
| | + break; |
| | + } |
| | + |
| | + $pos = $eos + 21; |
| | + } |
| | + } |
| | + |
| | + 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; |
| | + } |
| | + ); |
| | + } |
| | + |
| | + return $size; |
| | + } |
| | + |
| | + public function collectObjects( array $wants, array $haves = [] ): array { |
| | + $objs = $this->traverseObjects( $wants ); |
| | + $result = []; |
| | + |
| | + if( !empty( $haves ) ) { |
| | + $haveObjs = $this->traverseObjects( $haves ); |
| | + |
| | + foreach( $haveObjs as $sha => $type ) { |
| | + if( isset( $objs[$sha] ) ) { |
| | + unset( $objs[$sha] ); |
| | + } |
| | + } |
| | + } |
| | + |
| | + $result = $objs; |
| | + |
| | + return $result; |
| | + } |
| | + |
| | + private function traverseObjects( array $roots ): array { |
| | + $objs = []; |
| | + $queue = []; |
| | + |
| | + foreach( $roots as $sha ) { |
| | + $queue[] = [ 'sha' => $sha, 'type' => 0 ]; |
| | + } |
| | + |
| | + while( !empty( $queue ) ) { |
| | + $item = array_pop( $queue ); |
| | + $sha = $item['sha']; |
| | + $type = $item['type']; |
| | + |
| | + if( isset( $objs[$sha] ) ) { |
| | + continue; |
| | + } |
| | + |
| | + $data = ''; |
| | + |
| | + if( $type !== 3 ) { |
| | + $data = $this->read( $sha ); |
| | + |
| | + if( $type === 0 ) { |
| | + $type = $this->getObjectType( $data ); |
| | + } |
| | + } |
| | + |
| | + $objs[$sha] = $type; |
| | + |
| | + if( $type === 1 ) { |
| | + $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ); |
| | + |
| | + if( $hasTree ) { |
| | + $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; |
| | + } |
| | + |
| | + $hasParents = preg_match_all( |
| | + '/^parent ([0-9a-f]{40})/m', |
| | + $data, |
| | + $m |
| | + ); |
| | + |
| | + if( $hasParents ) { |
| | + foreach( $m[1] as $parentSha ) { |
| | + $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; |
| | + } |
| | + } |
| | + } elseif( $type === 2 ) { |
| | + $pos = 0; |
| | + $len = strlen( $data ); |
| | + |
| | + while( $pos < $len ) { |
| | + $space = strpos( $data, ' ', $pos ); |
| | + $eos = strpos( $data, "\0", $space ); |
| | + |
| | + if( $space === false || $eos === false ) { |
| | + break; |
| | + } |
| | + |
| | + $mode = substr( $data, $pos, $space - $pos ); |
| | + $hash = bin2hex( substr( $data, $eos + 1, 20 ) ); |
| | + |
| | + if( $mode !== '160000' ) { |
| | + $isDir = $mode === '40000' || $mode === '040000'; |
| | + $queue[] = [ 'sha' => $hash, 'type' => $isDir ? 2 : 3 ]; |
| | + } |
| | + |
| | + $pos = $eos + 21; |
| | + } |
| | + } elseif( $type === 4 ) { |
| | + $isTagTgt = preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ); |
| | + |
| | + if( $isTagTgt ) { |
| | + $nextType = 1; |
| | + |
| | + if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) { |
| | + $map = [ |
| | + 'commit' => 1, |
| | + 'tree' => 2, |
| | + 'blob' => 3, |
| | + 'tag' => 4 |
| | + ]; |
| | + $nextType = $map[$t[1]] ?? 1; |
| | + } |
| | + |
| | + $queue[] = [ 'sha' => $m[1], 'type' => $nextType ]; |
| | + } |
| | + } |
| | + } |
| | + |
| | + return $objs; |
| | + } |
| | + |
| | + private function getObjectType( string $data ): int { |
| | + $isTree = strpos( $data, "tree " ) === 0; |
| | + $isObj = strpos( $data, "object " ) === 0; |
| | + $result = 3; |
| | + |
| | + if( $isTree ) { |
| | + $result = 1; |
| | + } elseif( $isObj ) { |
| | + $result = 4; |
| | + } elseif( $this->isTreeData( $data ) ) { |
| | + $result = 2; |
| | + } |
| | + |
| | + return $result; |
| | + } |
| | +} |
| | + |
| | +class MissingFile extends File { |
| | + public function __construct() { |
| | + parent::__construct( '', '', '0', 0, 0, '' ); |
| | + } |
| | + |
| | + public function emitRawHeaders(): void { |
| | + header( "HTTP/1.1 404 Not Found" ); |
| | + exit; |
| | + } |
| | +} |
| | + |
| | |