Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
M File.php
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
  }
M git/CompressionStream.php
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
A git/DeltaDecoder.php
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
A git/FileHandlePool.php
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
M git/Git.php
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
M git/GitPacks.php
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
}
A git/PackEntryReader.php
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
A git/PackIndex.php
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
A git/PackLocator.php
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
M pages/BasePage.php
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())