| 51 | 51 | private bool $binary; |
| 52 | 52 | |
| 53 | private static ?finfo $finfo = null; |
|
| 54 | ||
| 53 | 55 | public function __construct( |
| 54 | 56 | string $name, |
| ... | ||
| 157 | 159 | default => self::ICON_FILE, |
| 158 | 160 | }); |
| 161 | } |
|
| 162 | ||
| 163 | private static function fileinfo(): finfo { |
|
| 164 | return self::$finfo ??= new finfo( FILEINFO_MIME_TYPE ); |
|
| 159 | 165 | } |
| 160 | 166 | |
| 161 | 167 | private function detectMediaType( string $buffer ): string { |
| 162 | 168 | return $buffer === '' |
| 163 | 169 | ? self::MEDIA_EMPTY |
| 164 | : ((new finfo( FILEINFO_MIME_TYPE )) |
|
| 165 | ->buffer( substr( $buffer, 0, 256 ) ) |
|
| 170 | : (self::fileinfo()->buffer( substr( $buffer, 0, 128 ) ) |
|
| 166 | 171 | ?: self::MEDIA_OCTET); |
| 167 | 172 | } |
| 15 | 15 | } |
| 16 | 16 | |
| 17 | public static function createExtractor(): self { |
|
| 18 | $context = inflate_init( ZLIB_ENCODING_DEFLATE ); |
|
| 19 | ||
| 20 | return new self( |
|
| 21 | function( string $chunk ) use ( $context ): string { |
|
| 22 | $before = inflate_get_read_len( $context ); |
|
| 23 | $discard = @inflate_add( $context, $chunk ); |
|
| 24 | $after = inflate_get_read_len( $context ); |
|
| 25 | $length = $after - $before; |
|
| 26 | ||
| 27 | return substr( $chunk, 0, $length ); |
|
| 28 | }, |
|
| 29 | function(): string { |
|
| 30 | return ''; |
|
| 31 | }, |
|
| 32 | function() use ( $context ): bool { |
|
| 33 | return inflate_get_status( $context ) === ZLIB_STREAM_END; |
|
| 34 | } |
|
| 35 | ); |
|
| 36 | } |
|
| 37 | ||
| 17 | 38 | public static function createInflater(): self { |
| 18 | 39 | $context = inflate_init( ZLIB_ENCODING_DEFLATE ); |
| ... | ||
| 51 | 72 | } |
| 52 | 73 | ); |
| 74 | } |
|
| 75 | ||
| 76 | public function stream( mixed $handle, int $chunkSize = 8192 ): Generator { |
|
| 77 | $done = false; |
|
| 78 | ||
| 79 | while( !$done && !feof( $handle ) ) { |
|
| 80 | $chunk = fread( $handle, $chunkSize ); |
|
| 81 | $done = $chunk === false || $chunk === ''; |
|
| 82 | ||
| 83 | if( !$done ) { |
|
| 84 | $data = $this->pump( $chunk ); |
|
| 85 | ||
| 86 | if( $data !== '' ) { |
|
| 87 | yield $data; |
|
| 88 | } |
|
| 89 | ||
| 90 | $done = $this->finished(); |
|
| 91 | } |
|
| 92 | } |
|
| 53 | 93 | } |
| 54 | 94 | |
| 1 | <?php |
|
| 2 | require_once __DIR__ . '/CompressionStream.php'; |
|
| 3 | ||
| 4 | class DeltaDecoder { |
|
| 5 | public function apply( string $base, string $delta, int $cap ): string { |
|
| 6 | $pos = 0; |
|
| 7 | $res = $this->readDeltaSize( $delta, $pos ); |
|
| 8 | $pos += $res['used']; |
|
| 9 | $res = $this->readDeltaSize( $delta, $pos ); |
|
| 10 | $pos += $res['used']; |
|
| 11 | ||
| 12 | $out = ''; |
|
| 13 | $len = strlen( $delta ); |
|
| 14 | $done = false; |
|
| 15 | ||
| 16 | while( !$done && $pos < $len ) { |
|
| 17 | if( $cap > 0 && strlen( $out ) >= $cap ) { |
|
| 18 | $done = true; |
|
| 19 | } |
|
| 20 | ||
| 21 | if( !$done ) { |
|
| 22 | $op = ord( $delta[$pos++] ); |
|
| 23 | ||
| 24 | if( $op & 128 ) { |
|
| 25 | $info = $this->parseCopyInstruction( $op, $delta, $pos ); |
|
| 26 | $out .= substr( $base, $info['off'], $info['len'] ); |
|
| 27 | $pos += $info['used']; |
|
| 28 | } else { |
|
| 29 | $ln = $op & 127; |
|
| 30 | $out .= substr( $delta, $pos, $ln ); |
|
| 31 | $pos += $ln; |
|
| 32 | } |
|
| 33 | } |
|
| 34 | } |
|
| 35 | ||
| 36 | return $out; |
|
| 37 | } |
|
| 38 | ||
| 39 | public function applyStreamGenerator( |
|
| 40 | mixed $handle, |
|
| 41 | mixed $base |
|
| 42 | ): Generator { |
|
| 43 | $stream = CompressionStream::createInflater(); |
|
| 44 | $state = 0; |
|
| 45 | $buffer = ''; |
|
| 46 | $isFile = is_resource( $base ); |
|
| 47 | ||
| 48 | foreach( $stream->stream( $handle ) as $data ) { |
|
| 49 | $buffer .= $data; |
|
| 50 | $doneBuffer = false; |
|
| 51 | ||
| 52 | while( !$doneBuffer ) { |
|
| 53 | $len = strlen( $buffer ); |
|
| 54 | ||
| 55 | if( $len === 0 ) { |
|
| 56 | $doneBuffer = true; |
|
| 57 | } |
|
| 58 | ||
| 59 | if( !$doneBuffer ) { |
|
| 60 | if( $state < 2 ) { |
|
| 61 | $pos = 0; |
|
| 62 | ||
| 63 | while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { |
|
| 64 | $pos++; |
|
| 65 | } |
|
| 66 | ||
| 67 | if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { |
|
| 68 | $doneBuffer = true; |
|
| 69 | } |
|
| 70 | ||
| 71 | if( !$doneBuffer ) { |
|
| 72 | $buffer = substr( $buffer, $pos + 1 ); |
|
| 73 | $state++; |
|
| 74 | } |
|
| 75 | } else { |
|
| 76 | $op = ord( $buffer[0] ); |
|
| 77 | ||
| 78 | if( $op & 128 ) { |
|
| 79 | $need = $this->calculateCopyInstructionSize( $op ); |
|
| 80 | ||
| 81 | if( $len < 1 + $need ) { |
|
| 82 | $doneBuffer = true; |
|
| 83 | } |
|
| 84 | ||
| 85 | if( !$doneBuffer ) { |
|
| 86 | $info = $this->parseCopyInstruction( $op, $buffer, 1 ); |
|
| 87 | ||
| 88 | if( $isFile ) { |
|
| 89 | fseek( $base, $info['off'] ); |
|
| 90 | ||
| 91 | $rem = $info['len']; |
|
| 92 | ||
| 93 | while( $rem > 0 ) { |
|
| 94 | $slc = fread( $base, min( 65536, $rem ) ); |
|
| 95 | ||
| 96 | if( $slc === false || $slc === '' ) { |
|
| 97 | $rem = 0; |
|
| 98 | } else { |
|
| 99 | yield $slc; |
|
| 100 | ||
| 101 | $rem -= strlen( $slc ); |
|
| 102 | } |
|
| 103 | } |
|
| 104 | } else { |
|
| 105 | yield substr( $base, $info['off'], $info['len'] ); |
|
| 106 | } |
|
| 107 | ||
| 108 | $buffer = substr( $buffer, 1 + $need ); |
|
| 109 | } |
|
| 110 | } else { |
|
| 111 | $ln = $op & 127; |
|
| 112 | ||
| 113 | if( $len < 1 + $ln ) { |
|
| 114 | $doneBuffer = true; |
|
| 115 | } |
|
| 116 | ||
| 117 | if( !$doneBuffer ) { |
|
| 118 | yield substr( $buffer, 1, $ln ); |
|
| 119 | ||
| 120 | $buffer = substr( $buffer, 1 + $ln ); |
|
| 121 | } |
|
| 122 | } |
|
| 123 | } |
|
| 124 | } |
|
| 125 | } |
|
| 126 | } |
|
| 127 | } |
|
| 128 | ||
| 129 | public function readDeltaTargetSize( mixed $handle, int $type ): int { |
|
| 130 | if( $type === 6 ) { |
|
| 131 | $byte = ord( fread( $handle, 1 ) ); |
|
| 132 | ||
| 133 | while( $byte & 128 ) { |
|
| 134 | $byte = ord( fread( $handle, 1 ) ); |
|
| 135 | } |
|
| 136 | } else { |
|
| 137 | fseek( $handle, 20, SEEK_CUR ); |
|
| 138 | } |
|
| 139 | ||
| 140 | $stream = CompressionStream::createInflater(); |
|
| 141 | $head = ''; |
|
| 142 | $try = 0; |
|
| 143 | ||
| 144 | foreach( $stream->stream( $handle, 512 ) as $out ) { |
|
| 145 | $head .= $out; |
|
| 146 | $try++; |
|
| 147 | ||
| 148 | if( strlen( $head ) >= 32 || $try >= 64 ) { |
|
| 149 | break; |
|
| 150 | } |
|
| 151 | } |
|
| 152 | ||
| 153 | $pos = 0; |
|
| 154 | $result = 0; |
|
| 155 | ||
| 156 | if( strlen( $head ) > 0 ) { |
|
| 157 | $res = $this->readDeltaSize( $head, $pos ); |
|
| 158 | $pos += $res['used']; |
|
| 159 | $res = $this->readDeltaSize( $head, $pos ); |
|
| 160 | $result = $res['val']; |
|
| 161 | } |
|
| 162 | ||
| 163 | return $result; |
|
| 164 | } |
|
| 165 | ||
| 166 | private function parseCopyInstruction( |
|
| 167 | int $op, |
|
| 168 | string $data, |
|
| 169 | int $pos |
|
| 170 | ): array { |
|
| 171 | $off = 0; |
|
| 172 | $len = 0; |
|
| 173 | $ptr = $pos; |
|
| 174 | ||
| 175 | if( $op & 0x01 ) { |
|
| 176 | $off |= ord( $data[$ptr++] ); |
|
| 177 | } |
|
| 178 | ||
| 179 | if( $op & 0x02 ) { |
|
| 180 | $off |= ord( $data[$ptr++] ) << 8; |
|
| 181 | } |
|
| 182 | ||
| 183 | if( $op & 0x04 ) { |
|
| 184 | $off |= ord( $data[$ptr++] ) << 16; |
|
| 185 | } |
|
| 186 | ||
| 187 | if( $op & 0x08 ) { |
|
| 188 | $off |= ord( $data[$ptr++] ) << 24; |
|
| 189 | } |
|
| 190 | ||
| 191 | if( $op & 0x10 ) { |
|
| 192 | $len |= ord( $data[$ptr++] ); |
|
| 193 | } |
|
| 194 | ||
| 195 | if( $op & 0x20 ) { |
|
| 196 | $len |= ord( $data[$ptr++] ) << 8; |
|
| 197 | } |
|
| 198 | ||
| 199 | if( $op & 0x40 ) { |
|
| 200 | $len |= ord( $data[$ptr++] ) << 16; |
|
| 201 | } |
|
| 202 | ||
| 203 | return [ |
|
| 204 | 'off' => $off, |
|
| 205 | 'len' => $len === 0 ? 0x10000 : $len, |
|
| 206 | 'used' => $ptr - $pos |
|
| 207 | ]; |
|
| 208 | } |
|
| 209 | ||
| 210 | private function calculateCopyInstructionSize( int $op ): int { |
|
| 211 | $calc = $op & 0x7F; |
|
| 212 | $calc = $calc - ($calc >> 1 & 0x55); |
|
| 213 | $calc = ($calc >> 2 & 0x33) + ($calc & 0x33); |
|
| 214 | $calc = (($calc >> 4) + $calc) & 0x0F; |
|
| 215 | ||
| 216 | return $calc; |
|
| 217 | } |
|
| 218 | ||
| 219 | private function readDeltaSize( string $data, int $pos ): array { |
|
| 220 | $len = strlen( $data ); |
|
| 221 | $val = 0; |
|
| 222 | $shift = 0; |
|
| 223 | $start = $pos; |
|
| 224 | $done = false; |
|
| 225 | ||
| 226 | while( !$done && $pos < $len ) { |
|
| 227 | $byte = ord( $data[$pos++] ); |
|
| 228 | $val |= ($byte & 0x7F) << $shift; |
|
| 229 | ||
| 230 | if( !($byte & 0x80) ) { |
|
| 231 | $done = true; |
|
| 232 | } |
|
| 233 | ||
| 234 | if( !$done ) { |
|
| 235 | $shift += 7; |
|
| 236 | } |
|
| 237 | } |
|
| 238 | ||
| 239 | return [ 'val' => $val, 'used' => $pos - $start ]; |
|
| 240 | } |
|
| 241 | } |
|
| 1 | 242 |
| 1 | <?php |
|
| 2 | class FileHandlePool { |
|
| 3 | private array $handles; |
|
| 4 | ||
| 5 | public function __construct() { |
|
| 6 | $this->handles = []; |
|
| 7 | } |
|
| 8 | ||
| 9 | public function __destruct() { |
|
| 10 | foreach( $this->handles as $handle ) { |
|
| 11 | if( is_resource( $handle ) ) { |
|
| 12 | fclose( $handle ); |
|
| 13 | } |
|
| 14 | } |
|
| 15 | } |
|
| 16 | ||
| 17 | public function computeInt( |
|
| 18 | string $path, |
|
| 19 | callable $action, |
|
| 20 | int $fallback = 0 |
|
| 21 | ): int { |
|
| 22 | $result = $this->withHandle( $path, $action ); |
|
| 23 | return is_int( $result ) ? $result : $fallback; |
|
| 24 | } |
|
| 25 | ||
| 26 | public function computeString( |
|
| 27 | string $path, |
|
| 28 | callable $action, |
|
| 29 | string $fallback = '' |
|
| 30 | ): string { |
|
| 31 | $result = $this->withHandle( $path, $action ); |
|
| 32 | return is_string( $result ) ? $result : $fallback; |
|
| 33 | } |
|
| 34 | ||
| 35 | public function computeVoid( string $path, callable $action ): void { |
|
| 36 | $this->withHandle( $path, $action ); |
|
| 37 | } |
|
| 38 | ||
| 39 | public function streamGenerator( |
|
| 40 | string $path, |
|
| 41 | callable $action |
|
| 42 | ): Generator { |
|
| 43 | $resultGenerator = $this->withHandle( $path, $action ); |
|
| 44 | ||
| 45 | if( $resultGenerator instanceof Generator ) { |
|
| 46 | yield from $resultGenerator; |
|
| 47 | } |
|
| 48 | } |
|
| 49 | ||
| 50 | private function withHandle( string $path, callable $action ) { |
|
| 51 | if( !array_key_exists( $path, $this->handles ) ) { |
|
| 52 | $this->handles[$path] = @fopen( $path, 'rb' ) ?: null; |
|
| 53 | } |
|
| 54 | ||
| 55 | $handle = $this->handles[$path] ?? null; |
|
| 56 | ||
| 57 | return is_resource( $handle ) ? $action( $handle ) : null; |
|
| 58 | } |
|
| 59 | } |
|
| 1 | 60 |
| 257 | 257 | yield $hdr; |
| 258 | 258 | |
| 259 | $deflate = deflate_init( ZLIB_ENCODING_DEFLATE ); |
|
| 260 | ||
| 261 | foreach( $this->slurpChunks( $sha ) as $raw ) { |
|
| 262 | $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH ); |
|
| 263 | ||
| 264 | if( $compressed !== '' ) { |
|
| 265 | hash_update( $ctx, $compressed ); |
|
| 266 | yield $compressed; |
|
| 267 | } |
|
| 268 | } |
|
| 269 | ||
| 270 | $final = deflate_add( $deflate, '', ZLIB_FINISH ); |
|
| 271 | ||
| 272 | if( $final !== '' ) { |
|
| 273 | hash_update( $ctx, $final ); |
|
| 274 | yield $final; |
|
| 275 | } |
|
| 276 | } |
|
| 277 | ||
| 278 | yield hash_final( $ctx, true ); |
|
| 279 | } |
|
| 280 | ||
| 281 | private function slurpChunks( string $sha ): Generator { |
|
| 282 | $path = $this->getLoosePath( $sha ); |
|
| 283 | ||
| 284 | if( is_file( $path ) ) { |
|
| 285 | yield from $this->looseObjectChunks( $path ); |
|
| 286 | } else { |
|
| 287 | $any = false; |
|
| 288 | ||
| 289 | foreach( $this->packs->streamGenerator( $sha ) as $chunk ) { |
|
| 290 | $any = true; |
|
| 291 | yield $chunk; |
|
| 292 | } |
|
| 293 | ||
| 294 | if( !$any ) { |
|
| 295 | $data = $this->packs->read( $sha ); |
|
| 296 | ||
| 297 | if( $data !== '' ) { |
|
| 298 | yield $data; |
|
| 299 | } |
|
| 300 | } |
|
| 301 | } |
|
| 302 | } |
|
| 303 | ||
| 304 | private function looseObjectChunks( string $path ): Generator { |
|
| 305 | $reader = BufferedFileReader::open( $path ); |
|
| 306 | $infl = $reader->isOpen() |
|
| 307 | ? inflate_init( ZLIB_ENCODING_DEFLATE ) |
|
| 308 | : false; |
|
| 309 | ||
| 310 | if( $reader->isOpen() && $infl !== false ) { |
|
| 311 | $found = false; |
|
| 312 | $buffer = ''; |
|
| 313 | ||
| 314 | while( !$reader->eof() ) { |
|
| 315 | $chunk = $reader->read( 16384 ); |
|
| 316 | $inflated = inflate_add( $infl, $chunk ); |
|
| 317 | ||
| 318 | if( $inflated === false ) { |
|
| 319 | break; |
|
| 320 | } |
|
| 321 | ||
| 322 | if( !$found ) { |
|
| 323 | $buffer .= $inflated; |
|
| 324 | $eos = strpos( $buffer, "\0" ); |
|
| 325 | ||
| 326 | if( $eos !== false ) { |
|
| 327 | $found = true; |
|
| 328 | $body = substr( $buffer, $eos + 1 ); |
|
| 329 | ||
| 330 | if( $body !== '' ) { |
|
| 331 | yield $body; |
|
| 332 | } |
|
| 333 | ||
| 334 | $buffer = ''; |
|
| 335 | } |
|
| 336 | } elseif( $inflated !== '' ) { |
|
| 337 | yield $inflated; |
|
| 338 | } |
|
| 339 | } |
|
| 340 | } |
|
| 341 | } |
|
| 342 | ||
| 343 | private function streamCompressedObject( string $sha, $ctx ): Generator { |
|
| 344 | $stream = CompressionStream::createDeflater(); |
|
| 345 | $buffer = ''; |
|
| 346 | ||
| 347 | $this->slurp( $sha, function( $chunk ) use ( |
|
| 348 | $stream, |
|
| 349 | $ctx, |
|
| 350 | &$buffer |
|
| 351 | ) { |
|
| 352 | $compressed = $stream->pump( $chunk ); |
|
| 353 | ||
| 354 | if( $compressed !== '' ) { |
|
| 355 | hash_update( $ctx, $compressed ); |
|
| 356 | $buffer .= $compressed; |
|
| 357 | } |
|
| 358 | } ); |
|
| 359 | ||
| 360 | $final = $stream->finish(); |
|
| 361 | ||
| 362 | if( $final !== '' ) { |
|
| 363 | hash_update( $ctx, $final ); |
|
| 364 | $buffer .= $final; |
|
| 365 | } |
|
| 366 | ||
| 367 | $pos = 0; |
|
| 368 | $len = strlen( $buffer ); |
|
| 369 | ||
| 370 | while( $pos < $len ) { |
|
| 371 | $chunk = substr( $buffer, $pos, 32768 ); |
|
| 372 | ||
| 373 | yield $chunk; |
|
| 374 | $pos += 32768; |
|
| 375 | } |
|
| 376 | } |
|
| 377 | ||
| 378 | private function getTreeSha( string $commitOrTreeSha ): string { |
|
| 379 | $data = $this->read( $commitOrTreeSha ); |
|
| 380 | $sha = $commitOrTreeSha; |
|
| 381 | ||
| 382 | if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) { |
|
| 383 | $sha = $this->getTreeSha( $matches[1] ); |
|
| 384 | } |
|
| 385 | ||
| 386 | if( $sha === $commitOrTreeSha && |
|
| 387 | preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { |
|
| 388 | $sha = $matches[1]; |
|
| 389 | } |
|
| 390 | ||
| 391 | return $sha; |
|
| 392 | } |
|
| 393 | ||
| 394 | private function resolvePath( string $treeSha, string $path ): array { |
|
| 395 | $parts = explode( '/', trim( $path, '/' ) ); |
|
| 396 | $sha = $treeSha; |
|
| 397 | $mode = '40000'; |
|
| 398 | ||
| 399 | foreach( $parts as $part ) { |
|
| 400 | $entry = [ 'sha' => '', 'mode' => '' ]; |
|
| 401 | ||
| 402 | if( $part !== '' && $sha !== '' ) { |
|
| 403 | $entry = $this->findTreeEntry( $sha, $part ); |
|
| 404 | } |
|
| 405 | ||
| 406 | $sha = $entry['sha']; |
|
| 407 | $mode = $entry['mode']; |
|
| 408 | } |
|
| 409 | ||
| 410 | return [ |
|
| 411 | 'sha' => $sha, |
|
| 412 | 'mode' => $mode, |
|
| 413 | 'isDir' => $mode === '40000' || $mode === '040000' |
|
| 414 | ]; |
|
| 415 | } |
|
| 416 | ||
| 417 | private function findTreeEntry( string $treeSha, string $name ): array { |
|
| 418 | $data = $this->read( $treeSha ); |
|
| 419 | $entry = [ 'sha' => '', 'mode' => '' ]; |
|
| 420 | ||
| 421 | $this->parseTreeData( |
|
| 422 | $data, |
|
| 423 | function( $file, $n, $sha, $mode ) use ( $name, &$entry ) { |
|
| 424 | if( $file->isName( $name ) ) { |
|
| 425 | $entry = [ 'sha' => $sha, 'mode' => $mode ]; |
|
| 426 | ||
| 427 | return false; |
|
| 428 | } |
|
| 429 | } |
|
| 430 | ); |
|
| 431 | ||
| 432 | return $entry; |
|
| 433 | } |
|
| 434 | ||
| 435 | private function parseTagData( |
|
| 436 | string $name, |
|
| 437 | string $sha, |
|
| 438 | string $data |
|
| 439 | ): Tag { |
|
| 440 | $isAnn = strncmp( $data, 'object ', 7 ) === 0; |
|
| 441 | $pattern = $isAnn |
|
| 442 | ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' |
|
| 443 | : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'; |
|
| 444 | $id = $this->parseIdentity( $data, $pattern ); |
|
| 445 | $target = $isAnn |
|
| 446 | ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha ) |
|
| 447 | : $sha; |
|
| 448 | ||
| 449 | return new Tag( |
|
| 450 | $name, |
|
| 451 | $sha, |
|
| 452 | $target, |
|
| 453 | $id['timestamp'], |
|
| 454 | $this->extractMessage( $data ), |
|
| 455 | $id['name'] |
|
| 456 | ); |
|
| 457 | } |
|
| 458 | ||
| 459 | private function extractPattern( |
|
| 460 | string $data, |
|
| 461 | string $pattern, |
|
| 462 | int $group, |
|
| 463 | string $default = '' |
|
| 464 | ): string { |
|
| 465 | return preg_match( $pattern, $data, $matches ) |
|
| 466 | ? $matches[$group] |
|
| 467 | : $default; |
|
| 468 | } |
|
| 469 | ||
| 470 | private function parseIdentity( string $data, string $pattern ): array { |
|
| 471 | $found = preg_match( $pattern, $data, $matches ); |
|
| 472 | ||
| 473 | return [ |
|
| 474 | 'name' => $found ? trim( $matches[1] ) : 'Unknown', |
|
| 475 | 'email' => $found ? $matches[2] : '', |
|
| 476 | 'timestamp' => $found ? (int)$matches[3] : 0 |
|
| 477 | ]; |
|
| 478 | } |
|
| 479 | ||
| 480 | private function extractMessage( string $data ): string { |
|
| 481 | $pos = strpos( $data, "\n\n" ); |
|
| 482 | ||
| 483 | return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : ''; |
|
| 484 | } |
|
| 485 | ||
| 486 | private function slurp( string $sha, callable $callback ): void { |
|
| 487 | $path = $this->getLoosePath( $sha ); |
|
| 488 | ||
| 489 | if( is_file( $path ) ) { |
|
| 490 | $this->slurpLooseObject( $path, $callback ); |
|
| 491 | } else { |
|
| 492 | $this->slurpPackedObject( $sha, $callback ); |
|
| 493 | } |
|
| 494 | } |
|
| 495 | ||
| 496 | private function slurpLooseObject( string $path, callable $callback ): void { |
|
| 497 | $this->iterateInflated( |
|
| 498 | $path, |
|
| 499 | function( $chunk ) use ( $callback ) { |
|
| 500 | if( $chunk !== '' ) { |
|
| 501 | $callback( $chunk ); |
|
| 502 | } |
|
| 503 | ||
| 504 | return true; |
|
| 505 | } |
|
| 506 | ); |
|
| 507 | } |
|
| 508 | ||
| 509 | private function slurpPackedObject( string $sha, callable $callback ): void { |
|
| 510 | $streamed = $this->packs->stream( $sha, $callback ); |
|
| 511 | ||
| 512 | if( !$streamed ) { |
|
| 513 | $data = $this->packs->read( $sha ); |
|
| 514 | ||
| 515 | if( $data !== '' ) { |
|
| 516 | $callback( $data ); |
|
| 517 | } |
|
| 518 | } |
|
| 519 | } |
|
| 520 | ||
| 521 | private function iterateInflated( |
|
| 522 | string $path, |
|
| 523 | callable $processor |
|
| 524 | ): void { |
|
| 525 | $reader = BufferedFileReader::open( $path ); |
|
| 526 | $infl = $reader->isOpen() |
|
| 527 | ? inflate_init( ZLIB_ENCODING_DEFLATE ) |
|
| 528 | : false; |
|
| 529 | $found = false; |
|
| 530 | $buffer = ''; |
|
| 531 | ||
| 532 | if( $reader->isOpen() && $infl !== false ) { |
|
| 533 | while( !$reader->eof() ) { |
|
| 534 | $chunk = $reader->read( 16384 ); |
|
| 535 | $inflated = inflate_add( $infl, $chunk ); |
|
| 536 | ||
| 537 | if( $inflated === false ) { |
|
| 538 | break; |
|
| 539 | } |
|
| 540 | ||
| 541 | if( !$found ) { |
|
| 542 | $buffer .= $inflated; |
|
| 543 | $eos = strpos( $buffer, "\0" ); |
|
| 544 | ||
| 545 | if( $eos !== false ) { |
|
| 546 | $found = true; |
|
| 547 | $body = substr( $buffer, $eos + 1 ); |
|
| 548 | $head = substr( $buffer, 0, $eos ); |
|
| 549 | ||
| 550 | if( $processor( $body, $head ) === false ) { |
|
| 551 | break; |
|
| 552 | } |
|
| 553 | } |
|
| 554 | } elseif( $processor( $inflated, '' ) === false ) { |
|
| 555 | break; |
|
| 556 | } |
|
| 557 | } |
|
| 558 | } |
|
| 559 | } |
|
| 560 | ||
| 561 | private function peekLooseObject( string $sha, int $length ): string { |
|
| 562 | $path = $this->getLoosePath( $sha ); |
|
| 563 | $buf = ''; |
|
| 564 | ||
| 565 | if( is_file( $path ) ) { |
|
| 566 | $this->iterateInflated( |
|
| 567 | $path, |
|
| 568 | function( $chunk ) use ( $length, &$buf ) { |
|
| 569 | $buf .= $chunk; |
|
| 570 | ||
| 571 | return strlen( $buf ) < $length; |
|
| 572 | } |
|
| 573 | ); |
|
| 574 | } |
|
| 575 | ||
| 576 | return substr( $buf, 0, $length ); |
|
| 577 | } |
|
| 578 | ||
| 579 | private function parseCommit( string $sha ): object { |
|
| 580 | $data = $this->read( $sha ); |
|
| 581 | $result = (object)[ 'sha' => '' ]; |
|
| 582 | ||
| 583 | if( $data !== '' ) { |
|
| 584 | $id = $this->parseIdentity( |
|
| 585 | $data, |
|
| 586 | '/^author (.*) <(.*)> (\d+)/m' |
|
| 587 | ); |
|
| 588 | ||
| 589 | $result = (object)[ |
|
| 590 | 'sha' => $sha, |
|
| 591 | 'message' => $this->extractMessage( $data ), |
|
| 592 | 'author' => $id['name'], |
|
| 593 | 'email' => $id['email'], |
|
| 594 | 'date' => $id['timestamp'], |
|
| 595 | 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 ) |
|
| 596 | ]; |
|
| 597 | } |
|
| 598 | ||
| 599 | return $result; |
|
| 600 | } |
|
| 601 | ||
| 602 | private function walkTree( string $sha, callable $callback ): void { |
|
| 603 | $data = $this->read( $sha ); |
|
| 604 | $tree = $data; |
|
| 605 | ||
| 606 | if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) { |
|
| 607 | $tree = $this->read( $m[1] ); |
|
| 608 | } |
|
| 609 | ||
| 610 | if( $tree !== '' && $this->isTreeData( $tree ) ) { |
|
| 611 | $this->processTree( $tree, $callback ); |
|
| 612 | } |
|
| 613 | } |
|
| 614 | ||
| 615 | private function processTree( string $data, callable $callback ): void { |
|
| 616 | $this->parseTreeData( |
|
| 617 | $data, |
|
| 618 | function( $file, $n, $s, $m ) use ( $callback ) { |
|
| 619 | $callback( $file ); |
|
| 620 | } |
|
| 621 | ); |
|
| 622 | } |
|
| 623 | ||
| 624 | public function parseTreeData( string $data, callable $callback ): void { |
|
| 625 | $pos = 0; |
|
| 626 | $len = strlen( $data ); |
|
| 627 | ||
| 628 | while( $pos < $len ) { |
|
| 629 | $space = strpos( $data, ' ', $pos ); |
|
| 630 | $eos = strpos( $data, "\0", $space ); |
|
| 631 | ||
| 632 | if( $space === false || $eos === false || $eos + 21 > $len ) { |
|
| 633 | break; |
|
| 634 | } |
|
| 635 | ||
| 636 | $mode = substr( $data, $pos, $space - $pos ); |
|
| 637 | $name = substr( $data, $space + 1, $eos - $space - 1 ); |
|
| 638 | $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); |
|
| 639 | $dir = $mode === '40000' || $mode === '040000'; |
|
| 640 | $isSub = $mode === '160000'; |
|
| 641 | ||
| 642 | $file = new File( |
|
| 643 | $name, |
|
| 644 | $sha, |
|
| 645 | $mode, |
|
| 646 | 0, |
|
| 647 | $dir || $isSub ? 0 : $this->getObjectSize( $sha ), |
|
| 648 | $dir || $isSub ? '' : $this->peek( $sha ) |
|
| 649 | ); |
|
| 650 | ||
| 651 | if( $callback( $file, $name, $sha, $mode ) === false ) { |
|
| 652 | break; |
|
| 653 | } |
|
| 654 | ||
| 655 | $pos = $eos + 21; |
|
| 656 | } |
|
| 657 | } |
|
| 658 | ||
| 659 | private function isTreeData( string $data ): bool { |
|
| 660 | $len = strlen( $data ); |
|
| 661 | $patt = '/^(40000|100644|100755|120000|160000) /'; |
|
| 662 | $match = $len >= 25 && preg_match( $patt, $data ); |
|
| 663 | $eos = $match ? strpos( $data, "\0" ) : false; |
|
| 664 | ||
| 665 | return $match && $eos !== false && $eos + 21 <= $len; |
|
| 666 | } |
|
| 667 | ||
| 668 | private function getLoosePath( string $sha ): string { |
|
| 669 | return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" . |
|
| 670 | substr( $sha, 2 ); |
|
| 671 | } |
|
| 672 | ||
| 673 | private function getLooseObjectSize( string $sha ): int { |
|
| 674 | $path = $this->getLoosePath( $sha ); |
|
| 675 | $size = 0; |
|
| 676 | ||
| 677 | if( is_file( $path ) ) { |
|
| 678 | $this->iterateInflated( |
|
| 679 | $path, |
|
| 680 | function( $c, $head ) use ( &$size ) { |
|
| 681 | if( $head !== '' ) { |
|
| 682 | $parts = explode( ' ', $head ); |
|
| 683 | $size = isset( $parts[1] ) ? (int)$parts[1] : 0; |
|
| 684 | } |
|
| 685 | ||
| 686 | return false; |
|
| 687 | } |
|
| 688 | ); |
|
| 689 | } |
|
| 690 | ||
| 691 | return $size; |
|
| 692 | } |
|
| 693 | ||
| 694 | public function collectObjects( array $wants, array $haves = [] ): array { |
|
| 695 | $objs = $this->traverseObjects( $wants ); |
|
| 696 | $result = []; |
|
| 697 | ||
| 698 | if( !empty( $haves ) ) { |
|
| 699 | $haveObjs = $this->traverseObjects( $haves ); |
|
| 700 | ||
| 701 | foreach( $haveObjs as $sha => $type ) { |
|
| 702 | if( isset( $objs[$sha] ) ) { |
|
| 703 | unset( $objs[$sha] ); |
|
| 704 | } |
|
| 705 | } |
|
| 706 | } |
|
| 707 | ||
| 708 | $result = $objs; |
|
| 709 | ||
| 710 | return $result; |
|
| 711 | } |
|
| 712 | ||
| 713 | private function traverseObjects( array $roots ): array { |
|
| 714 | $objs = []; |
|
| 715 | $queue = []; |
|
| 716 | ||
| 717 | foreach( $roots as $sha ) { |
|
| 718 | $queue[] = [ 'sha' => $sha, 'type' => 0 ]; |
|
| 719 | } |
|
| 720 | ||
| 721 | while( !empty( $queue ) ) { |
|
| 722 | $item = array_pop( $queue ); |
|
| 723 | $sha = $item['sha']; |
|
| 724 | $type = $item['type']; |
|
| 725 | ||
| 726 | if( isset( $objs[$sha] ) ) { |
|
| 727 | continue; |
|
| 728 | } |
|
| 729 | ||
| 730 | $data = ''; |
|
| 731 | ||
| 732 | if( $type !== 3 ) { |
|
| 733 | $data = $this->read( $sha ); |
|
| 734 | ||
| 735 | if( $type === 0 ) { |
|
| 736 | $type = $this->getObjectType( $data ); |
|
| 737 | } |
|
| 738 | } |
|
| 739 | ||
| 740 | $objs[$sha] = $type; |
|
| 741 | ||
| 742 | if( $type === 1 ) { |
|
| 743 | $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ); |
|
| 744 | ||
| 745 | if( $hasTree ) { |
|
| 746 | $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; |
|
| 747 | } |
|
| 748 | ||
| 749 | $hasParents = preg_match_all( |
|
| 750 | '/^parent ([0-9a-f]{40})/m', |
|
| 751 | $data, |
|
| 752 | $m |
|
| 753 | ); |
|
| 754 | ||
| 755 | if( $hasParents ) { |
|
| 756 | foreach( $m[1] as $parentSha ) { |
|
| 757 | $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; |
|
| 758 | } |
|
| 759 | } |
|
| 760 | } elseif( $type === 2 ) { |
|
| 761 | $pos = 0; |
|
| 762 | $len = strlen( $data ); |
|
| 763 | ||
| 764 | while( $pos < $len ) { |
|
| 765 | $space = strpos( $data, ' ', $pos ); |
|
| 766 | $eos = strpos( $data, "\0", $space ); |
|
| 767 | ||
| 768 | if( $space === false || $eos === false ) { |
|
| 769 | break; |
|
| 770 | } |
|
| 771 | ||
| 772 | $mode = substr( $data, $pos, $space - $pos ); |
|
| 773 | $hash = bin2hex( substr( $data, $eos + 1, 20 ) ); |
|
| 774 | ||
| 775 | if( $mode !== '160000' ) { |
|
| 776 | $isDir = $mode === '40000' || $mode === '040000'; |
|
| 777 | $queue[] = [ 'sha' => $hash, 'type' => $isDir ? 2 : 3 ]; |
|
| 778 | } |
|
| 779 | ||
| 780 | $pos = $eos + 21; |
|
| 781 | } |
|
| 782 | } elseif( $type === 4 ) { |
|
| 783 | $isTagTgt = preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ); |
|
| 784 | ||
| 785 | if( $isTagTgt ) { |
|
| 786 | $nextType = 1; |
|
| 787 | ||
| 788 | if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) { |
|
| 789 | $map = [ |
|
| 790 | 'commit' => 1, |
|
| 791 | 'tree' => 2, |
|
| 792 | 'blob' => 3, |
|
| 793 | 'tag' => 4 |
|
| 794 | ]; |
|
| 795 | $nextType = $map[$t[1]] ?? 1; |
|
| 796 | } |
|
| 797 | ||
| 798 | $queue[] = [ 'sha' => $m[1], 'type' => $nextType ]; |
|
| 799 | } |
|
| 800 | } |
|
| 801 | } |
|
| 802 | ||
| 803 | return $objs; |
|
| 804 | } |
|
| 805 | ||
| 806 | private function getObjectType( string $data ): int { |
|
| 807 | $isTree = strpos( $data, "tree " ) === 0; |
|
| 808 | $isObj = strpos( $data, "object " ) === 0; |
|
| 809 | $result = 3; |
|
| 810 | ||
| 811 | if( $isTree ) { |
|
| 812 | $result = 1; |
|
| 813 | } elseif( $isObj ) { |
|
| 814 | $result = 4; |
|
| 815 | } elseif( $this->isTreeData( $data ) ) { |
|
| 816 | $result = 2; |
|
| 817 | } |
|
| 818 | ||
| 819 | return $result; |
|
| 820 | } |
|
| 821 | } |
|
| 822 | ||
| 823 | class MissingFile extends File { |
|
| 824 | public function __construct() { |
|
| 825 | parent::__construct( '', '', '0', 0, 0, '' ); |
|
| 826 | } |
|
| 827 | ||
| 828 | public function emitRawHeaders(): void { |
|
| 829 | header( "HTTP/1.1 404 Not Found" ); |
|
| 830 | exit; |
|
| 831 | } |
|
| 832 | } |
|
| 259 | foreach( $this->streamCompressed( $sha ) as $compressed ) { |
|
| 260 | hash_update( $ctx, $compressed ); |
|
| 261 | yield $compressed; |
|
| 262 | } |
|
| 263 | } |
|
| 264 | ||
| 265 | yield hash_final( $ctx, true ); |
|
| 266 | } |
|
| 267 | ||
| 268 | private function streamCompressed( string $sha ): Generator { |
|
| 269 | $yielded = false; |
|
| 270 | ||
| 271 | foreach( $this->packs->streamRawCompressed( $sha ) as $chunk ) { |
|
| 272 | $yielded = true; |
|
| 273 | yield $chunk; |
|
| 274 | } |
|
| 275 | ||
| 276 | if( !$yielded ) { |
|
| 277 | $deflate = deflate_init( ZLIB_ENCODING_DEFLATE ); |
|
| 278 | ||
| 279 | foreach( $this->slurpChunks( $sha ) as $raw ) { |
|
| 280 | $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH ); |
|
| 281 | ||
| 282 | if( $compressed !== '' ) { |
|
| 283 | yield $compressed; |
|
| 284 | } |
|
| 285 | } |
|
| 286 | ||
| 287 | $final = deflate_add( $deflate, '', ZLIB_FINISH ); |
|
| 288 | ||
| 289 | if( $final !== '' ) { |
|
| 290 | yield $final; |
|
| 291 | } |
|
| 292 | } |
|
| 293 | } |
|
| 294 | ||
| 295 | private function slurpChunks( string $sha ): Generator { |
|
| 296 | $path = $this->getLoosePath( $sha ); |
|
| 297 | ||
| 298 | if( is_file( $path ) ) { |
|
| 299 | yield from $this->looseObjectChunks( $path ); |
|
| 300 | } else { |
|
| 301 | $any = false; |
|
| 302 | ||
| 303 | foreach( $this->packs->streamGenerator( $sha ) as $chunk ) { |
|
| 304 | $any = true; |
|
| 305 | yield $chunk; |
|
| 306 | } |
|
| 307 | ||
| 308 | if( !$any ) { |
|
| 309 | $data = $this->packs->read( $sha ); |
|
| 310 | ||
| 311 | if( $data !== '' ) { |
|
| 312 | yield $data; |
|
| 313 | } |
|
| 314 | } |
|
| 315 | } |
|
| 316 | } |
|
| 317 | ||
| 318 | private function looseObjectChunks( string $path ): Generator { |
|
| 319 | $reader = BufferedFileReader::open( $path ); |
|
| 320 | $infl = $reader->isOpen() |
|
| 321 | ? inflate_init( ZLIB_ENCODING_DEFLATE ) |
|
| 322 | : false; |
|
| 323 | ||
| 324 | if( $reader->isOpen() && $infl !== false ) { |
|
| 325 | $found = false; |
|
| 326 | $buffer = ''; |
|
| 327 | ||
| 328 | while( !$reader->eof() ) { |
|
| 329 | $chunk = $reader->read( 16384 ); |
|
| 330 | $inflated = inflate_add( $infl, $chunk ); |
|
| 331 | ||
| 332 | if( $inflated === false ) { |
|
| 333 | break; |
|
| 334 | } |
|
| 335 | ||
| 336 | if( !$found ) { |
|
| 337 | $buffer .= $inflated; |
|
| 338 | $eos = strpos( $buffer, "\0" ); |
|
| 339 | ||
| 340 | if( $eos !== false ) { |
|
| 341 | $found = true; |
|
| 342 | $body = substr( $buffer, $eos + 1 ); |
|
| 343 | ||
| 344 | if( $body !== '' ) { |
|
| 345 | yield $body; |
|
| 346 | } |
|
| 347 | ||
| 348 | $buffer = ''; |
|
| 349 | } |
|
| 350 | } elseif( $inflated !== '' ) { |
|
| 351 | yield $inflated; |
|
| 352 | } |
|
| 353 | } |
|
| 354 | } |
|
| 355 | } |
|
| 356 | ||
| 357 | private function streamCompressedObject( string $sha, $ctx ): Generator { |
|
| 358 | $stream = CompressionStream::createDeflater(); |
|
| 359 | $buffer = ''; |
|
| 360 | ||
| 361 | $this->slurp( $sha, function( $chunk ) use ( |
|
| 362 | $stream, |
|
| 363 | $ctx, |
|
| 364 | &$buffer |
|
| 365 | ) { |
|
| 366 | $compressed = $stream->pump( $chunk ); |
|
| 367 | ||
| 368 | if( $compressed !== '' ) { |
|
| 369 | hash_update( $ctx, $compressed ); |
|
| 370 | $buffer .= $compressed; |
|
| 371 | } |
|
| 372 | } ); |
|
| 373 | ||
| 374 | $final = $stream->finish(); |
|
| 375 | ||
| 376 | if( $final !== '' ) { |
|
| 377 | hash_update( $ctx, $final ); |
|
| 378 | $buffer .= $final; |
|
| 379 | } |
|
| 380 | ||
| 381 | $pos = 0; |
|
| 382 | $len = strlen( $buffer ); |
|
| 383 | ||
| 384 | while( $pos < $len ) { |
|
| 385 | $chunk = substr( $buffer, $pos, 32768 ); |
|
| 386 | ||
| 387 | yield $chunk; |
|
| 388 | $pos += 32768; |
|
| 389 | } |
|
| 390 | } |
|
| 391 | ||
| 392 | private function getTreeSha( string $commitOrTreeSha ): string { |
|
| 393 | $data = $this->read( $commitOrTreeSha ); |
|
| 394 | $sha = $commitOrTreeSha; |
|
| 395 | ||
| 396 | if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) { |
|
| 397 | $sha = $this->getTreeSha( $matches[1] ); |
|
| 398 | } |
|
| 399 | ||
| 400 | if( $sha === $commitOrTreeSha && |
|
| 401 | preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) { |
|
| 402 | $sha = $matches[1]; |
|
| 403 | } |
|
| 404 | ||
| 405 | return $sha; |
|
| 406 | } |
|
| 407 | ||
| 408 | private function resolvePath( string $treeSha, string $path ): array { |
|
| 409 | $parts = explode( '/', trim( $path, '/' ) ); |
|
| 410 | $sha = $treeSha; |
|
| 411 | $mode = '40000'; |
|
| 412 | ||
| 413 | foreach( $parts as $part ) { |
|
| 414 | $entry = [ 'sha' => '', 'mode' => '' ]; |
|
| 415 | ||
| 416 | if( $part !== '' && $sha !== '' ) { |
|
| 417 | $entry = $this->findTreeEntry( $sha, $part ); |
|
| 418 | } |
|
| 419 | ||
| 420 | $sha = $entry['sha']; |
|
| 421 | $mode = $entry['mode']; |
|
| 422 | } |
|
| 423 | ||
| 424 | return [ |
|
| 425 | 'sha' => $sha, |
|
| 426 | 'mode' => $mode, |
|
| 427 | 'isDir' => $mode === '40000' || $mode === '040000' |
|
| 428 | ]; |
|
| 429 | } |
|
| 430 | ||
| 431 | private function findTreeEntry( string $treeSha, string $name ): array { |
|
| 432 | $data = $this->read( $treeSha ); |
|
| 433 | $entry = [ 'sha' => '', 'mode' => '' ]; |
|
| 434 | ||
| 435 | $this->parseTreeData( |
|
| 436 | $data, |
|
| 437 | function( $file, $n, $sha, $mode ) use ( $name, &$entry ) { |
|
| 438 | if( $file->isName( $name ) ) { |
|
| 439 | $entry = [ 'sha' => $sha, 'mode' => $mode ]; |
|
| 440 | ||
| 441 | return false; |
|
| 442 | } |
|
| 443 | } |
|
| 444 | ); |
|
| 445 | ||
| 446 | return $entry; |
|
| 447 | } |
|
| 448 | ||
| 449 | private function parseTagData( |
|
| 450 | string $name, |
|
| 451 | string $sha, |
|
| 452 | string $data |
|
| 453 | ): Tag { |
|
| 454 | $isAnn = strncmp( $data, 'object ', 7 ) === 0; |
|
| 455 | $pattern = $isAnn |
|
| 456 | ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m' |
|
| 457 | : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m'; |
|
| 458 | $id = $this->parseIdentity( $data, $pattern ); |
|
| 459 | $target = $isAnn |
|
| 460 | ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha ) |
|
| 461 | : $sha; |
|
| 462 | ||
| 463 | return new Tag( |
|
| 464 | $name, |
|
| 465 | $sha, |
|
| 466 | $target, |
|
| 467 | $id['timestamp'], |
|
| 468 | $this->extractMessage( $data ), |
|
| 469 | $id['name'] |
|
| 470 | ); |
|
| 471 | } |
|
| 472 | ||
| 473 | private function extractPattern( |
|
| 474 | string $data, |
|
| 475 | string $pattern, |
|
| 476 | int $group, |
|
| 477 | string $default = '' |
|
| 478 | ): string { |
|
| 479 | return preg_match( $pattern, $data, $matches ) |
|
| 480 | ? $matches[$group] |
|
| 481 | : $default; |
|
| 482 | } |
|
| 483 | ||
| 484 | private function parseIdentity( string $data, string $pattern ): array { |
|
| 485 | $found = preg_match( $pattern, $data, $matches ); |
|
| 486 | ||
| 487 | return [ |
|
| 488 | 'name' => $found ? trim( $matches[1] ) : 'Unknown', |
|
| 489 | 'email' => $found ? $matches[2] : '', |
|
| 490 | 'timestamp' => $found ? (int)$matches[3] : 0 |
|
| 491 | ]; |
|
| 492 | } |
|
| 493 | ||
| 494 | private function extractMessage( string $data ): string { |
|
| 495 | $pos = strpos( $data, "\n\n" ); |
|
| 496 | ||
| 497 | return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : ''; |
|
| 498 | } |
|
| 499 | ||
| 500 | private function slurp( string $sha, callable $callback ): void { |
|
| 501 | $path = $this->getLoosePath( $sha ); |
|
| 502 | ||
| 503 | if( is_file( $path ) ) { |
|
| 504 | $this->slurpLooseObject( $path, $callback ); |
|
| 505 | } else { |
|
| 506 | $this->slurpPackedObject( $sha, $callback ); |
|
| 507 | } |
|
| 508 | } |
|
| 509 | ||
| 510 | private function slurpLooseObject( string $path, callable $callback ): void { |
|
| 511 | $this->iterateInflated( |
|
| 512 | $path, |
|
| 513 | function( $chunk ) use ( $callback ) { |
|
| 514 | if( $chunk !== '' ) { |
|
| 515 | $callback( $chunk ); |
|
| 516 | } |
|
| 517 | ||
| 518 | return true; |
|
| 519 | } |
|
| 520 | ); |
|
| 521 | } |
|
| 522 | ||
| 523 | private function slurpPackedObject( string $sha, callable $callback ): void { |
|
| 524 | $streamed = $this->packs->stream( $sha, $callback ); |
|
| 525 | ||
| 526 | if( !$streamed ) { |
|
| 527 | $data = $this->packs->read( $sha ); |
|
| 528 | ||
| 529 | if( $data !== '' ) { |
|
| 530 | $callback( $data ); |
|
| 531 | } |
|
| 532 | } |
|
| 533 | } |
|
| 534 | ||
| 535 | private function iterateInflated( |
|
| 536 | string $path, |
|
| 537 | callable $processor, |
|
| 538 | int $bufferSize = 16384 |
|
| 539 | ): void { |
|
| 540 | $reader = BufferedFileReader::open( $path ); |
|
| 541 | $infl = $reader->isOpen() |
|
| 542 | ? inflate_init( ZLIB_ENCODING_DEFLATE ) |
|
| 543 | : false; |
|
| 544 | $found = false; |
|
| 545 | $buffer = ''; |
|
| 546 | ||
| 547 | if( $reader->isOpen() && $infl !== false ) { |
|
| 548 | while( !$reader->eof() ) { |
|
| 549 | $chunk = $reader->read( $bufferSize ); |
|
| 550 | $inflated = inflate_add( $infl, $chunk ); |
|
| 551 | ||
| 552 | if( $inflated === false ) { |
|
| 553 | break; |
|
| 554 | } |
|
| 555 | ||
| 556 | if( !$found ) { |
|
| 557 | $buffer .= $inflated; |
|
| 558 | $eos = strpos( $buffer, "\0" ); |
|
| 559 | ||
| 560 | if( $eos !== false ) { |
|
| 561 | $found = true; |
|
| 562 | $body = substr( $buffer, $eos + 1 ); |
|
| 563 | $head = substr( $buffer, 0, $eos ); |
|
| 564 | ||
| 565 | if( $processor( $body, $head ) === false ) { |
|
| 566 | break; |
|
| 567 | } |
|
| 568 | } |
|
| 569 | } elseif( $processor( $inflated, '' ) === false ) { |
|
| 570 | break; |
|
| 571 | } |
|
| 572 | } |
|
| 573 | } |
|
| 574 | } |
|
| 575 | ||
| 576 | private function peekLooseObject( string $sha, int $length ): string { |
|
| 577 | $path = $this->getLoosePath( $sha ); |
|
| 578 | $buf = ''; |
|
| 579 | ||
| 580 | if( is_file( $path ) ) { |
|
| 581 | $this->iterateInflated( |
|
| 582 | $path, |
|
| 583 | function( $chunk ) use ( $length, &$buf ) { |
|
| 584 | $buf .= $chunk; |
|
| 585 | ||
| 586 | return strlen( $buf ) < $length; |
|
| 587 | }, |
|
| 588 | 8192 |
|
| 589 | ); |
|
| 590 | } |
|
| 591 | ||
| 592 | return substr( $buf, 0, $length ); |
|
| 593 | } |
|
| 594 | ||
| 595 | private function parseCommit( string $sha ): object { |
|
| 596 | $data = $this->read( $sha ); |
|
| 597 | $result = (object)[ 'sha' => '' ]; |
|
| 598 | ||
| 599 | if( $data !== '' ) { |
|
| 600 | $id = $this->parseIdentity( |
|
| 601 | $data, |
|
| 602 | '/^author (.*) <(.*)> (\d+)/m' |
|
| 603 | ); |
|
| 604 | ||
| 605 | $result = (object)[ |
|
| 606 | 'sha' => $sha, |
|
| 607 | 'message' => $this->extractMessage( $data ), |
|
| 608 | 'author' => $id['name'], |
|
| 609 | 'email' => $id['email'], |
|
| 610 | 'date' => $id['timestamp'], |
|
| 611 | 'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 ) |
|
| 612 | ]; |
|
| 613 | } |
|
| 614 | ||
| 615 | return $result; |
|
| 616 | } |
|
| 617 | ||
| 618 | private function walkTree( string $sha, callable $callback ): void { |
|
| 619 | $data = $this->read( $sha ); |
|
| 620 | $tree = $data; |
|
| 621 | ||
| 622 | if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) { |
|
| 623 | $tree = $this->read( $m[1] ); |
|
| 624 | } |
|
| 625 | ||
| 626 | if( $tree !== '' && $this->isTreeData( $tree ) ) { |
|
| 627 | $this->processTree( $tree, $callback ); |
|
| 628 | } |
|
| 629 | } |
|
| 630 | ||
| 631 | private function processTree( string $data, callable $callback ): void { |
|
| 632 | $this->parseTreeData( |
|
| 633 | $data, |
|
| 634 | function( $file, $n, $s, $m ) use ( $callback ) { |
|
| 635 | $callback( $file ); |
|
| 636 | } |
|
| 637 | ); |
|
| 638 | } |
|
| 639 | ||
| 640 | public function parseTreeData( string $data, callable $callback ): void { |
|
| 641 | $pos = 0; |
|
| 642 | $len = strlen( $data ); |
|
| 643 | ||
| 644 | while( $pos < $len ) { |
|
| 645 | $space = strpos( $data, ' ', $pos ); |
|
| 646 | $eos = strpos( $data, "\0", $space ); |
|
| 647 | ||
| 648 | if( $space === false || $eos === false || $eos + 21 > $len ) { |
|
| 649 | break; |
|
| 650 | } |
|
| 651 | ||
| 652 | $mode = substr( $data, $pos, $space - $pos ); |
|
| 653 | $name = substr( $data, $space + 1, $eos - $space - 1 ); |
|
| 654 | $sha = bin2hex( substr( $data, $eos + 1, 20 ) ); |
|
| 655 | $dir = $mode === '40000' || $mode === '040000'; |
|
| 656 | $isSub = $mode === '160000'; |
|
| 657 | ||
| 658 | $file = new File( |
|
| 659 | $name, |
|
| 660 | $sha, |
|
| 661 | $mode, |
|
| 662 | 0, |
|
| 663 | $dir || $isSub ? 0 : $this->getObjectSize( $sha ), |
|
| 664 | $dir || $isSub ? '' : $this->peek( $sha ) |
|
| 665 | ); |
|
| 666 | ||
| 667 | if( $callback( $file, $name, $sha, $mode ) === false ) { |
|
| 668 | break; |
|
| 669 | } |
|
| 670 | ||
| 671 | $pos = $eos + 21; |
|
| 672 | } |
|
| 673 | } |
|
| 674 | ||
| 675 | private function isTreeData( string $data ): bool { |
|
| 676 | $len = strlen( $data ); |
|
| 677 | $patt = '/^(40000|100644|100755|120000|160000) /'; |
|
| 678 | $match = $len >= 25 && preg_match( $patt, $data ); |
|
| 679 | $eos = $match ? strpos( $data, "\0" ) : false; |
|
| 680 | ||
| 681 | return $match && $eos !== false && $eos + 21 <= $len; |
|
| 682 | } |
|
| 683 | ||
| 684 | private function getLoosePath( string $sha ): string { |
|
| 685 | return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" . |
|
| 686 | substr( $sha, 2 ); |
|
| 687 | } |
|
| 688 | ||
| 689 | private function getLooseObjectSize( string $sha ): int { |
|
| 690 | $path = $this->getLoosePath( $sha ); |
|
| 691 | $size = 0; |
|
| 692 | ||
| 693 | if( is_file( $path ) ) { |
|
| 694 | $this->iterateInflated( |
|
| 695 | $path, |
|
| 696 | function( $c, $head ) use ( &$size ) { |
|
| 697 | if( $head !== '' ) { |
|
| 698 | $parts = explode( ' ', $head ); |
|
| 699 | $size = isset( $parts[1] ) ? (int)$parts[1] : 0; |
|
| 700 | } |
|
| 701 | ||
| 702 | return false; |
|
| 703 | } |
|
| 704 | ); |
|
| 705 | } |
|
| 706 | ||
| 707 | return $size; |
|
| 708 | } |
|
| 709 | ||
| 710 | public function collectObjects( array $wants, array $haves = [] ): array { |
|
| 711 | $objs = $this->traverseObjects( $wants ); |
|
| 712 | $result = []; |
|
| 713 | ||
| 714 | if( !empty( $haves ) ) { |
|
| 715 | $haveObjs = $this->traverseObjects( $haves ); |
|
| 716 | ||
| 717 | foreach( $haveObjs as $sha => $type ) { |
|
| 718 | if( isset( $objs[$sha] ) ) { |
|
| 719 | unset( $objs[$sha] ); |
|
| 720 | } |
|
| 721 | } |
|
| 722 | } |
|
| 723 | ||
| 724 | $result = $objs; |
|
| 725 | ||
| 726 | return $result; |
|
| 727 | } |
|
| 728 | ||
| 729 | private function traverseObjects( array $roots ): array { |
|
| 730 | $objs = []; |
|
| 731 | $queue = []; |
|
| 732 | ||
| 733 | foreach( $roots as $sha ) { |
|
| 734 | $queue[] = [ 'sha' => $sha, 'type' => 0 ]; |
|
| 735 | } |
|
| 736 | ||
| 737 | while( !empty( $queue ) ) { |
|
| 738 | $item = array_pop( $queue ); |
|
| 739 | $sha = $item['sha']; |
|
| 740 | $type = $item['type']; |
|
| 741 | ||
| 742 | if( isset( $objs[$sha] ) ) { |
|
| 743 | continue; |
|
| 744 | } |
|
| 745 | ||
| 746 | $data = ''; |
|
| 747 | ||
| 748 | if( $type !== 3 ) { |
|
| 749 | $data = $this->read( $sha ); |
|
| 750 | ||
| 751 | if( $type === 0 ) { |
|
| 752 | $type = $this->getObjectType( $data ); |
|
| 753 | } |
|
| 754 | } |
|
| 755 | ||
| 756 | $objs[$sha] = $type; |
|
| 757 | ||
| 758 | if( $type === 1 ) { |
|
| 759 | $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m ); |
|
| 760 | ||
| 761 | if( $hasTree ) { |
|
| 762 | $queue[] = [ 'sha' => $m[1], 'type' => 2 ]; |
|
| 763 | } |
|
| 764 | ||
| 765 | $hasParents = preg_match_all( |
|
| 766 | '/^parent ([0-9a-f]{40})/m', |
|
| 767 | $data, |
|
| 768 | $m |
|
| 769 | ); |
|
| 770 | ||
| 771 | if( $hasParents ) { |
|
| 772 | foreach( $m[1] as $parentSha ) { |
|
| 773 | $queue[] = [ 'sha' => $parentSha, 'type' => 1 ]; |
|
| 774 | } |
|
| 775 | } |
|
| 776 | } elseif( $type === 2 ) { |
|
| 777 | $pos = 0; |
|
| 778 | $len = strlen( $data ); |
|
| 779 | ||
| 780 | while( $pos < $len ) { |
|
| 781 | $space = strpos( $data, ' ', $pos ); |
|
| 782 | $eos = strpos( $data, "\0", $space ); |
|
| 783 | ||
| 784 | if( $space === false || $eos === false ) { |
|
| 785 | break; |
|
| 786 | } |
|
| 787 | ||
| 788 | $mode = substr( $data, $pos, $space - $pos ); |
|
| 789 | $hash = bin2hex( substr( $data, $eos + 1, 20 ) ); |
|
| 790 | ||
| 791 | if( $mode !== '160000' ) { |
|
| 792 | $isDir = $mode === '40000' || $mode === '040000'; |
|
| 793 | $queue[] = [ 'sha' => $hash, 'type' => $isDir ? 2 : 3 ]; |
|
| 794 | } |
|
| 795 | ||
| 796 | $pos = $eos + 21; |
|
| 797 | } |
|
| 798 | } elseif( $type === 4 ) { |
|
| 799 | $isTagTgt = preg_match( '/^object ([0-9a-f]{40})/m', $data, $m ); |
|
| 800 | ||
| 801 | if( $isTagTgt ) { |
|
| 802 | $nextType = 1; |
|
| 803 | ||
| 804 | if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) { |
|
| 805 | $map = [ |
|
| 806 | 'commit' => 1, |
|
| 807 | 'tree' => 2, |
|
| 808 | 'blob' => 3, |
|
| 809 | 'tag' => 4 |
|
| 810 | ]; |
|
| 811 | $nextType = $map[$t[1]] ?? 1; |
|
| 812 | } |
|
| 813 | ||
| 814 | $queue[] = [ 'sha' => $m[1], 'type' => $nextType ]; |
|
| 815 | } |
|
| 816 | } |
|
| 817 | } |
|
| 818 | ||
| 819 | return $objs; |
|
| 820 | } |
|
| 821 | ||
| 822 | private function getObjectType( string $data ): int { |
|
| 823 | $isTree = strpos( $data, "tree " ) === 0; |
|
| 824 | $isObj = strpos( $data, "object " ) === 0; |
|
| 825 | $result = 3; |
|
| 826 | ||
| 827 | if( $isTree ) { |
|
| 828 | $result = 1; |
|
| 829 | } elseif( $isObj ) { |
|
| 830 | $result = 4; |
|
| 831 | } elseif( $this->isTreeData( $data ) ) { |
|
| 832 | $result = 2; |
|
| 833 | } |
|
| 834 | ||
| 835 | return $result; |
|
| 836 | } |
|
| 837 | } |
|
| 838 | ||
| 839 | class MissingFile extends File { |
|
| 840 | public function __construct() { |
|
| 841 | parent::__construct( '', '', '0', 0, 0, '' ); |
|
| 842 | } |
|
| 843 | ||
| 844 | public function emitRawHeaders(): void { |
|
| 845 | header( "HTTP/1.1 404 Not Found" ); |
|
| 846 | exit; |
|
| 847 | } |
|
| 848 | } |
|
| 849 | ||
| 833 | 850 |
| 1 | 1 | <?php |
| 2 | require_once __DIR__ . '/CompressionStream.php'; |
|
| 3 | ||
| 4 | class GitPacks { |
|
| 5 | private const MAX_READ = 1040576; |
|
| 6 | private const MAX_RAM = 1048576; |
|
| 7 | private const MAX_BASE_RAM = 2097152; |
|
| 8 | private const MAX_DEPTH = 200; |
|
| 9 | ||
| 10 | private string $objectsPath; |
|
| 11 | private array $packFiles; |
|
| 12 | private string $lastPack = ''; |
|
| 13 | private array $fileHandles; |
|
| 14 | private array $fanoutCache; |
|
| 15 | private array $shaBucketCache; |
|
| 16 | private array $offsetBucketCache; |
|
| 17 | ||
| 18 | public function __construct( string $objectsPath ) { |
|
| 19 | $this->objectsPath = $objectsPath; |
|
| 20 | $this->packFiles = glob( "{$this->objectsPath}/pack/*.idx" ) ?: []; |
|
| 21 | $this->fileHandles = []; |
|
| 22 | $this->fanoutCache = []; |
|
| 23 | $this->shaBucketCache = []; |
|
| 24 | $this->offsetBucketCache = []; |
|
| 25 | } |
|
| 26 | ||
| 27 | public function __destruct() { |
|
| 28 | foreach( $this->fileHandles as $handle ) { |
|
| 29 | if( is_resource( $handle ) ) { |
|
| 30 | fclose( $handle ); |
|
| 31 | } |
|
| 32 | } |
|
| 33 | } |
|
| 34 | ||
| 35 | public function peek( string $sha, int $len = 12 ): string { |
|
| 36 | $info = $this->findPackInfo( $sha ); |
|
| 37 | $result = ''; |
|
| 38 | ||
| 39 | if( $info['offset'] !== 0 ) { |
|
| 40 | $handle = $this->getHandle( $info['file'] ); |
|
| 41 | ||
| 42 | if( $handle ) { |
|
| 43 | $result = $this->readPackEntry( |
|
| 44 | $handle, |
|
| 45 | $info['offset'], |
|
| 46 | $len, |
|
| 47 | $len |
|
| 48 | ); |
|
| 49 | } |
|
| 50 | } |
|
| 51 | ||
| 52 | return $result; |
|
| 53 | } |
|
| 54 | ||
| 55 | public function read( string $sha ): string { |
|
| 56 | $info = $this->findPackInfo( $sha ); |
|
| 57 | $result = ''; |
|
| 58 | ||
| 59 | if( $info['offset'] !== 0 ) { |
|
| 60 | $size = $this->extractPackedSize( $info['file'], $info['offset'] ); |
|
| 61 | ||
| 62 | if( $size <= self::MAX_RAM ) { |
|
| 63 | $handle = $this->getHandle( $info['file'] ); |
|
| 64 | ||
| 65 | if( $handle ) { |
|
| 66 | $result = $this->readPackEntry( |
|
| 67 | $handle, |
|
| 68 | $info['offset'], |
|
| 69 | $size |
|
| 70 | ); |
|
| 71 | } |
|
| 72 | } |
|
| 73 | } |
|
| 74 | ||
| 75 | return $result; |
|
| 76 | } |
|
| 77 | ||
| 78 | public function stream( string $sha, callable $callback ): bool { |
|
| 79 | $result = false; |
|
| 80 | ||
| 81 | foreach( $this->streamGenerator( $sha ) as $chunk ) { |
|
| 82 | $callback( $chunk ); |
|
| 83 | $result = true; |
|
| 84 | } |
|
| 85 | ||
| 86 | return $result; |
|
| 87 | } |
|
| 88 | ||
| 89 | public function streamGenerator( string $sha ): Generator { |
|
| 90 | yield from $this->streamShaGenerator( $sha, 0 ); |
|
| 91 | } |
|
| 92 | ||
| 93 | private function streamShaGenerator( string $sha, int $depth ): Generator { |
|
| 94 | $info = $this->findPackInfo( $sha ); |
|
| 95 | ||
| 96 | if( $info['offset'] !== 0 ) { |
|
| 97 | $handle = $this->getHandle( $info['file'] ); |
|
| 98 | ||
| 99 | if( $handle ) { |
|
| 100 | yield from $this->streamPackEntryGenerator( |
|
| 101 | $handle, |
|
| 102 | $info['offset'], |
|
| 103 | $depth |
|
| 104 | ); |
|
| 105 | } |
|
| 106 | } |
|
| 107 | } |
|
| 108 | ||
| 109 | public function getSize( string $sha ): int { |
|
| 110 | $info = $this->findPackInfo( $sha ); |
|
| 111 | $result = 0; |
|
| 112 | ||
| 113 | if( $info['offset'] !== 0 ) { |
|
| 114 | $result = $this->extractPackedSize( $info['file'], $info['offset'] ); |
|
| 115 | } |
|
| 116 | ||
| 117 | return $result; |
|
| 118 | } |
|
| 119 | ||
| 120 | private function findPackInfo( string $sha ): array { |
|
| 121 | $result = [ 'offset' => 0, 'file' => '' ]; |
|
| 122 | ||
| 123 | if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) { |
|
| 124 | $binarySha = hex2bin( $sha ); |
|
| 125 | ||
| 126 | if( $this->lastPack !== '' ) { |
|
| 127 | $offset = $this->findInIdx( $this->lastPack, $binarySha ); |
|
| 128 | ||
| 129 | if( $offset !== 0 ) { |
|
| 130 | $result = [ |
|
| 131 | 'file' => str_replace( '.idx', '.pack', $this->lastPack ), |
|
| 132 | 'offset' => $offset |
|
| 133 | ]; |
|
| 134 | } |
|
| 135 | } |
|
| 136 | ||
| 137 | if( $result['offset'] === 0 ) { |
|
| 138 | $count = count( $this->packFiles ); |
|
| 139 | $idx = 0; |
|
| 140 | $found = false; |
|
| 141 | ||
| 142 | while( !$found && $idx < $count ) { |
|
| 143 | $indexFile = $this->packFiles[$idx]; |
|
| 144 | ||
| 145 | if( $indexFile !== $this->lastPack ) { |
|
| 146 | $offset = $this->findInIdx( $indexFile, $binarySha ); |
|
| 147 | ||
| 148 | if( $offset !== 0 ) { |
|
| 149 | $this->lastPack = $indexFile; |
|
| 150 | $result = [ |
|
| 151 | 'file' => str_replace( '.idx', '.pack', $indexFile ), |
|
| 152 | 'offset' => $offset |
|
| 153 | ]; |
|
| 154 | $found = true; |
|
| 155 | } |
|
| 156 | } |
|
| 157 | ||
| 158 | $idx++; |
|
| 159 | } |
|
| 160 | } |
|
| 161 | } |
|
| 162 | ||
| 163 | return $result; |
|
| 164 | } |
|
| 165 | ||
| 166 | private function findInIdx( string $indexFile, string $binarySha ): int { |
|
| 167 | $handle = $this->getHandle( $indexFile ); |
|
| 168 | $result = 0; |
|
| 169 | ||
| 170 | if( $handle ) { |
|
| 171 | if( !isset( $this->fanoutCache[$indexFile] ) ) { |
|
| 172 | fseek( $handle, 0 ); |
|
| 173 | $head = fread( $handle, 8 ); |
|
| 174 | ||
| 175 | if( $head === "\377tOc\0\0\0\2" ) { |
|
| 176 | $this->fanoutCache[$indexFile] = array_values( |
|
| 177 | unpack( 'N*', fread( $handle, 1024 ) ) |
|
| 178 | ); |
|
| 179 | } |
|
| 180 | } |
|
| 181 | ||
| 182 | if( isset( $this->fanoutCache[$indexFile] ) ) { |
|
| 183 | $fanout = $this->fanoutCache[$indexFile]; |
|
| 184 | $byte = ord( $binarySha[0] ); |
|
| 185 | $start = $byte === 0 ? 0 : $fanout[$byte - 1]; |
|
| 186 | $end = $fanout[$byte]; |
|
| 187 | ||
| 188 | if( $end > $start ) { |
|
| 189 | $result = $this->binarySearchIdx( |
|
| 190 | $indexFile, |
|
| 191 | $handle, |
|
| 192 | $start, |
|
| 193 | $end, |
|
| 194 | $binarySha, |
|
| 195 | $fanout[255] |
|
| 196 | ); |
|
| 197 | } |
|
| 198 | } |
|
| 199 | } |
|
| 200 | ||
| 201 | return $result; |
|
| 202 | } |
|
| 203 | ||
| 204 | private function binarySearchIdx( |
|
| 205 | string $indexFile, |
|
| 206 | $handle, |
|
| 207 | int $start, |
|
| 208 | int $end, |
|
| 209 | string $binarySha, |
|
| 210 | int $total |
|
| 211 | ): int { |
|
| 212 | $key = "$indexFile:$start"; |
|
| 213 | $count = $end - $start; |
|
| 214 | $result = 0; |
|
| 215 | ||
| 216 | if( !isset( $this->shaBucketCache[$key] ) ) { |
|
| 217 | fseek( $handle, 1032 + ($start * 20) ); |
|
| 218 | $this->shaBucketCache[$key] = fread( $handle, $count * 20 ); |
|
| 219 | ||
| 220 | fseek( $handle, 1032 + ($total * 24) + ($start * 4) ); |
|
| 221 | $this->offsetBucketCache[$key] = fread( $handle, $count * 4 ); |
|
| 222 | } |
|
| 223 | ||
| 224 | $shaBlock = $this->shaBucketCache[$key]; |
|
| 225 | $low = 0; |
|
| 226 | $high = $count - 1; |
|
| 227 | $found = -1; |
|
| 228 | ||
| 229 | while( $found === -1 && $low <= $high ) { |
|
| 230 | $mid = ($low + $high) >> 1; |
|
| 231 | $cmp = substr( $shaBlock, $mid * 20, 20 ); |
|
| 232 | ||
| 233 | if( $cmp < $binarySha ) { |
|
| 234 | $low = $mid + 1; |
|
| 235 | } elseif( $cmp > $binarySha ) { |
|
| 236 | $high = $mid - 1; |
|
| 237 | } else { |
|
| 238 | $found = $mid; |
|
| 239 | } |
|
| 240 | } |
|
| 241 | ||
| 242 | if( $found !== -1 ) { |
|
| 243 | $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 ); |
|
| 244 | $offset = unpack( 'N', $packed )[1]; |
|
| 245 | ||
| 246 | if( $offset & 0x80000000 ) { |
|
| 247 | $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8); |
|
| 248 | ||
| 249 | fseek( $handle, $pos64 ); |
|
| 250 | $offset = unpack( 'J', fread( $handle, 8 ) )[1]; |
|
| 251 | } |
|
| 252 | ||
| 253 | $result = (int)$offset; |
|
| 254 | } |
|
| 255 | ||
| 256 | return $result; |
|
| 257 | } |
|
| 258 | ||
| 259 | private function readPackEntry( |
|
| 260 | $handle, |
|
| 261 | int $offset, |
|
| 262 | int $size, |
|
| 263 | int $cap = 0 |
|
| 264 | ): string { |
|
| 265 | fseek( $handle, $offset ); |
|
| 266 | $header = $this->readVarInt( $handle ); |
|
| 267 | $type = ($header['byte'] >> 4) & 7; |
|
| 268 | $result = ''; |
|
| 269 | ||
| 270 | if( $type === 6 ) { |
|
| 271 | $result = $this->handleOfsDelta( $handle, $offset, $size, $cap ); |
|
| 272 | } elseif( $type === 7 ) { |
|
| 273 | $result = $this->handleRefDelta( $handle, $size, $cap ); |
|
| 274 | } else { |
|
| 275 | $result = $this->decompressToString( $handle, $cap ); |
|
| 276 | } |
|
| 277 | ||
| 278 | return $result; |
|
| 279 | } |
|
| 280 | ||
| 281 | private function streamPackEntryGenerator( |
|
| 282 | $handle, |
|
| 283 | int $offset, |
|
| 284 | int $depth |
|
| 285 | ): Generator { |
|
| 286 | fseek( $handle, $offset ); |
|
| 287 | $header = $this->readVarInt( $handle ); |
|
| 288 | $type = ($header['byte'] >> 4) & 7; |
|
| 289 | ||
| 290 | if( $type === 6 || $type === 7 ) { |
|
| 291 | yield from $this->streamDeltaObjectGenerator( |
|
| 292 | $handle, |
|
| 293 | $offset, |
|
| 294 | $type, |
|
| 295 | $depth |
|
| 296 | ); |
|
| 297 | } else { |
|
| 298 | yield from $this->streamDecompressionGenerator( $handle ); |
|
| 299 | } |
|
| 300 | } |
|
| 301 | ||
| 302 | private function resolveBaseToTempFile( |
|
| 303 | $packHandle, |
|
| 304 | int $baseOffset, |
|
| 305 | int $depth |
|
| 306 | ) { |
|
| 307 | $tmpHandle = tmpfile(); |
|
| 308 | ||
| 309 | if( $tmpHandle !== false ) { |
|
| 310 | foreach( $this->streamPackEntryGenerator( |
|
| 311 | $packHandle, |
|
| 312 | $baseOffset, |
|
| 313 | $depth + 1 |
|
| 314 | ) as $chunk ) { |
|
| 315 | fwrite( $tmpHandle, $chunk ); |
|
| 316 | } |
|
| 317 | ||
| 318 | rewind( $tmpHandle ); |
|
| 319 | } else { |
|
| 320 | error_log( |
|
| 321 | "[GitPacks] tmpfile failed for ofs-delta base at $baseOffset" |
|
| 322 | ); |
|
| 323 | } |
|
| 324 | ||
| 325 | return $tmpHandle; |
|
| 326 | } |
|
| 327 | ||
| 328 | private function streamDeltaObjectGenerator( |
|
| 329 | $handle, |
|
| 330 | int $offset, |
|
| 331 | int $type, |
|
| 332 | int $depth |
|
| 333 | ): Generator { |
|
| 334 | if( $depth < self::MAX_DEPTH ) { |
|
| 335 | fseek( $handle, $offset ); |
|
| 336 | $this->readVarInt( $handle ); |
|
| 337 | ||
| 338 | if( $type === 6 ) { |
|
| 339 | $neg = $this->readOffsetDelta( $handle ); |
|
| 340 | $deltaPos = ftell( $handle ); |
|
| 341 | $baseSize = $this->extractPackedSize( $handle, $offset - $neg ); |
|
| 342 | ||
| 343 | if( $baseSize > self::MAX_BASE_RAM ) { |
|
| 344 | $tmpHandle = $this->resolveBaseToTempFile( |
|
| 345 | $handle, |
|
| 346 | $offset - $neg, |
|
| 347 | $depth |
|
| 348 | ); |
|
| 349 | ||
| 350 | if( $tmpHandle !== false ) { |
|
| 351 | fseek( $handle, $deltaPos ); |
|
| 352 | yield from $this->applyDeltaStreamGenerator( |
|
| 353 | $handle, |
|
| 354 | $tmpHandle |
|
| 355 | ); |
|
| 356 | ||
| 357 | fclose( $tmpHandle ); |
|
| 358 | } |
|
| 359 | } else { |
|
| 360 | $base = ''; |
|
| 361 | ||
| 362 | foreach( $this->streamPackEntryGenerator( |
|
| 363 | $handle, |
|
| 364 | $offset - $neg, |
|
| 365 | $depth + 1 |
|
| 366 | ) as $chunk ) { |
|
| 367 | $base .= $chunk; |
|
| 368 | } |
|
| 369 | ||
| 370 | fseek( $handle, $deltaPos ); |
|
| 371 | yield from $this->applyDeltaStreamGenerator( $handle, $base ); |
|
| 372 | } |
|
| 373 | } else { |
|
| 374 | $baseSha = bin2hex( fread( $handle, 20 ) ); |
|
| 375 | $baseSize = $this->getSize( $baseSha ); |
|
| 376 | ||
| 377 | if( $baseSize > self::MAX_BASE_RAM ) { |
|
| 378 | $tmpHandle = tmpfile(); |
|
| 379 | ||
| 380 | if( $tmpHandle !== false ) { |
|
| 381 | $written = false; |
|
| 382 | ||
| 383 | foreach( $this->streamShaGenerator( |
|
| 384 | $baseSha, |
|
| 385 | $depth + 1 |
|
| 386 | ) as $chunk ) { |
|
| 387 | fwrite( $tmpHandle, $chunk ); |
|
| 388 | $written = true; |
|
| 389 | } |
|
| 390 | ||
| 391 | if( $written ) { |
|
| 392 | rewind( $tmpHandle ); |
|
| 393 | yield from $this->applyDeltaStreamGenerator( |
|
| 394 | $handle, |
|
| 395 | $tmpHandle |
|
| 396 | ); |
|
| 397 | } |
|
| 398 | ||
| 399 | fclose( $tmpHandle ); |
|
| 400 | } else { |
|
| 401 | error_log( |
|
| 402 | "[GitPacks] tmpfile() failed for ref-delta (sha=$baseSha)" |
|
| 403 | ); |
|
| 404 | } |
|
| 405 | } else { |
|
| 406 | $base = ''; |
|
| 407 | $written = false; |
|
| 408 | ||
| 409 | foreach( $this->streamShaGenerator( |
|
| 410 | $baseSha, |
|
| 411 | $depth + 1 |
|
| 412 | ) as $chunk ) { |
|
| 413 | $base .= $chunk; |
|
| 414 | $written = true; |
|
| 415 | } |
|
| 416 | ||
| 417 | if( $written ) { |
|
| 418 | yield from $this->applyDeltaStreamGenerator( $handle, $base ); |
|
| 419 | } |
|
| 420 | } |
|
| 421 | } |
|
| 422 | } else { |
|
| 423 | error_log( "[GitPacks] delta depth limit exceeded at offset $offset" ); |
|
| 424 | } |
|
| 425 | } |
|
| 426 | ||
| 427 | private function applyDeltaStreamGenerator( |
|
| 428 | $handle, |
|
| 429 | $base |
|
| 430 | ): Generator { |
|
| 431 | $stream = CompressionStream::createInflater(); |
|
| 432 | $state = 0; |
|
| 433 | $buffer = ''; |
|
| 434 | $done = false; |
|
| 435 | $isFile = is_resource( $base ); |
|
| 436 | ||
| 437 | while( !$done && !feof( $handle ) ) { |
|
| 438 | $chunk = fread( $handle, 8192 ); |
|
| 439 | $done = $chunk === false || $chunk === ''; |
|
| 440 | ||
| 441 | if( !$done ) { |
|
| 442 | $data = $stream->pump( $chunk ); |
|
| 443 | ||
| 444 | if( $data !== '' ) { |
|
| 445 | $buffer .= $data; |
|
| 446 | $doneBuffer = false; |
|
| 447 | ||
| 448 | while( !$doneBuffer ) { |
|
| 449 | $len = strlen( $buffer ); |
|
| 450 | ||
| 451 | if( $len === 0 ) { |
|
| 452 | $doneBuffer = true; |
|
| 453 | } |
|
| 454 | ||
| 455 | if( !$doneBuffer ) { |
|
| 456 | if( $state < 2 ) { |
|
| 457 | $pos = 0; |
|
| 458 | ||
| 459 | while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { |
|
| 460 | $pos++; |
|
| 461 | } |
|
| 462 | ||
| 463 | if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) { |
|
| 464 | $doneBuffer = true; |
|
| 465 | } |
|
| 466 | ||
| 467 | if( !$doneBuffer ) { |
|
| 468 | $buffer = substr( $buffer, $pos + 1 ); |
|
| 469 | $state++; |
|
| 470 | } |
|
| 471 | } else { |
|
| 472 | $op = ord( $buffer[0] ); |
|
| 473 | ||
| 474 | if( $op & 128 ) { |
|
| 475 | $need = $this->getCopyInstructionSize( $op ); |
|
| 476 | ||
| 477 | if( $len < 1 + $need ) { |
|
| 478 | $doneBuffer = true; |
|
| 479 | } |
|
| 480 | ||
| 481 | if( !$doneBuffer ) { |
|
| 482 | $info = $this->parseCopyInstruction( $op, $buffer, 1 ); |
|
| 483 | ||
| 484 | if( $isFile ) { |
|
| 485 | fseek( $base, $info['off'] ); |
|
| 486 | $rem = $info['len']; |
|
| 487 | ||
| 488 | while( $rem > 0 ) { |
|
| 489 | $slc = fread( $base, min( 65536, $rem ) ); |
|
| 490 | ||
| 491 | if( $slc === false || $slc === '' ) { |
|
| 492 | $rem = 0; |
|
| 493 | } else { |
|
| 494 | yield $slc; |
|
| 495 | $rem -= strlen( $slc ); |
|
| 496 | } |
|
| 497 | } |
|
| 498 | } else { |
|
| 499 | yield substr( $base, $info['off'], $info['len'] ); |
|
| 500 | } |
|
| 501 | ||
| 502 | $buffer = substr( $buffer, 1 + $need ); |
|
| 503 | } |
|
| 504 | } else { |
|
| 505 | $ln = $op & 127; |
|
| 506 | ||
| 507 | if( $len < 1 + $ln ) { |
|
| 508 | $doneBuffer = true; |
|
| 509 | } |
|
| 510 | ||
| 511 | if( !$doneBuffer ) { |
|
| 512 | yield substr( $buffer, 1, $ln ); |
|
| 513 | $buffer = substr( $buffer, 1 + $ln ); |
|
| 514 | } |
|
| 515 | } |
|
| 516 | } |
|
| 517 | } |
|
| 518 | } |
|
| 519 | } |
|
| 520 | ||
| 521 | $done = $stream->finished(); |
|
| 522 | } |
|
| 523 | } |
|
| 524 | } |
|
| 525 | ||
| 526 | private function streamDecompressionGenerator( $handle ): Generator { |
|
| 527 | $stream = CompressionStream::createInflater(); |
|
| 528 | $done = false; |
|
| 529 | ||
| 530 | while( !$done && !feof( $handle ) ) { |
|
| 531 | $chunk = fread( $handle, 8192 ); |
|
| 532 | $done = $chunk === false || $chunk === ''; |
|
| 533 | ||
| 534 | if( !$done ) { |
|
| 535 | $data = $stream->pump( $chunk ); |
|
| 536 | ||
| 537 | if( $data !== '' ) { |
|
| 538 | yield $data; |
|
| 539 | } |
|
| 540 | ||
| 541 | $done = $stream->finished(); |
|
| 542 | } |
|
| 543 | } |
|
| 544 | } |
|
| 545 | ||
| 546 | private function decompressToString( |
|
| 547 | $handle, |
|
| 548 | int $cap = 0 |
|
| 549 | ): string { |
|
| 550 | $stream = CompressionStream::createInflater(); |
|
| 551 | $res = ''; |
|
| 552 | $done = false; |
|
| 553 | ||
| 554 | while( !$done && !feof( $handle ) ) { |
|
| 555 | $chunk = fread( $handle, 8192 ); |
|
| 556 | $done = $chunk === false || $chunk === ''; |
|
| 557 | ||
| 558 | if( !$done ) { |
|
| 559 | $data = $stream->pump( $chunk ); |
|
| 560 | ||
| 561 | if( $data !== '' ) { |
|
| 562 | $res .= $data; |
|
| 563 | } |
|
| 564 | ||
| 565 | if( $cap > 0 && strlen( $res ) >= $cap ) { |
|
| 566 | $res = substr( $res, 0, $cap ); |
|
| 567 | $done = true; |
|
| 568 | } |
|
| 569 | ||
| 570 | if( !$done ) { |
|
| 571 | $done = $stream->finished(); |
|
| 572 | } |
|
| 573 | } |
|
| 574 | } |
|
| 575 | ||
| 576 | return $res; |
|
| 577 | } |
|
| 578 | ||
| 579 | private function extractPackedSize( $packPathOrHandle, int $offset ): int { |
|
| 580 | $handle = is_resource( $packPathOrHandle ) |
|
| 581 | ? $packPathOrHandle |
|
| 582 | : $this->getHandle( $packPathOrHandle ); |
|
| 583 | $size = 0; |
|
| 584 | ||
| 585 | if( $handle ) { |
|
| 586 | fseek( $handle, $offset ); |
|
| 587 | $header = $this->readVarInt( $handle ); |
|
| 588 | $size = $header['value']; |
|
| 589 | $type = ($header['byte'] >> 4) & 7; |
|
| 590 | ||
| 591 | if( $type === 6 || $type === 7 ) { |
|
| 592 | $size = $this->readDeltaTargetSize( $handle, $type ); |
|
| 593 | } |
|
| 594 | } |
|
| 595 | ||
| 596 | return $size; |
|
| 597 | } |
|
| 598 | ||
| 599 | private function handleOfsDelta( |
|
| 600 | $handle, |
|
| 601 | int $offset, |
|
| 602 | int $size, |
|
| 603 | int $cap |
|
| 604 | ): string { |
|
| 605 | $neg = $this->readOffsetDelta( $handle ); |
|
| 606 | $cur = ftell( $handle ); |
|
| 607 | $base = $offset - $neg; |
|
| 608 | ||
| 609 | fseek( $handle, $base ); |
|
| 610 | $bHead = $this->readVarInt( $handle ); |
|
| 611 | ||
| 612 | fseek( $handle, $base ); |
|
| 613 | $bData = $this->readPackEntry( $handle, $base, $bHead['value'], $cap ); |
|
| 614 | ||
| 615 | fseek( $handle, $cur ); |
|
| 616 | $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
|
| 617 | $comp = fread( $handle, $rem ); |
|
| 618 | $delta = @gzuncompress( $comp ) ?: ''; |
|
| 619 | ||
| 620 | return $this->applyDelta( $bData, $delta, $cap ); |
|
| 621 | } |
|
| 622 | ||
| 623 | private function handleRefDelta( $handle, int $size, int $cap ): string { |
|
| 624 | $sha = bin2hex( fread( $handle, 20 ) ); |
|
| 625 | $bas = $cap > 0 ? $this->peek( $sha, $cap ) : $this->read( $sha ); |
|
| 626 | $rem = min( self::MAX_READ, max( $size * 2, 1048576 ) ); |
|
| 627 | $cmp = fread( $handle, $rem ); |
|
| 628 | $del = @gzuncompress( $cmp ) ?: ''; |
|
| 629 | ||
| 630 | return $this->applyDelta( $bas, $del, $cap ); |
|
| 631 | } |
|
| 632 | ||
| 633 | private function applyDelta( string $base, string $delta, int $cap ): string { |
|
| 634 | $pos = 0; |
|
| 635 | $res = $this->readDeltaSize( $delta, $pos ); |
|
| 636 | $pos += $res['used']; |
|
| 637 | $res = $this->readDeltaSize( $delta, $pos ); |
|
| 638 | $pos += $res['used']; |
|
| 639 | ||
| 640 | $out = ''; |
|
| 641 | $len = strlen( $delta ); |
|
| 642 | $done = false; |
|
| 643 | ||
| 644 | while( !$done && $pos < $len ) { |
|
| 645 | if( $cap > 0 && strlen( $out ) >= $cap ) { |
|
| 646 | $done = true; |
|
| 647 | } |
|
| 648 | ||
| 649 | if( !$done ) { |
|
| 650 | $op = ord( $delta[$pos++] ); |
|
| 651 | ||
| 652 | if( $op & 128 ) { |
|
| 653 | $info = $this->parseCopyInstruction( $op, $delta, $pos ); |
|
| 654 | $out .= substr( $base, $info['off'], $info['len'] ); |
|
| 655 | $pos += $info['used']; |
|
| 656 | } else { |
|
| 657 | $ln = $op & 127; |
|
| 658 | $out .= substr( $delta, $pos, $ln ); |
|
| 659 | $pos += $ln; |
|
| 660 | } |
|
| 661 | } |
|
| 662 | } |
|
| 663 | ||
| 664 | return $out; |
|
| 665 | } |
|
| 666 | ||
| 667 | private function parseCopyInstruction( |
|
| 668 | int $op, |
|
| 669 | string $data, |
|
| 670 | int $pos |
|
| 671 | ): array { |
|
| 672 | $off = 0; |
|
| 673 | $len = 0; |
|
| 674 | $ptr = $pos; |
|
| 675 | ||
| 676 | if( $op & 0x01 ) { |
|
| 677 | $off |= ord( $data[$ptr++] ); |
|
| 678 | } |
|
| 679 | ||
| 680 | if( $op & 0x02 ) { |
|
| 681 | $off |= ord( $data[$ptr++] ) << 8; |
|
| 682 | } |
|
| 683 | ||
| 684 | if( $op & 0x04 ) { |
|
| 685 | $off |= ord( $data[$ptr++] ) << 16; |
|
| 686 | } |
|
| 687 | ||
| 688 | if( $op & 0x08 ) { |
|
| 689 | $off |= ord( $data[$ptr++] ) << 24; |
|
| 690 | } |
|
| 691 | ||
| 692 | if( $op & 0x10 ) { |
|
| 693 | $len |= ord( $data[$ptr++] ); |
|
| 694 | } |
|
| 695 | ||
| 696 | if( $op & 0x20 ) { |
|
| 697 | $len |= ord( $data[$ptr++] ) << 8; |
|
| 698 | } |
|
| 699 | ||
| 700 | if( $op & 0x40 ) { |
|
| 701 | $len |= ord( $data[$ptr++] ) << 16; |
|
| 702 | } |
|
| 703 | ||
| 704 | return [ |
|
| 705 | 'off' => $off, |
|
| 706 | 'len' => $len === 0 ? 0x10000 : $len, |
|
| 707 | 'used' => $ptr - $pos |
|
| 708 | ]; |
|
| 709 | } |
|
| 710 | ||
| 711 | private function getCopyInstructionSize( int $op ): int { |
|
| 712 | $c = $op & 0x7F; |
|
| 713 | $c = $c - (($c >> 1) & 0x55); |
|
| 714 | $c = (($c >> 2) & 0x33) + ($c & 0x33); |
|
| 715 | $c = (($c >> 4) + $c) & 0x0F; |
|
| 716 | ||
| 717 | return $c; |
|
| 718 | } |
|
| 719 | ||
| 720 | private function readVarInt( $handle ): array { |
|
| 721 | $byte = ord( fread( $handle, 1 ) ); |
|
| 722 | $val = $byte & 15; |
|
| 723 | $shft = 4; |
|
| 724 | $fst = $byte; |
|
| 725 | ||
| 726 | while( $byte & 128 ) { |
|
| 727 | $byte = ord( fread( $handle, 1 ) ); |
|
| 728 | $val |= (($byte & 127) << $shft); |
|
| 729 | $shft += 7; |
|
| 730 | } |
|
| 731 | ||
| 732 | return [ 'value' => $val, 'byte' => $fst ]; |
|
| 733 | } |
|
| 734 | ||
| 735 | private function readOffsetDelta( $handle ): int { |
|
| 736 | $byte = ord( fread( $handle, 1 ) ); |
|
| 737 | $neg = $byte & 127; |
|
| 738 | ||
| 739 | while( $byte & 128 ) { |
|
| 740 | $byte = ord( fread( $handle, 1 ) ); |
|
| 741 | $neg = (($neg + 1) << 7) | ($byte & 127); |
|
| 742 | } |
|
| 743 | ||
| 744 | return $neg; |
|
| 745 | } |
|
| 746 | ||
| 747 | private function readDeltaTargetSize( $handle, int $type ): int { |
|
| 748 | if( $type === 6 ) { |
|
| 749 | $b = ord( fread( $handle, 1 ) ); |
|
| 750 | ||
| 751 | while( $b & 128 ) { |
|
| 752 | $b = ord( fread( $handle, 1 ) ); |
|
| 753 | } |
|
| 754 | } else { |
|
| 755 | fseek( $handle, 20, SEEK_CUR ); |
|
| 756 | } |
|
| 757 | ||
| 758 | $stream = CompressionStream::createInflater(); |
|
| 759 | $head = ''; |
|
| 760 | $try = 0; |
|
| 761 | $done = false; |
|
| 762 | ||
| 763 | while( !$done && !feof( $handle ) && strlen( $head ) < 32 && $try < 64 ) { |
|
| 764 | $chunk = fread( $handle, 512 ); |
|
| 765 | $done = $chunk === false || $chunk === ''; |
|
| 766 | ||
| 767 | if( !$done ) { |
|
| 768 | $out = $stream->pump( $chunk ); |
|
| 769 | ||
| 770 | if( $out !== '' ) { |
|
| 771 | $head .= $out; |
|
| 772 | } |
|
| 773 | ||
| 774 | $done = $stream->finished(); |
|
| 775 | $try++; |
|
| 776 | } |
|
| 777 | } |
|
| 778 | ||
| 779 | $pos = 0; |
|
| 780 | $result = 0; |
|
| 781 | ||
| 782 | if( strlen( $head ) > 0 ) { |
|
| 783 | $res = $this->readDeltaSize( $head, $pos ); |
|
| 784 | $pos += $res['used']; |
|
| 785 | $res = $this->readDeltaSize( $head, $pos ); |
|
| 786 | ||
| 787 | $result = $res['val']; |
|
| 788 | } |
|
| 789 | ||
| 790 | return $result; |
|
| 791 | } |
|
| 792 | ||
| 793 | private function readDeltaSize( string $data, int $pos ): array { |
|
| 794 | $len = strlen( $data ); |
|
| 795 | $val = 0; |
|
| 796 | $shift = 0; |
|
| 797 | $start = $pos; |
|
| 798 | $done = false; |
|
| 799 | ||
| 800 | while( !$done && $pos < $len ) { |
|
| 801 | $byte = ord( $data[$pos++] ); |
|
| 802 | $val |= ($byte & 0x7F) << $shift; |
|
| 803 | ||
| 804 | if( !($byte & 0x80) ) { |
|
| 805 | $done = true; |
|
| 806 | } |
|
| 807 | ||
| 808 | if( !$done ) { |
|
| 809 | $shift += 7; |
|
| 810 | } |
|
| 811 | } |
|
| 812 | ||
| 813 | return [ 'val' => $val, 'used' => $pos - $start ]; |
|
| 814 | } |
|
| 815 | ||
| 816 | private function getHandle( string $path ) { |
|
| 817 | if( !isset( $this->fileHandles[$path] ) ) { |
|
| 818 | $this->fileHandles[$path] = @fopen( $path, 'rb' ); |
|
| 819 | } |
|
| 820 | ||
| 821 | return $this->fileHandles[$path]; |
|
| 2 | require_once __DIR__ . '/FileHandlePool.php'; |
|
| 3 | require_once __DIR__ . '/PackLocator.php'; |
|
| 4 | require_once __DIR__ . '/DeltaDecoder.php'; |
|
| 5 | require_once __DIR__ . '/PackEntryReader.php'; |
|
| 6 | ||
| 7 | class GitPacks { |
|
| 8 | private const MAX_RAM = 1048576; |
|
| 9 | ||
| 10 | private FileHandlePool $pool; |
|
| 11 | private PackLocator $locator; |
|
| 12 | private PackEntryReader $reader; |
|
| 13 | ||
| 14 | public function __construct( string $objectsPath ) { |
|
| 15 | $this->pool = new FileHandlePool(); |
|
| 16 | $this->locator = new PackLocator( $objectsPath ); |
|
| 17 | $this->reader = new PackEntryReader( new DeltaDecoder() ); |
|
| 18 | } |
|
| 19 | ||
| 20 | public function peek( string $sha, int $len = 12 ): string { |
|
| 21 | $result = ''; |
|
| 22 | ||
| 23 | $this->locator->locate( |
|
| 24 | $this->pool, |
|
| 25 | $sha, |
|
| 26 | function( string $packFile, int $offset ) use ( &$result, $len ): void { |
|
| 27 | $result = $this->reader->read( |
|
| 28 | $this->pool, |
|
| 29 | $packFile, |
|
| 30 | $offset, |
|
| 31 | $len, |
|
| 32 | function( string $baseSha, int $cap ): string { |
|
| 33 | return $this->peek( $baseSha, $cap ); |
|
| 34 | } |
|
| 35 | ); |
|
| 36 | } |
|
| 37 | ); |
|
| 38 | ||
| 39 | return $result; |
|
| 40 | } |
|
| 41 | ||
| 42 | public function read( string $sha ): string { |
|
| 43 | $result = ''; |
|
| 44 | ||
| 45 | $this->locator->locate( |
|
| 46 | $this->pool, |
|
| 47 | $sha, |
|
| 48 | function( string $packFile, int $offset ) use ( &$result ): void { |
|
| 49 | $size = $this->reader->getSize( $this->pool, $packFile, $offset ); |
|
| 50 | ||
| 51 | if( $size <= self::MAX_RAM ) { |
|
| 52 | $result = $this->reader->read( |
|
| 53 | $this->pool, |
|
| 54 | $packFile, |
|
| 55 | $offset, |
|
| 56 | 0, |
|
| 57 | function( string $baseSha, int $cap ): string { |
|
| 58 | $val = ''; |
|
| 59 | ||
| 60 | if( $cap > 0 ) { |
|
| 61 | $val = $this->peek( $baseSha, $cap ); |
|
| 62 | } else { |
|
| 63 | $val = $this->read( $baseSha ); |
|
| 64 | } |
|
| 65 | ||
| 66 | return $val; |
|
| 67 | } |
|
| 68 | ); |
|
| 69 | } |
|
| 70 | } |
|
| 71 | ); |
|
| 72 | ||
| 73 | return $result; |
|
| 74 | } |
|
| 75 | ||
| 76 | public function stream( string $sha, callable $callback ): bool { |
|
| 77 | $result = false; |
|
| 78 | ||
| 79 | foreach( $this->streamGenerator( $sha ) as $chunk ) { |
|
| 80 | $callback( $chunk ); |
|
| 81 | ||
| 82 | $result = true; |
|
| 83 | } |
|
| 84 | ||
| 85 | return $result; |
|
| 86 | } |
|
| 87 | ||
| 88 | public function streamGenerator( string $sha ): Generator { |
|
| 89 | yield from $this->streamShaGenerator( $sha, 0 ); |
|
| 90 | } |
|
| 91 | ||
| 92 | public function streamRawCompressed( string $sha ): Generator { |
|
| 93 | $found = false; |
|
| 94 | $file = ''; |
|
| 95 | $off = 0; |
|
| 96 | ||
| 97 | $this->locator->locate( |
|
| 98 | $this->pool, |
|
| 99 | $sha, |
|
| 100 | function( string $packFile, int $offset ) use ( |
|
| 101 | &$found, |
|
| 102 | &$file, |
|
| 103 | &$off |
|
| 104 | ): void { |
|
| 105 | $found = true; |
|
| 106 | $file = $packFile; |
|
| 107 | $off = $offset; |
|
| 108 | } |
|
| 109 | ); |
|
| 110 | ||
| 111 | if( $found ) { |
|
| 112 | yield from $this->reader->streamRawCompressed( |
|
| 113 | $this->pool, |
|
| 114 | $file, |
|
| 115 | $off |
|
| 116 | ); |
|
| 117 | } |
|
| 118 | } |
|
| 119 | ||
| 120 | private function streamShaGenerator( string $sha, int $depth ): Generator { |
|
| 121 | $found = false; |
|
| 122 | $file = ''; |
|
| 123 | $off = 0; |
|
| 124 | ||
| 125 | $this->locator->locate( |
|
| 126 | $this->pool, |
|
| 127 | $sha, |
|
| 128 | function( string $packFile, int $offset ) use ( |
|
| 129 | &$found, |
|
| 130 | &$file, |
|
| 131 | &$off |
|
| 132 | ): void { |
|
| 133 | $found = true; |
|
| 134 | $file = $packFile; |
|
| 135 | $off = $offset; |
|
| 136 | } |
|
| 137 | ); |
|
| 138 | ||
| 139 | if( $found ) { |
|
| 140 | yield from $this->reader->streamEntryGenerator( |
|
| 141 | $this->pool, |
|
| 142 | $file, |
|
| 143 | $off, |
|
| 144 | $depth, |
|
| 145 | function( string $baseSha ): int { |
|
| 146 | return $this->getSize( $baseSha ); |
|
| 147 | }, |
|
| 148 | function( string $baseSha, int $baseDepth ): Generator { |
|
| 149 | yield from $this->streamShaGenerator( $baseSha, $baseDepth ); |
|
| 150 | } |
|
| 151 | ); |
|
| 152 | } |
|
| 153 | } |
|
| 154 | ||
| 155 | public function getSize( string $sha ): int { |
|
| 156 | $result = 0; |
|
| 157 | ||
| 158 | $this->locator->locate( |
|
| 159 | $this->pool, |
|
| 160 | $sha, |
|
| 161 | function( string $packFile, int $offset ) use ( &$result ): void { |
|
| 162 | $result = $this->reader->getSize( $this->pool, $packFile, $offset ); |
|
| 163 | } |
|
| 164 | ); |
|
| 165 | ||
| 166 | return $result; |
|
| 822 | 167 | } |
| 823 | 168 | } |
| 1 | <?php |
|
| 2 | require_once __DIR__ . '/FileHandlePool.php'; |
|
| 3 | require_once __DIR__ . '/DeltaDecoder.php'; |
|
| 4 | require_once __DIR__ . '/CompressionStream.php'; |
|
| 5 | ||
| 6 | class PackEntryReader { |
|
| 7 | private const MAX_DEPTH = 200; |
|
| 8 | private const MAX_BASE_RAM = 2097152; |
|
| 9 | ||
| 10 | private DeltaDecoder $decoder; |
|
| 11 | ||
| 12 | public function __construct( DeltaDecoder $decoder ) { |
|
| 13 | $this->decoder = $decoder; |
|
| 14 | } |
|
| 15 | ||
| 16 | public function getSize( |
|
| 17 | FileHandlePool $pool, |
|
| 18 | string $packFile, |
|
| 19 | int $offset |
|
| 20 | ): int { |
|
| 21 | return $pool->computeInt( |
|
| 22 | $packFile, |
|
| 23 | function( mixed $handle ) use ( $offset ): int { |
|
| 24 | fseek( $handle, $offset ); |
|
| 25 | ||
| 26 | $header = $this->readVarInt( $handle ); |
|
| 27 | $size = $header['value']; |
|
| 28 | $type = $header['byte'] >> 4 & 7; |
|
| 29 | ||
| 30 | if( $type === 6 || $type === 7 ) { |
|
| 31 | $size = $this->decoder->readDeltaTargetSize( $handle, $type ); |
|
| 32 | } |
|
| 33 | ||
| 34 | return $size; |
|
| 35 | }, |
|
| 36 | 0 |
|
| 37 | ); |
|
| 38 | } |
|
| 39 | ||
| 40 | public function read( |
|
| 41 | FileHandlePool $pool, |
|
| 42 | string $packFile, |
|
| 43 | int $offset, |
|
| 44 | int $cap, |
|
| 45 | callable $readShaBaseFn |
|
| 46 | ): string { |
|
| 47 | return $pool->computeString( |
|
| 48 | $packFile, |
|
| 49 | function( mixed $handle ) use ( |
|
| 50 | $offset, |
|
| 51 | $cap, |
|
| 52 | $pool, |
|
| 53 | $packFile, |
|
| 54 | $readShaBaseFn |
|
| 55 | ): string { |
|
| 56 | fseek( $handle, $offset ); |
|
| 57 | ||
| 58 | $header = $this->readVarInt( $handle ); |
|
| 59 | $type = $header['byte'] >> 4 & 7; |
|
| 60 | $result = ''; |
|
| 61 | ||
| 62 | if( $type === 6 ) { |
|
| 63 | $neg = $this->readOffsetDelta( $handle ); |
|
| 64 | $cur = ftell( $handle ); |
|
| 65 | $base = $offset - $neg; |
|
| 66 | $bData = $this->read( |
|
| 67 | $pool, |
|
| 68 | $packFile, |
|
| 69 | $base, |
|
| 70 | $cap, |
|
| 71 | $readShaBaseFn |
|
| 72 | ); |
|
| 73 | ||
| 74 | fseek( $handle, $cur ); |
|
| 75 | ||
| 76 | $delta = $this->inflate( $handle ); |
|
| 77 | $result = $this->decoder->apply( $bData, $delta, $cap ); |
|
| 78 | } elseif( $type === 7 ) { |
|
| 79 | $sha = bin2hex( fread( $handle, 20 ) ); |
|
| 80 | $bas = $readShaBaseFn( $sha, $cap ); |
|
| 81 | $del = $this->inflate( $handle ); |
|
| 82 | $result = $this->decoder->apply( $bas, $del, $cap ); |
|
| 83 | } else { |
|
| 84 | $result = $this->inflate( $handle, $cap ); |
|
| 85 | } |
|
| 86 | ||
| 87 | return $result; |
|
| 88 | }, |
|
| 89 | '' |
|
| 90 | ); |
|
| 91 | } |
|
| 92 | ||
| 93 | public function streamRawCompressed( |
|
| 94 | FileHandlePool $pool, |
|
| 95 | string $packFile, |
|
| 96 | int $offset |
|
| 97 | ): Generator { |
|
| 98 | yield from $pool->streamGenerator( |
|
| 99 | $packFile, |
|
| 100 | function( mixed $handle ) use ( $offset ): Generator { |
|
| 101 | fseek( $handle, $offset ); |
|
| 102 | ||
| 103 | $header = $this->readVarInt( $handle ); |
|
| 104 | $type = $header['byte'] >> 4 & 7; |
|
| 105 | ||
| 106 | if( $type !== 6 && $type !== 7 ) { |
|
| 107 | $stream = CompressionStream::createExtractor(); |
|
| 108 | ||
| 109 | yield from $stream->stream( $handle ); |
|
| 110 | } |
|
| 111 | } |
|
| 112 | ); |
|
| 113 | } |
|
| 114 | ||
| 115 | public function streamEntryGenerator( |
|
| 116 | FileHandlePool $pool, |
|
| 117 | string $packFile, |
|
| 118 | int $offset, |
|
| 119 | int $depth, |
|
| 120 | callable $getSizeShaFn, |
|
| 121 | callable $streamShaFn |
|
| 122 | ): Generator { |
|
| 123 | yield from $pool->streamGenerator( |
|
| 124 | $packFile, |
|
| 125 | function( mixed $handle ) use ( |
|
| 126 | $pool, |
|
| 127 | $packFile, |
|
| 128 | $offset, |
|
| 129 | $depth, |
|
| 130 | $getSizeShaFn, |
|
| 131 | $streamShaFn |
|
| 132 | ): Generator { |
|
| 133 | fseek( $handle, $offset ); |
|
| 134 | ||
| 135 | $header = $this->readVarInt( $handle ); |
|
| 136 | $type = $header['byte'] >> 4 & 7; |
|
| 137 | ||
| 138 | if( $type === 6 || $type === 7 ) { |
|
| 139 | yield from $this->streamDeltaObjectGenerator( |
|
| 140 | $handle, |
|
| 141 | $pool, |
|
| 142 | $packFile, |
|
| 143 | $offset, |
|
| 144 | $type, |
|
| 145 | $depth, |
|
| 146 | $getSizeShaFn, |
|
| 147 | $streamShaFn |
|
| 148 | ); |
|
| 149 | } else { |
|
| 150 | $stream = CompressionStream::createInflater(); |
|
| 151 | ||
| 152 | yield from $stream->stream( $handle ); |
|
| 153 | } |
|
| 154 | } |
|
| 155 | ); |
|
| 156 | } |
|
| 157 | ||
| 158 | private function streamDeltaObjectGenerator( |
|
| 159 | mixed $handle, |
|
| 160 | FileHandlePool $pool, |
|
| 161 | string $packFile, |
|
| 162 | int $offset, |
|
| 163 | int $type, |
|
| 164 | int $depth, |
|
| 165 | callable $getSizeShaFn, |
|
| 166 | callable $streamShaFn |
|
| 167 | ): Generator { |
|
| 168 | if( $depth < self::MAX_DEPTH ) { |
|
| 169 | if( $type === 6 ) { |
|
| 170 | $neg = $this->readOffsetDelta( $handle ); |
|
| 171 | $deltaPos = ftell( $handle ); |
|
| 172 | $baseSize = $this->getSize( $pool, $packFile, $offset - $neg ); |
|
| 173 | ||
| 174 | if( $baseSize > self::MAX_BASE_RAM ) { |
|
| 175 | $tmpHandle = $this->resolveBaseToTempFile( |
|
| 176 | $pool, |
|
| 177 | $packFile, |
|
| 178 | $offset - $neg, |
|
| 179 | $depth, |
|
| 180 | $getSizeShaFn, |
|
| 181 | $streamShaFn |
|
| 182 | ); |
|
| 183 | ||
| 184 | if( $tmpHandle !== false ) { |
|
| 185 | fseek( $handle, $deltaPos ); |
|
| 186 | ||
| 187 | yield from $this->decoder->applyStreamGenerator( |
|
| 188 | $handle, |
|
| 189 | $tmpHandle |
|
| 190 | ); |
|
| 191 | ||
| 192 | fclose( $tmpHandle ); |
|
| 193 | } |
|
| 194 | } else { |
|
| 195 | $base = ''; |
|
| 196 | ||
| 197 | foreach( $this->streamEntryGenerator( |
|
| 198 | $pool, |
|
| 199 | $packFile, |
|
| 200 | $offset - $neg, |
|
| 201 | $depth + 1, |
|
| 202 | $getSizeShaFn, |
|
| 203 | $streamShaFn |
|
| 204 | ) as $chunk ) { |
|
| 205 | $base .= $chunk; |
|
| 206 | } |
|
| 207 | ||
| 208 | fseek( $handle, $deltaPos ); |
|
| 209 | ||
| 210 | yield from $this->decoder->applyStreamGenerator( |
|
| 211 | $handle, |
|
| 212 | $base |
|
| 213 | ); |
|
| 214 | } |
|
| 215 | } else { |
|
| 216 | $baseSha = bin2hex( fread( $handle, 20 ) ); |
|
| 217 | $baseSize = $getSizeShaFn( $baseSha ); |
|
| 218 | ||
| 219 | if( $baseSize > self::MAX_BASE_RAM ) { |
|
| 220 | $tmpHandle = tmpfile(); |
|
| 221 | ||
| 222 | if( $tmpHandle !== false ) { |
|
| 223 | $written = false; |
|
| 224 | ||
| 225 | foreach( $streamShaFn( $baseSha, $depth + 1 ) as $chunk ) { |
|
| 226 | fwrite( $tmpHandle, $chunk ); |
|
| 227 | ||
| 228 | $written = true; |
|
| 229 | } |
|
| 230 | ||
| 231 | if( $written ) { |
|
| 232 | rewind( $tmpHandle ); |
|
| 233 | ||
| 234 | yield from $this->decoder->applyStreamGenerator( |
|
| 235 | $handle, |
|
| 236 | $tmpHandle |
|
| 237 | ); |
|
| 238 | } |
|
| 239 | ||
| 240 | fclose( $tmpHandle ); |
|
| 241 | } |
|
| 242 | } else { |
|
| 243 | $base = ''; |
|
| 244 | $written = false; |
|
| 245 | ||
| 246 | foreach( $streamShaFn( $baseSha, $depth + 1 ) as $chunk ) { |
|
| 247 | $base .= $chunk; |
|
| 248 | $written = true; |
|
| 249 | } |
|
| 250 | ||
| 251 | if( $written ) { |
|
| 252 | yield from $this->decoder->applyStreamGenerator( |
|
| 253 | $handle, |
|
| 254 | $base |
|
| 255 | ); |
|
| 256 | } |
|
| 257 | } |
|
| 258 | } |
|
| 259 | } |
|
| 260 | } |
|
| 261 | ||
| 262 | private function resolveBaseToTempFile( |
|
| 263 | FileHandlePool $pool, |
|
| 264 | string $packFile, |
|
| 265 | int $baseOffset, |
|
| 266 | int $depth, |
|
| 267 | callable $getSizeShaFn, |
|
| 268 | callable $streamShaFn |
|
| 269 | ) { |
|
| 270 | $tmpHandle = tmpfile(); |
|
| 271 | ||
| 272 | if( $tmpHandle !== false ) { |
|
| 273 | foreach( $this->streamEntryGenerator( |
|
| 274 | $pool, |
|
| 275 | $packFile, |
|
| 276 | $baseOffset, |
|
| 277 | $depth + 1, |
|
| 278 | $getSizeShaFn, |
|
| 279 | $streamShaFn |
|
| 280 | ) as $chunk ) { |
|
| 281 | fwrite( $tmpHandle, $chunk ); |
|
| 282 | } |
|
| 283 | ||
| 284 | rewind( $tmpHandle ); |
|
| 285 | } |
|
| 286 | ||
| 287 | return $tmpHandle; |
|
| 288 | } |
|
| 289 | ||
| 290 | private function readVarInt( mixed $handle ): array { |
|
| 291 | $byte = ord( fread( $handle, 1 ) ); |
|
| 292 | $val = $byte & 15; |
|
| 293 | $shft = 4; |
|
| 294 | $fst = $byte; |
|
| 295 | ||
| 296 | while( $byte & 128 ) { |
|
| 297 | $byte = ord( fread( $handle, 1 ) ); |
|
| 298 | $val |= ($byte & 127) << $shft; |
|
| 299 | $shft += 7; |
|
| 300 | } |
|
| 301 | ||
| 302 | return [ 'value' => $val, 'byte' => $fst ]; |
|
| 303 | } |
|
| 304 | ||
| 305 | private function readOffsetDelta( mixed $handle ): int { |
|
| 306 | $byte = ord( fread( $handle, 1 ) ); |
|
| 307 | $neg = $byte & 127; |
|
| 308 | ||
| 309 | while( $byte & 128 ) { |
|
| 310 | $byte = ord( fread( $handle, 1 ) ); |
|
| 311 | $neg = ($neg + 1) << 7 | $byte & 127; |
|
| 312 | } |
|
| 313 | ||
| 314 | return $neg; |
|
| 315 | } |
|
| 316 | ||
| 317 | private function inflate( mixed $handle, int $cap = 0 ): string { |
|
| 318 | $stream = CompressionStream::createInflater(); |
|
| 319 | $result = ''; |
|
| 320 | ||
| 321 | foreach( $stream->stream( $handle ) as $data ) { |
|
| 322 | $result .= $data; |
|
| 323 | ||
| 324 | if( $cap > 0 && strlen( $result ) >= $cap ) { |
|
| 325 | $result = substr( $result, 0, $cap ); |
|
| 326 | ||
| 327 | break; |
|
| 328 | } |
|
| 329 | } |
|
| 330 | ||
| 331 | return $result; |
|
| 332 | } |
|
| 333 | } |
|
| 1 | 334 |
| 1 | <?php |
|
| 2 | class PackIndex { |
|
| 3 | private string $indexFile; |
|
| 4 | private string $packFile; |
|
| 5 | private array $fanoutCache; |
|
| 6 | ||
| 7 | public function __construct( string $indexFile ) { |
|
| 8 | $this->indexFile = $indexFile; |
|
| 9 | $this->packFile = str_replace( '.idx', '.pack', $indexFile ); |
|
| 10 | $this->fanoutCache = []; |
|
| 11 | } |
|
| 12 | ||
| 13 | public function search( |
|
| 14 | FileHandlePool $pool, |
|
| 15 | string $sha, |
|
| 16 | callable $onFound |
|
| 17 | ): void { |
|
| 18 | $pool->computeVoid( |
|
| 19 | $this->indexFile, |
|
| 20 | function( mixed $handle ) use ( $sha, $onFound ): void { |
|
| 21 | $this->ensureFanout( $handle ); |
|
| 22 | ||
| 23 | if( !empty( $this->fanoutCache ) ) { |
|
| 24 | $this->binarySearch( $handle, $sha, $onFound ); |
|
| 25 | } |
|
| 26 | } |
|
| 27 | ); |
|
| 28 | } |
|
| 29 | ||
| 30 | private function ensureFanout( mixed $handle ): void { |
|
| 31 | if( empty( $this->fanoutCache ) ) { |
|
| 32 | fseek( $handle, 0 ); |
|
| 33 | ||
| 34 | $head = fread( $handle, 8 ); |
|
| 35 | ||
| 36 | if( $head === "\377tOc\0\0\0\2" ) { |
|
| 37 | $this->fanoutCache = array_values( |
|
| 38 | unpack( 'N*', fread( $handle, 1024 ) ) |
|
| 39 | ); |
|
| 40 | } |
|
| 41 | } |
|
| 42 | } |
|
| 43 | ||
| 44 | private function binarySearch( |
|
| 45 | mixed $handle, |
|
| 46 | string $sha, |
|
| 47 | callable $onFound |
|
| 48 | ): void { |
|
| 49 | $byte = ord( $sha[0] ); |
|
| 50 | $start = $byte === 0 ? 0 : $this->fanoutCache[$byte - 1]; |
|
| 51 | $end = $this->fanoutCache[$byte]; |
|
| 52 | $result = 0; |
|
| 53 | ||
| 54 | if( $end > $start ) { |
|
| 55 | $low = $start; |
|
| 56 | $high = $end - 1; |
|
| 57 | ||
| 58 | while( $result === 0 && $low <= $high ) { |
|
| 59 | $mid = ($low + $high) >> 1; |
|
| 60 | ||
| 61 | fseek( $handle, 1032 + $mid * 20 ); |
|
| 62 | ||
| 63 | $cmp = fread( $handle, 20 ); |
|
| 64 | ||
| 65 | if( $cmp < $sha ) { |
|
| 66 | $low = $mid + 1; |
|
| 67 | } elseif( $cmp > $sha ) { |
|
| 68 | $high = $mid - 1; |
|
| 69 | } else { |
|
| 70 | $result = $this->readOffset( $handle, $mid ); |
|
| 71 | } |
|
| 72 | } |
|
| 73 | } |
|
| 74 | ||
| 75 | if( $result !== 0 ) { |
|
| 76 | $onFound( $this->packFile, $result ); |
|
| 77 | } |
|
| 78 | } |
|
| 79 | ||
| 80 | private function readOffset( mixed $handle, int $mid ): int { |
|
| 81 | $total = $this->fanoutCache[255]; |
|
| 82 | $pos = 1032 + $total * 24 + $mid * 4; |
|
| 83 | $result = 0; |
|
| 84 | ||
| 85 | fseek( $handle, $pos ); |
|
| 86 | ||
| 87 | $packed = fread( $handle, 4 ); |
|
| 88 | $offset = unpack( 'N', $packed )[1]; |
|
| 89 | ||
| 90 | if( $offset & 0x80000000 ) { |
|
| 91 | $pos64 = 1032 + $total * 28 + ($offset & 0x7FFFFFFF) * 8; |
|
| 92 | ||
| 93 | fseek( $handle, $pos64 ); |
|
| 94 | ||
| 95 | $offset = unpack( 'J', fread( $handle, 8 ) )[1]; |
|
| 96 | } |
|
| 97 | ||
| 98 | $result = (int)$offset; |
|
| 99 | ||
| 100 | return $result; |
|
| 101 | } |
|
| 102 | } |
|
| 1 | 103 |
| 1 | <?php |
|
| 2 | require_once __DIR__ . '/PackIndex.php'; |
|
| 3 | require_once __DIR__ . '/FileHandlePool.php'; |
|
| 4 | ||
| 5 | class PackLocator { |
|
| 6 | private array $indexes; |
|
| 7 | ||
| 8 | public function __construct( string $objectsPath ) { |
|
| 9 | $this->indexes = []; |
|
| 10 | $packFiles = glob( "{$objectsPath}/pack/*.idx" ) ?: []; |
|
| 11 | ||
| 12 | foreach( $packFiles as $indexFile ) { |
|
| 13 | $this->indexes[] = new PackIndex( $indexFile ); |
|
| 14 | } |
|
| 15 | } |
|
| 16 | ||
| 17 | public function locate( |
|
| 18 | FileHandlePool $pool, |
|
| 19 | string $sha, |
|
| 20 | callable $action |
|
| 21 | ): void { |
|
| 22 | if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) { |
|
| 23 | $binarySha = hex2bin( $sha ); |
|
| 24 | $found = false; |
|
| 25 | $count = count( $this->indexes ); |
|
| 26 | $index = 0; |
|
| 27 | ||
| 28 | while( !$found && $index < $count ) { |
|
| 29 | $this->indexes[$index]->search( |
|
| 30 | $pool, |
|
| 31 | $binarySha, |
|
| 32 | function( |
|
| 33 | string $packFile, |
|
| 34 | int $offset |
|
| 35 | ) use ( |
|
| 36 | &$found, |
|
| 37 | $index, |
|
| 38 | $action |
|
| 39 | ): void { |
|
| 40 | $found = true; |
|
| 41 | ||
| 42 | if( $index > 0 ) { |
|
| 43 | $temp = $this->indexes[0]; |
|
| 44 | $this->indexes[0] = $this->indexes[$index]; |
|
| 45 | $this->indexes[$index] = $temp; |
|
| 46 | } |
|
| 47 | ||
| 48 | $action( $packFile, $offset ); |
|
| 49 | } |
|
| 50 | ); |
|
| 51 | ||
| 52 | $index++; |
|
| 53 | } |
|
| 54 | } |
|
| 55 | } |
|
| 56 | } |
|
| 1 | 57 |
| 43 | 43 | |
| 44 | 44 | <nav class="nav"> |
| 45 | <a href="<?php echo (new UrlBuilder())->build(); ?>">Home</a> |
|
| 46 | 45 | <?php if( $currentRepo ) { ?> |
| 46 | <a href="<?php echo (new UrlBuilder())->build(); ?>">Home</a> |
|
| 47 | 47 | <?php $safeName = $currentRepo['safe_name']; ?> |
| 48 | 48 | <a href="<?php echo (new UrlBuilder()) |