Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
D .htaccess
1
RewriteEngine On
2
3
RewriteCond %{HTTP_USER_AGENT} SemrushBot [NC]
4
RewriteRule .* - [F]
5
6
RewriteRule ^(git|pages|render)/ - [F]
7
8
RewriteCond %{REQUEST_URI} !^/index\.php$
9
RewriteCond %{REQUEST_FILENAME} -f
10
RewriteRule \.php$ - [F]
11
12
RewriteCond %{REQUEST_FILENAME} !-f
13
RewriteRule ^(.*)$ index.php [QSA,L]
14
15
deny from 185.213.83.81
16
17 1
M Config.php
9 9
    ini_set( 'log_errors', 1 );
10 10
    ini_set( 'error_log', __DIR__ . self::LOG_FILE );
11
12
    set_time_limit( 0 );
11 13
  }
12 14
A INSTALL.md
1
# Requirements
2
3
TreeTrek was built and tested using the following software components:
4
5
* Debian Linux 13 (kernel 6.12.69)
6
* nginx 1.26.3
7
* PHP 8.4.16
8
* Git 2.53.0
9
10
# Installation
11
12
To install:
13
14
* Download code
15
* Configure settings
16
* Configure web server routing and security
17
* Define display order
18
19
Replace placeholder values with the configuration details.
20
21
1. Set placeholders:
22
   ``` bash
23
   export REPO=repo.domain.com
24
   export OWNER=username
25
   export WEBDIR=/var/www
26
   ```
27
1. Download code:
28
   ```bash
29
   mkdir -p /var/www/${REPO}
30
   cd /var/www/${REPO}
31
   git clone https://repo.autonoma.ca/repo/treetrek
32
   sudo usermod -aG ${OWNER} www-data
33
   ```
34
1. Edit `${WEBDIR}/${REPO}/Config.php`.
35
1. Set `SITE_TITLE`.
36
1. Set `REPOS_SUBDIR`.
37
1. Save the file.
38
1. Edit `${WEBDIR}/${REPO}/order.txt`
39
1. Add repositories you want to come first, hide using a hyphen prefix.
40
1. Edit nginx configuration file.
41
1. Add routing and security rules:
42
   ```nginx
43
   location ^~ /images/ {
44
     allow all;
45
   }
46
47
   location ~* ^/images/.*\.svg$ {
48
     add_header Content-Type image/svg+xml;
49
   }
50
51
   location ~ ^/(git|pages|render)/ {
52
     deny  all;
53
     return 403;
54
   }
55
56
   location ^~ /repo/ {
57
     try_files $uri $uri/ /index.php?$query_string;
58
   }
59
60
   location / {
61
     try_files $uri $uri/ /index.php?$query_string;
62
   }
63
64
   location = /index.php {
65
     include      snippets/fastcgi-php.conf;
66
     fastcgi_pass unix:/run/php/php8.4-fpm.sock;
67
   }
68
69
   location ~ \.php$ {
70
     deny   all;
71
     return 403;
72
   }
73
   ```
74
1. Apply changes:
75
   ```bash
76
   sudo nginx -t && sudo systemctl reload nginx
77
   ```
78
79
The application is installed.
80
1 81
M git/Git.php
189 189
190 190
        if( $real && strpos( $real, $repo ) === 0 ) {
191
          $result = readfile( $path ) !== false;
192
        }
193
      }
194
    }
195
196
    return $result;
197
  }
198
199
  public function eachRef( callable $callback ): void {
200
    $head = $this->resolve( 'HEAD' );
201
202
    if( $head !== '' ) {
203
      $callback( 'HEAD', $head );
204
    }
205
206
    $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) {
207
      $callback( "refs/heads/$n", $s );
208
    } );
209
210
    $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) {
211
      $callback( "refs/tags/$n", $s );
212
    } );
213
  }
214
215
  public function generatePackfile( array $objs ): string {
216
    $pData = "PACK" . pack( 'N', 2 ) . pack( 'N', 0 );
217
218
    if( !empty( $objs ) ) {
219
      $data = '';
220
221
      foreach( $objs as $sha => $info ) {
222
        $cont  = $this->read( $sha );
223
        $size  = strlen( $cont );
224
        $byte  = $info['type'] << 4 | $size & 0x0f;
225
        $size >>= 4;
226
227
        while( $size > 0 ) {
228
          $data .= chr( $byte | 0x80 );
229
          $byte  = $size & 0x7f;
230
          $size >>= 7;
231
        }
232
233
        $data .= chr( $byte ) . gzcompress( $cont );
234
      }
235
236
      $pData = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) ) . $data;
237
    }
238
239
    return $pData . hash( 'sha1', $pData, true );
240
  }
241
242
  private function getTreeSha( string $commitOrTreeSha ): string {
243
    $data = $this->read( $commitOrTreeSha );
244
    $sha  = $commitOrTreeSha;
245
246
    if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
247
      $sha = $this->getTreeSha( $matches[1] );
248
    }
249
250
    if( $sha === $commitOrTreeSha &&
251
        preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
252
      $sha = $matches[1];
253
    }
254
255
    return $sha;
256
  }
257
258
  private function resolvePath( string $treeSha, string $path ): array {
259
    $parts = explode( '/', trim( $path, '/' ) );
260
    $sha   = $treeSha;
261
    $mode  = '40000';
262
263
    foreach( $parts as $part ) {
264
      $entry = [ 'sha' => '', 'mode' => '' ];
265
266
      if( $part !== '' && $sha !== '' ) {
267
        $entry = $this->findTreeEntry( $sha, $part );
268
      }
269
270
      $sha   = $entry['sha'];
271
      $mode  = $entry['mode'];
272
    }
273
274
    return [
275
      'sha'   => $sha,
276
      'mode'  => $mode,
277
      'isDir' => $mode === '40000' || $mode === '040000'
278
    ];
279
  }
280
281
  private function findTreeEntry( string $treeSha, string $name ): array {
282
    $data  = $this->read( $treeSha );
283
    $pos   = 0;
284
    $len   = strlen( $data );
285
    $entry = [ 'sha' => '', 'mode' => '' ];
286
287
    while( $pos < $len ) {
288
      $space = strpos( $data, ' ', $pos );
289
      $eos   = strpos( $data, "\0", $space );
290
291
      if( $space === false || $eos === false ) {
292
        break;
293
      }
294
295
      if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) {
296
        $entry = [
297
          'sha'  => bin2hex( substr( $data, $eos + 1, 20 ) ),
298
          'mode' => substr( $data, $pos, $space - $pos )
299
        ];
300
        break;
301
      }
302
303
      $pos = $eos + 21;
304
    }
305
306
    return $entry;
307
  }
308
309
  private function parseTagData(
310
    string $name,
311
    string $sha,
312
    string $data
313
  ): Tag {
314
    $isAnn   = strncmp( $data, 'object ', 7 ) === 0;
315
    $pattern = $isAnn
316
      ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
317
      : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
318
    $id      = $this->parseIdentity( $data, $pattern );
319
    $target  = $isAnn
320
      ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha )
321
      : $sha;
322
323
    return new Tag(
324
      $name,
325
      $sha,
326
      $target,
327
      $id['timestamp'],
328
      $this->extractMessage( $data ),
329
      $id['name']
330
    );
331
  }
332
333
  private function extractPattern(
334
    string $data,
335
    string $pattern,
336
    int $group,
337
    string $default = ''
338
  ): string {
339
    return preg_match( $pattern, $data, $matches )
340
      ? $matches[$group]
341
      : $default;
342
  }
343
344
  private function parseIdentity( string $data, string $pattern ): array {
345
    $found = preg_match( $pattern, $data, $matches );
346
347
    return [
348
      'name'      => $found ? trim( $matches[1] ) : 'Unknown',
349
      'email'     => $found ? $matches[2] : '',
350
      'timestamp' => $found ? (int)$matches[3] : 0
351
    ];
352
  }
353
354
  private function extractMessage( string $data ): string {
355
    $pos = strpos( $data, "\n\n" );
356
357
    return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : '';
358
  }
359
360
  private function slurp( string $sha, callable $callback ): void {
361
    $path = $this->getLoosePath( $sha );
362
363
    if( is_file( $path ) ) {
364
      $this->slurpLooseObject( $path, $callback );
365
    } else {
366
      $this->slurpPackedObject( $sha, $callback );
367
    }
368
  }
369
370
  private function slurpLooseObject( string $path, callable $callback ): void {
371
    $this->iterateInflated(
372
      $path,
373
      function( $chunk ) use ( $callback ) {
374
        if( $chunk !== '' ) {
375
          $callback( $chunk );
376
        }
377
        return true;
378
      }
379
    );
380
  }
381
382
  private function slurpPackedObject( string $sha, callable $callback ): void {
383
    $streamed = $this->packs->stream( $sha, $callback );
384
385
    if( !$streamed ) {
386
      $data = $this->packs->read( $sha );
387
388
      if( $data !== '' ) {
389
        $callback( $data );
390
      }
391
    }
392
  }
393
394
  private function iterateInflated(
395
    string $path,
396
    callable $processor
397
  ): void {
398
    $handle = fopen( $path, 'rb' );
399
    $infl   = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
400
    $found  = false;
401
    $buffer = '';
402
403
    if( $handle && $infl ) {
404
      while( !feof( $handle ) ) {
405
        $chunk    = fread( $handle, 16384 );
406
        $inflated = inflate_add( $infl, $chunk );
407
408
        if( $inflated === false ) {
409
          break;
410
        }
411
412
        if( !$found ) {
413
          $buffer .= $inflated;
414
          $eos     = strpos( $buffer, "\0" );
415
416
          if( $eos !== false ) {
417
            $found = true;
418
            $body  = substr( $buffer, $eos + 1 );
419
            $head  = substr( $buffer, 0, $eos );
420
421
            if( $processor( $body, $head ) === false ) {
422
              break;
423
            }
424
          }
425
        } elseif( $processor( $inflated, null ) === false ) {
426
          break;
427
        }
428
      }
429
430
      fclose( $handle );
431
    }
432
  }
433
434
  private function peekLooseObject( string $sha, int $length ): string {
435
    $path = $this->getLoosePath( $sha );
436
    $buf  = '';
437
438
    if( is_file( $path ) ) {
439
      $this->iterateInflated(
440
        $path,
441
        function( $chunk ) use ( $length, &$buf ) {
442
          $buf .= $chunk;
443
          return strlen( $buf ) < $length;
444
        }
445
      );
446
    }
447
448
    return substr( $buf, 0, $length );
449
  }
450
451
  private function parseCommit( string $sha ): object {
452
    $data   = $this->read( $sha );
453
    $result = (object)[ 'sha' => '' ];
454
455
    if( $data !== '' ) {
456
      $id = $this->parseIdentity(
457
        $data,
458
        '/^author (.*) <(.*)> (\d+)/m'
459
      );
460
461
      $result = (object)[
462
        'sha'       => $sha,
463
        'message'   => $this->extractMessage( $data ),
464
        'author'    => $id['name'],
465
        'email'     => $id['email'],
466
        'date'      => $id['timestamp'],
467
        'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 )
468
      ];
469
    }
470
471
    return $result;
472
  }
473
474
  private function walkTree( string $sha, callable $callback ): void {
475
    $data = $this->read( $sha );
476
    $tree = $data;
477
478
    if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) {
479
      $tree = $this->read( $m[1] );
480
    }
481
482
    if( $tree !== '' && $this->isTreeData( $tree ) ) {
483
      $this->processTree( $tree, $callback );
484
    }
485
  }
486
487
  private function processTree( string $data, callable $callback ): void {
488
    $pos = 0;
489
    $len = strlen( $data );
490
491
    while( $pos < $len ) {
492
      $space = strpos( $data, ' ', $pos );
493
      $eos   = strpos( $data, "\0", $space );
494
      $entry = null;
495
496
      if( $space !== false && $eos !== false && $eos + 21 <= $len ) {
497
        $mode = substr( $data, $pos, $space - $pos );
498
        $sha  = bin2hex( substr( $data, $eos + 1, 20 ) );
499
        $isD  = $mode === '40000' || $mode === '040000';
500
501
        $entry = [
502
          'file' => new File(
503
            substr( $data, $space + 1, $eos - $space - 1 ),
504
            $sha,
505
            $mode,
506
            0,
507
            $isD ? 0 : $this->getObjectSize( $sha ),
508
            $isD ? '' : $this->peek( $sha )
509
          ),
510
          'nextPosition' => $eos + 21
511
        ];
512
      }
513
514
      if( $entry === null ) {
515
        break;
516
      }
517
518
      $callback( $entry['file'] );
519
      $pos = $entry['nextPosition'];
520
    }
521
  }
522
523
  private function isTreeData( string $data ): bool {
524
    $len   = strlen( $data );
525
    $patt  = '/^(40000|100644|100755|120000|160000) /';
526
    $match = $len >= 25 && preg_match( $patt, $data );
527
    $eos   = $match ? strpos( $data, "\0" ) : false;
528
529
    return $match && $eos !== false && $eos + 21 <= $len;
530
  }
531
532
  private function getLoosePath( string $sha ): string {
533
    return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" .
534
      substr( $sha, 2 );
535
  }
536
537
  private function getLooseObjectSize( string $sha ): int {
538
    $path = $this->getLoosePath( $sha );
539
    $size = 0;
540
541
    if( is_file( $path ) ) {
542
      $this->iterateInflated(
543
        $path,
544
        function( $c, $head ) use ( &$size ) {
545
          if( $head !== null ) {
546
            $parts = explode( ' ', $head );
547
            $size  = isset( $parts[1] ) ? (int)$parts[1] : 0;
548
          }
549
          return false;
550
        }
551
      );
552
    }
553
554
    return $size;
555
  }
556
557
  public function collectObjects( array $wants, array $haves = [] ): array {
558
    $objs   = [];
559
    $result = [];
560
561
    foreach( $wants as $sha ) {
562
      $objs = $this->collectObjectsRecursive( $sha, $objs, 0 );
563
    }
564
565
    foreach( $haves as $sha ) {
566
      if( isset( $objs[$sha] ) ) {
567
        unset( $objs[$sha] );
568
      }
569
    }
570
571
    $result = $objs;
572
573
    return $result;
574
  }
575
576
  private function collectObjectsRecursive(
577
    string $sha,
578
    array $objs,
579
    int $expectedType = 0
580
  ): array {
581
    $result = $objs;
582
583
    if( !isset( $result[$sha] ) ) {
584
      $data = $this->read( $sha );
585
      $type = $expectedType === 0
586
        ? $this->getObjectType( $data )
587
        : $expectedType;
588
589
      $result[$sha] = [
590
        'type' => $type,
591
        'size' => strlen( $data )
592
      ];
593
594
      if( $type === 1 ) {
595
        $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m );
596
597
        if( $hasTree ) {
598
          $result = $this->collectObjectsRecursive( $m[1], $result, 2 );
599
        }
600
601
        $hasParents = preg_match_all(
602
          '/^parent ([0-9a-f]{40})/m',
603
          $data,
604
          $m
605
        );
606
607
        if( $hasParents ) {
608
          foreach( $m[1] as $parentSha ) {
609
            $result = $this->collectObjectsRecursive(
610
              $parentSha,
611
              $result,
612
              1
613
            );
614
          }
615
        }
616
      }
617
618
      if( $type === 2 ) {
619
        $pos = 0;
620
        $len = strlen( $data );
621
622
        while( $pos < $len ) {
623
          $sp  = strpos( $data, ' ', $pos );
624
          $eos = strpos( $data, "\0", $sp );
625
626
          if( $sp === false || $eos === false ) {
627
            break;
628
          }
629
630
          $mode     = substr( $data, $pos, $sp - $pos );
631
          $s        = bin2hex( substr( $data, $eos + 1, 20 ) );
632
          $isDir    = $mode === '40000' || $mode === '040000';
633
          $nextType = $isDir ? 2 : 3;
634
          $pos      = $eos + 21;
635
636
          $result = $this->collectObjectsRecursive(
637
            $s,
638
            $result,
639
            $nextType
640
          );
641
        }
642
      }
643
644
      $isTagTarget = $type === 4 &&
645
        preg_match( '/^object ([0-9a-f]{40})/m', $data, $m );
646
647
      if( $isTagTarget ) {
648
        $nextType = 1;
649
650
        if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) {
651
          $map = [
652
            'commit' => 1,
653
            'tree'   => 2,
654
            'blob'   => 3,
655
            'tag'    => 4
656
          ];
657
658
          $nextType = $map[$t[1]] ?? 1;
659
        }
660
661
        $result = $this->collectObjectsRecursive(
662
          $m[1],
663
          $result,
664
          $nextType
665
        );
666
      }
667
    }
668
669
    return $result;
191
          $result = $this->streamFileContent( $path );
192
        }
193
      }
194
    }
195
196
    return $result;
197
  }
198
199
  private function streamFileContent( string $path ): bool {
200
    $result = false;
201
202
    if( $path !== '' ) {
203
      header( 'X-Accel-Redirect: ' . $path );
204
      header( 'Content-Type: application/octet-stream' );
205
206
      $result = true;
207
    }
208
209
    return $result;
210
  }
211
212
  public function eachRef( callable $callback ): void {
213
    $head = $this->resolve( 'HEAD' );
214
215
    if( $head !== '' ) {
216
      $callback( 'HEAD', $head );
217
    }
218
219
    $this->refs->scanRefs( 'refs/heads', function( $n, $s ) use ( $callback ) {
220
      $callback( "refs/heads/$n", $s );
221
    } );
222
223
    $this->refs->scanRefs( 'refs/tags', function( $n, $s ) use ( $callback ) {
224
      $callback( "refs/tags/$n", $s );
225
    } );
226
  }
227
228
  public function generatePackfile( array $objs ): Generator {
229
    $ctx  = hash_init( 'sha1' );
230
    $head = "PACK" . pack( 'N', 2 ) . pack( 'N', count( $objs ) );
231
232
    hash_update( $ctx, $head );
233
    yield $head;
234
235
    foreach( $objs as $sha => $type ) {
236
      $size = $this->getObjectSize( $sha );
237
      $byte = $type << 4 | $size & 0x0f;
238
      $sz   = $size >> 4;
239
      $hdr  = '';
240
241
      while( $sz > 0 ) {
242
        $hdr .= chr( $byte | 0x80 );
243
        $byte = $sz & 0x7f;
244
        $sz >>= 7;
245
      }
246
247
      $hdr .= chr( $byte );
248
      hash_update( $ctx, $hdr );
249
      yield $hdr;
250
251
      $deflate = deflate_init( ZLIB_ENCODING_DEFLATE );
252
253
      foreach( $this->slurpChunks( $sha ) as $raw ) {
254
        $compressed = deflate_add( $deflate, $raw, ZLIB_NO_FLUSH );
255
256
        if( $compressed !== '' ) {
257
          hash_update( $ctx, $compressed );
258
          yield $compressed;
259
        }
260
      }
261
262
      $final = deflate_add( $deflate, '', ZLIB_FINISH );
263
264
      if( $final !== '' ) {
265
        hash_update( $ctx, $final );
266
        yield $final;
267
      }
268
    }
269
270
    yield hash_final( $ctx, true );
271
  }
272
273
  private function slurpChunks( string $sha ): Generator {
274
    $path = $this->getLoosePath( $sha );
275
276
    if( is_file( $path ) ) {
277
      yield from $this->looseObjectChunks( $path );
278
    } else {
279
      $any = false;
280
281
      foreach( $this->packs->streamGenerator( $sha ) as $chunk ) {
282
        $any = true;
283
        yield $chunk;
284
      }
285
286
      if( !$any ) {
287
        $data = $this->packs->read( $sha );
288
289
        if( $data !== '' ) {
290
          yield $data;
291
        }
292
      }
293
    }
294
  }
295
296
  private function looseObjectChunks( string $path ): Generator {
297
    $handle = fopen( $path, 'rb' );
298
    $infl   = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
299
300
    if( !$handle || !$infl ) {
301
      return;
302
    }
303
304
    $found  = false;
305
    $buffer = '';
306
307
    while( !feof( $handle ) ) {
308
      $chunk    = fread( $handle, 16384 );
309
      $inflated = inflate_add( $infl, $chunk );
310
311
      if( $inflated === false ) {
312
        break;
313
      }
314
315
      if( !$found ) {
316
        $buffer .= $inflated;
317
        $eos     = strpos( $buffer, "\0" );
318
319
        if( $eos !== false ) {
320
          $found = true;
321
          $body  = substr( $buffer, $eos + 1 );
322
323
          if( $body !== '' ) {
324
            yield $body;
325
          }
326
327
          $buffer = '';
328
        }
329
      } elseif( $inflated !== '' ) {
330
        yield $inflated;
331
      }
332
    }
333
334
    fclose( $handle );
335
  }
336
337
  private function streamCompressedObject( string $sha, $ctx ): Generator {
338
    $deflate = deflate_init( ZLIB_ENCODING_DEFLATE );
339
    $buffer  = '';
340
341
    $this->slurp( $sha, function( $chunk ) use (
342
      $deflate,
343
      $ctx,
344
      &$buffer
345
    ) {
346
      $compressed = deflate_add( $deflate, $chunk, ZLIB_NO_FLUSH );
347
348
      if( $compressed !== '' ) {
349
        hash_update( $ctx, $compressed );
350
        $buffer .= $compressed;
351
      }
352
    } );
353
354
    $final = deflate_add( $deflate, '', ZLIB_FINISH );
355
356
    if( $final !== '' ) {
357
      hash_update( $ctx, $final );
358
      $buffer .= $final;
359
    }
360
361
    $pos = 0;
362
    $len = strlen( $buffer );
363
364
    while( $pos < $len ) {
365
      $chunk = substr( $buffer, $pos, 32768 );
366
      yield $chunk;
367
      $pos  += 32768;
368
    }
369
  }
370
371
  private function getTreeSha( string $commitOrTreeSha ): string {
372
    $data = $this->read( $commitOrTreeSha );
373
    $sha  = $commitOrTreeSha;
374
375
    if( preg_match( '/^object ([0-9a-f]{40})/m', $data, $matches ) ) {
376
      $sha = $this->getTreeSha( $matches[1] );
377
    }
378
379
    if( $sha === $commitOrTreeSha &&
380
        preg_match( '/^tree ([0-9a-f]{40})/m', $data, $matches ) ) {
381
      $sha = $matches[1];
382
    }
383
384
    return $sha;
385
  }
386
387
  private function resolvePath( string $treeSha, string $path ): array {
388
    $parts = explode( '/', trim( $path, '/' ) );
389
    $sha   = $treeSha;
390
    $mode  = '40000';
391
392
    foreach( $parts as $part ) {
393
      $entry = [ 'sha' => '', 'mode' => '' ];
394
395
      if( $part !== '' && $sha !== '' ) {
396
        $entry = $this->findTreeEntry( $sha, $part );
397
      }
398
399
      $sha   = $entry['sha'];
400
      $mode  = $entry['mode'];
401
    }
402
403
    return [
404
      'sha'   => $sha,
405
      'mode'  => $mode,
406
      'isDir' => $mode === '40000' || $mode === '040000'
407
    ];
408
  }
409
410
  private function findTreeEntry( string $treeSha, string $name ): array {
411
    $data  = $this->read( $treeSha );
412
    $pos   = 0;
413
    $len   = strlen( $data );
414
    $entry = [ 'sha' => '', 'mode' => '' ];
415
416
    while( $pos < $len ) {
417
      $space = strpos( $data, ' ', $pos );
418
      $eos   = strpos( $data, "\0", $space );
419
420
      if( $space === false || $eos === false ) {
421
        break;
422
      }
423
424
      if( substr( $data, $space + 1, $eos - $space - 1 ) === $name ) {
425
        $entry = [
426
          'sha'  => bin2hex( substr( $data, $eos + 1, 20 ) ),
427
          'mode' => substr( $data, $pos, $space - $pos )
428
        ];
429
        break;
430
      }
431
432
      $pos = $eos + 21;
433
    }
434
435
    return $entry;
436
  }
437
438
  private function parseTagData(
439
    string $name,
440
    string $sha,
441
    string $data
442
  ): Tag {
443
    $isAnn   = strncmp( $data, 'object ', 7 ) === 0;
444
    $pattern = $isAnn
445
      ? '/^tagger (.*) <(.*)> (\d+) [+\-]\d{4}$/m'
446
      : '/^author (.*) <(.*)> (\d+) [+\-]\d{4}$/m';
447
    $id      = $this->parseIdentity( $data, $pattern );
448
    $target  = $isAnn
449
      ? $this->extractPattern( $data, '/^object (.*)$/m', 1, $sha )
450
      : $sha;
451
452
    return new Tag(
453
      $name,
454
      $sha,
455
      $target,
456
      $id['timestamp'],
457
      $this->extractMessage( $data ),
458
      $id['name']
459
    );
460
  }
461
462
  private function extractPattern(
463
    string $data,
464
    string $pattern,
465
    int $group,
466
    string $default = ''
467
  ): string {
468
    return preg_match( $pattern, $data, $matches )
469
      ? $matches[$group]
470
      : $default;
471
  }
472
473
  private function parseIdentity( string $data, string $pattern ): array {
474
    $found = preg_match( $pattern, $data, $matches );
475
476
    return [
477
      'name'      => $found ? trim( $matches[1] ) : 'Unknown',
478
      'email'     => $found ? $matches[2] : '',
479
      'timestamp' => $found ? (int)$matches[3] : 0
480
    ];
481
  }
482
483
  private function extractMessage( string $data ): string {
484
    $pos = strpos( $data, "\n\n" );
485
486
    return $pos !== false ? trim( substr( $data, $pos + 2 ) ) : '';
487
  }
488
489
  private function slurp( string $sha, callable $callback ): void {
490
    $path = $this->getLoosePath( $sha );
491
492
    if( is_file( $path ) ) {
493
      $this->slurpLooseObject( $path, $callback );
494
    } else {
495
      $this->slurpPackedObject( $sha, $callback );
496
    }
497
  }
498
499
  private function slurpLooseObject( string $path, callable $callback ): void {
500
    $this->iterateInflated(
501
      $path,
502
      function( $chunk ) use ( $callback ) {
503
        if( $chunk !== '' ) {
504
          $callback( $chunk );
505
        }
506
        return true;
507
      }
508
    );
509
  }
510
511
  private function slurpPackedObject( string $sha, callable $callback ): void {
512
    $streamed = $this->packs->stream( $sha, $callback );
513
514
    if( !$streamed ) {
515
      $data = $this->packs->read( $sha );
516
517
      if( $data !== '' ) {
518
        $callback( $data );
519
      }
520
    }
521
  }
522
523
  private function iterateInflated(
524
    string $path,
525
    callable $processor
526
  ): void {
527
    $handle = fopen( $path, 'rb' );
528
    $infl   = $handle ? inflate_init( ZLIB_ENCODING_DEFLATE ) : null;
529
    $found  = false;
530
    $buffer = '';
531
532
    if( $handle && $infl ) {
533
      while( !feof( $handle ) ) {
534
        $chunk    = fread( $handle, 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, null ) === false ) {
555
          break;
556
        }
557
      }
558
559
      fclose( $handle );
560
    }
561
  }
562
563
  private function peekLooseObject( string $sha, int $length ): string {
564
    $path = $this->getLoosePath( $sha );
565
    $buf  = '';
566
567
    if( is_file( $path ) ) {
568
      $this->iterateInflated(
569
        $path,
570
        function( $chunk ) use ( $length, &$buf ) {
571
          $buf .= $chunk;
572
          return strlen( $buf ) < $length;
573
        }
574
      );
575
    }
576
577
    return substr( $buf, 0, $length );
578
  }
579
580
  private function parseCommit( string $sha ): object {
581
    $data   = $this->read( $sha );
582
    $result = (object)[ 'sha' => '' ];
583
584
    if( $data !== '' ) {
585
      $id = $this->parseIdentity(
586
        $data,
587
        '/^author (.*) <(.*)> (\d+)/m'
588
      );
589
590
      $result = (object)[
591
        'sha'       => $sha,
592
        'message'   => $this->extractMessage( $data ),
593
        'author'    => $id['name'],
594
        'email'     => $id['email'],
595
        'date'      => $id['timestamp'],
596
        'parentSha' => $this->extractPattern( $data, '/^parent (.*)$/m', 1 )
597
      ];
598
    }
599
600
    return $result;
601
  }
602
603
  private function walkTree( string $sha, callable $callback ): void {
604
    $data = $this->read( $sha );
605
    $tree = $data;
606
607
    if( $data !== '' && preg_match( '/^tree (.*)$/m', $data, $m ) ) {
608
      $tree = $this->read( $m[1] );
609
    }
610
611
    if( $tree !== '' && $this->isTreeData( $tree ) ) {
612
      $this->processTree( $tree, $callback );
613
    }
614
  }
615
616
  private function processTree( string $data, callable $callback ): void {
617
    $pos = 0;
618
    $len = strlen( $data );
619
620
    while( $pos < $len ) {
621
      $space = strpos( $data, ' ', $pos );
622
      $eos   = strpos( $data, "\0", $space );
623
      $entry = null;
624
625
      if( $space !== false && $eos !== false && $eos + 21 <= $len ) {
626
        $mode  = substr( $data, $pos, $space - $pos );
627
        $sha   = bin2hex( substr( $data, $eos + 1, 20 ) );
628
        $dir   = $mode === '40000' || $mode === '040000';
629
        $isSub = $mode === '160000';
630
631
        $entry = [
632
          'file' => new File(
633
            substr( $data, $space + 1, $eos - $space - 1 ),
634
            $sha,
635
            $mode,
636
            0,
637
            $dir || $isSub ? 0 : $this->getObjectSize( $sha ),
638
            $dir || $isSub ? '' : $this->peek( $sha )
639
          ),
640
          'nextPosition' => $eos + 21
641
        ];
642
      }
643
644
      if( $entry === null ) {
645
        break;
646
      }
647
648
      $callback( $entry['file'] );
649
      $pos = $entry['nextPosition'];
650
    }
651
  }
652
653
  private function isTreeData( string $data ): bool {
654
    $len   = strlen( $data );
655
    $patt  = '/^(40000|100644|100755|120000|160000) /';
656
    $match = $len >= 25 && preg_match( $patt, $data );
657
    $eos   = $match ? strpos( $data, "\0" ) : false;
658
659
    return $match && $eos !== false && $eos + 21 <= $len;
660
  }
661
662
  private function getLoosePath( string $sha ): string {
663
    return "{$this->objPath}/" . substr( $sha, 0, 2 ) . "/" .
664
      substr( $sha, 2 );
665
  }
666
667
  private function getLooseObjectSize( string $sha ): int {
668
    $path = $this->getLoosePath( $sha );
669
    $size = 0;
670
671
    if( is_file( $path ) ) {
672
      $this->iterateInflated(
673
        $path,
674
        function( $c, $head ) use ( &$size ) {
675
          if( $head !== null ) {
676
            $parts = explode( ' ', $head );
677
            $size  = isset( $parts[1] ) ? (int)$parts[1] : 0;
678
          }
679
          return false;
680
        }
681
      );
682
    }
683
684
    return $size;
685
  }
686
687
  public function collectObjects( array $wants, array $haves = [] ): array {
688
    $objs   = $this->traverseObjects( $wants );
689
    $result = [];
690
691
    if( !empty( $haves ) ) {
692
      $haveObjs = $this->traverseObjects( $haves );
693
694
      foreach( $haveObjs as $sha => $type ) {
695
        if( isset( $objs[$sha] ) ) {
696
          unset( $objs[$sha] );
697
        }
698
      }
699
    }
700
701
    $result = $objs;
702
703
    return $result;
704
  }
705
706
  private function traverseObjects( array $roots ): array {
707
    $objs  = [];
708
    $queue = [];
709
710
    foreach( $roots as $sha ) {
711
      $queue[] = [ 'sha' => $sha, 'type' => 0 ];
712
    }
713
714
    while( !empty( $queue ) ) {
715
      $item = array_pop( $queue );
716
      $sha  = $item['sha'];
717
      $type = $item['type'];
718
719
      if( isset( $objs[$sha] ) ) {
720
        continue;
721
      }
722
723
      $data = '';
724
725
      if( $type !== 3 ) {
726
        $data = $this->read( $sha );
727
728
        if( $type === 0 ) {
729
          $type = $this->getObjectType( $data );
730
        }
731
      }
732
733
      $objs[$sha] = $type;
734
735
      if( $type === 1 ) {
736
        $hasTree = preg_match( '/^tree ([0-9a-f]{40})/m', $data, $m );
737
738
        if( $hasTree ) {
739
          $queue[] = [ 'sha' => $m[1], 'type' => 2 ];
740
        }
741
742
        $hasParents = preg_match_all(
743
          '/^parent ([0-9a-f]{40})/m',
744
          $data,
745
          $m
746
        );
747
748
        if( $hasParents ) {
749
          foreach( $m[1] as $parentSha ) {
750
            $queue[] = [ 'sha' => $parentSha, 'type' => 1 ];
751
          }
752
        }
753
      } elseif( $type === 2 ) {
754
        $pos = 0;
755
        $len = strlen( $data );
756
757
        while( $pos < $len ) {
758
          $space = strpos( $data, ' ', $pos );
759
          $eos   = strpos( $data, "\0", $space );
760
761
          if( $space === false || $eos === false ) {
762
            break;
763
          }
764
765
          $mode = substr( $data, $pos, $space - $pos );
766
          $hash = bin2hex( substr( $data, $eos + 1, 20 ) );
767
768
          if( $mode !== '160000' ) {
769
            $isDir   = $mode === '40000' || $mode === '040000';
770
            $queue[] = [ 'sha' => $hash, 'type' => $isDir ? 2 : 3 ];
771
          }
772
773
          $pos = $eos + 21;
774
        }
775
      } elseif( $type === 4 ) {
776
        $isTagTgt = preg_match( '/^object ([0-9a-f]{40})/m', $data, $m );
777
778
        if( $isTagTgt ) {
779
          $nextType = 1;
780
781
          if( preg_match( '/^type (commit|tree|blob|tag)/m', $data, $t ) ) {
782
            $map      = [
783
              'commit' => 1,
784
              'tree'   => 2,
785
              'blob'   => 3,
786
              'tag'    => 4
787
            ];
788
            $nextType = $map[$t[1]] ?? 1;
789
          }
790
791
          $queue[] = [ 'sha' => $m[1], 'type' => $nextType ];
792
        }
793
      }
794
    }
795
796
    return $objs;
670 797
  }
671 798
M git/GitPacks.php
3 3
  private const MAX_READ     = 1040576;
4 4
  private const MAX_RAM      = 1048576;
5
  private const MAX_BASE_RAM = 524288;
6
  private const MAX_DEPTH    = 50;
7
8
  private string $objectsPath;
9
  private array  $packFiles;
10
  private string $lastPack = '';
11
  private array  $fileHandles;
12
  private array  $fanoutCache;
13
  private array  $shaBucketCache;
14
  private array  $offsetBucketCache;
15
16
  public function __construct( string $objectsPath ) {
17
    $this->objectsPath       = $objectsPath;
18
    $this->packFiles         = glob( "{$this->objectsPath}/pack/*.idx" ) ?: [];
19
    $this->fileHandles       = [];
20
    $this->fanoutCache       = [];
21
    $this->shaBucketCache    = [];
22
    $this->offsetBucketCache = [];
23
  }
24
25
  public function __destruct() {
26
    foreach( $this->fileHandles as $handle ) {
27
      if( is_resource( $handle ) ) {
28
        fclose( $handle );
29
      }
30
    }
31
  }
32
33
  public function peek( string $sha, int $len = 12 ): string {
34
    $info   = $this->findPackInfo( $sha );
35
    $result = '';
36
37
    if( $info['offset'] !== 0 ) {
38
      $handle = $this->getHandle( $info['file'] );
39
40
      if( $handle ) {
41
        $result = $this->readPackEntry(
42
          $handle,
43
          $info['offset'],
44
          $len,
45
          $len
46
        );
47
      }
48
    }
49
50
    return $result;
51
  }
52
53
  public function read( string $sha ): string {
54
    $info   = $this->findPackInfo( $sha );
55
    $result = '';
56
57
    if( $info['offset'] !== 0 ) {
58
      $size = $this->extractPackedSize( $info['file'], $info['offset'] );
59
60
      if( $size <= self::MAX_RAM ) {
61
        $handle = $this->getHandle( $info['file'] );
62
63
        if( $handle ) {
64
          $result = $this->readPackEntry(
65
            $handle,
66
            $info['offset'],
67
            $size
68
          );
69
        }
70
      }
71
    }
72
73
    return $result;
74
  }
75
76
  public function stream( string $sha, callable $callback ): bool {
77
    return $this->streamInternal( $sha, $callback, 0 );
78
  }
79
80
  private function streamInternal(
81
    string $sha,
82
    callable $callback,
83
    int $depth
84
  ): bool {
85
    $info   = $this->findPackInfo( $sha );
86
    $result = false;
87
88
    if( $info['offset'] !== 0 ) {
89
      $size   = $this->extractPackedSize( $info['file'], $info['offset'] );
90
      $handle = $this->getHandle( $info['file'] );
91
92
      if( $handle ) {
93
        $result = $this->streamPackEntry(
94
          $handle,
95
          $info['offset'],
96
          $size,
97
          $callback,
98
          $depth
99
        );
100
      }
101
    }
102
103
    return $result;
104
  }
105
106
  public function getSize( string $sha ): int {
107
    $info   = $this->findPackInfo( $sha );
108
    $result = 0;
109
110
    if( $info['offset'] !== 0 ) {
111
      $result = $this->extractPackedSize( $info['file'], $info['offset'] );
112
    }
113
114
    return $result;
115
  }
116
117
  private function findPackInfo( string $sha ): array {
118
    $result = [ 'offset' => 0, 'file' => '' ];
119
120
    if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
121
      $binarySha = hex2bin( $sha );
122
      if( $this->lastPack !== '' ) {
123
        $offset = $this->findInIdx( $this->lastPack, $binarySha );
124
125
        if( $offset !== 0 ) {
126
          $result = [
127
            'file'   => str_replace( '.idx', '.pack', $this->lastPack ),
128
            'offset' => $offset
129
          ];
130
        }
131
      }
132
133
      if( $result['offset'] === 0 ) {
134
        foreach( $this->packFiles as $indexFile ) {
135
          if( $indexFile !== $this->lastPack ) {
136
            $offset = $this->findInIdx( $indexFile, $binarySha );
137
138
            if( $offset !== 0 ) {
139
              $this->lastPack = $indexFile;
140
              $result         = [
141
                'file'   => str_replace( '.idx', '.pack', $indexFile ),
142
                'offset' => $offset
143
              ];
144
              break;
145
            }
146
          }
147
        }
148
      }
149
    }
150
151
    return $result;
152
  }
153
154
  private function findInIdx( string $indexFile, string $binarySha ): int {
155
    $handle = $this->getHandle( $indexFile );
156
    $result = 0;
157
158
    if( $handle ) {
159
      if( !isset( $this->fanoutCache[$indexFile] ) ) {
160
        fseek( $handle, 0 );
161
        $head = fread( $handle, 8 );
162
163
        if( $head === "\377tOc\0\0\0\2" ) {
164
          $this->fanoutCache[$indexFile] = array_values(
165
            unpack( 'N*', fread( $handle, 1024 ) )
166
          );
167
        }
168
      }
169
170
      if( isset( $this->fanoutCache[$indexFile] ) ) {
171
        $fanout = $this->fanoutCache[$indexFile];
172
        $byte   = ord( $binarySha[0] );
173
        $start  = $byte === 0 ? 0 : $fanout[$byte - 1];
174
        $end    = $fanout[$byte];
175
176
        if( $end > $start ) {
177
          $result = $this->binarySearchIdx(
178
            $indexFile,
179
            $handle,
180
            $start,
181
            $end,
182
            $binarySha,
183
            $fanout[255]
184
          );
185
        }
186
      }
187
    }
188
189
    return $result;
190
  }
191
192
  private function binarySearchIdx(
193
    string $indexFile,
194
    $handle,
195
    int $start,
196
    int $end,
197
    string $binarySha,
198
    int $total
199
  ): int {
200
    $key    = "$indexFile:$start";
201
    $count  = $end - $start;
202
    $result = 0;
203
204
    if( !isset( $this->shaBucketCache[$key] ) ) {
205
      fseek( $handle, 1032 + ($start * 20) );
206
      $this->shaBucketCache[$key] = fread( $handle, $count * 20 );
207
208
      fseek( $handle, 1032 + ($total * 24) + ($start * 4) );
209
      $this->offsetBucketCache[$key] = fread( $handle, $count * 4 );
210
    }
211
212
    $shaBlock = $this->shaBucketCache[$key];
213
    $low      = 0;
214
    $high     = $count - 1;
215
    $found    = -1;
216
217
    while( $low <= $high ) {
218
      $mid = ($low + $high) >> 1;
219
      $cmp = substr( $shaBlock, $mid * 20, 20 );
220
221
      if( $cmp < $binarySha ) {
222
        $low = $mid + 1;
223
      } elseif( $cmp > $binarySha ) {
224
        $high = $mid - 1;
225
      } else {
226
        $found = $mid;
227
        break;
228
      }
229
    }
230
231
    if( $found !== -1 ) {
232
      $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 );
233
      $offset = unpack( 'N', $packed )[1];
234
235
      if( $offset & 0x80000000 ) {
236
        $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8);
237
        fseek( $handle, $pos64 );
238
        $offset = unpack( 'J', fread( $handle, 8 ) )[1];
239
      }
240
      $result = (int)$offset;
241
    }
242
243
    return $result;
244
  }
245
246
  private function readPackEntry(
247
    $handle,
248
    int $offset,
249
    int $size,
250
    int $cap = 0
251
  ): string {
252
    fseek( $handle, $offset );
253
    $header = $this->readVarInt( $handle );
254
    $type   = ($header['byte'] >> 4) & 7;
255
256
    return ($type === 6)
257
      ? $this->handleOfsDelta( $handle, $offset, $size, $cap )
258
      : (($type === 7)
259
        ? $this->handleRefDelta( $handle, $size, $cap )
260
        : $this->decompressToString( $handle, $cap ));
261
  }
262
263
  private function streamPackEntry(
264
    $handle,
265
    int $offset,
266
    int $size,
267
    callable $callback,
268
    int $depth = 0
269
  ): bool {
270
    fseek( $handle, $offset );
271
    $header = $this->readVarInt( $handle );
272
    $type   = ($header['byte'] >> 4) & 7;
273
274
    return ($type === 6 || $type === 7)
275
      ? $this->streamDeltaObject( $handle, $offset, $type, $callback, $depth )
276
      : $this->streamDecompression( $handle, $callback );
277
  }
278
279
  private function streamDeltaObject(
280
    $handle,
281
    int $offset,
282
    int $type,
283
    callable $callback,
284
    int $depth = 0
285
  ): bool {
286
    if( $depth >= self::MAX_DEPTH ) {
287
      return false;
288
    }
289
290
    fseek( $handle, $offset );
291
    $this->readVarInt( $handle );
292
    $result = false;
293
294
    if( $type === 6 ) {
295
      $neg      = $this->readOffsetDelta( $handle );
296
      $deltaPos = ftell( $handle );
297
      $base     = '';
298
299
      $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
300
301
      if( $baseSize > self::MAX_BASE_RAM ) {
302
        return false;
303
      }
304
305
      $this->streamPackEntry(
306
        $handle,
307
        $offset - $neg,
308
        0,
309
        function( $c ) use ( &$base ) { $base .= $c; },
310
        $depth + 1
311
      );
312
313
      fseek( $handle, $deltaPos );
314
      $result = $this->applyDeltaStream( $handle, $base, $callback );
315
    } else {
316
      $baseSha  = bin2hex( fread( $handle, 20 ) );
317
      $baseSize = $this->getSize( $baseSha );
318
319
      if( $baseSize > self::MAX_BASE_RAM ) {
320
        return false;
321
      }
322
323
      $base = '';
324
325
      if( $this->streamInternal( $baseSha, function( $c ) use ( &$base ) {
326
        $base .= $c;
327
      }, $depth + 1 ) ) {
328
        $result = $this->applyDeltaStream( $handle, $base, $callback );
329
      }
330
    }
331
332
    return $result;
333
  }
334
335
  private function applyDeltaStream(
336
    $handle,
337
    string $base,
338
    callable $callback
339
  ): bool {
340
    $infl = inflate_init( ZLIB_ENCODING_DEFLATE );
341
    $ok   = false;
342
343
    if( $infl ) {
344
      $state  = 0;
345
      $buffer = '';
346
      $ok     = true;
347
348
      while( !feof( $handle ) ) {
349
        $chunk = fread( $handle, 8192 );
350
351
        if( $chunk === '' ) {
352
          break;
353
        }
354
355
        $data = @inflate_add( $infl, $chunk );
356
357
        if( $data === false ) {
358
          $ok = false;
359
          break;
360
        }
361
362
        $buffer .= $data;
363
364
        while( true ) {
365
          $len = strlen( $buffer );
366
367
          if( $len === 0 ) {
368
            break;
369
          }
370
371
          if( $state < 2 ) {
372
            $pos = 0;
373
            while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; }
374
375
            if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
376
              break;
377
            }
378
379
            $buffer = substr( $buffer, $pos + 1 );
380
            $state++;
381
            continue;
382
          }
383
384
          $op = ord( $buffer[0] );
385
386
          if( $op & 128 ) {
387
            $need = $this->getCopyInstructionSize( $op );
388
389
            if( $len < 1 + $need ) {
390
              break;
391
            }
392
393
            $info = $this->parseCopyInstruction( $op, $buffer, 1 );
394
395
            $callback( substr( $base, $info['off'], $info['len'] ) );
396
            $buffer = substr( $buffer, 1 + $need );
397
          } else {
398
            $ln = $op & 127;
399
400
            if( $len < 1 + $ln ) {
401
              break;
402
            }
403
404
            $callback( substr( $buffer, 1, $ln ) );
405
            $buffer = substr( $buffer, 1 + $ln );
406
          }
407
        }
408
409
        if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) {
410
          break;
411
        }
412
      }
413
    }
414
415
    return $ok;
5
  private const MAX_BASE_RAM = 2097152;
6
  private const MAX_DEPTH    = 200;
7
8
  private string $objectsPath;
9
  private array  $packFiles;
10
  private string $lastPack = '';
11
  private array  $fileHandles;
12
  private array  $fanoutCache;
13
  private array  $shaBucketCache;
14
  private array  $offsetBucketCache;
15
16
  public function __construct( string $objectsPath ) {
17
    $this->objectsPath       = $objectsPath;
18
    $this->packFiles         = glob( "{$this->objectsPath}/pack/*.idx" ) ?: [];
19
    $this->fileHandles       = [];
20
    $this->fanoutCache       = [];
21
    $this->shaBucketCache    = [];
22
    $this->offsetBucketCache = [];
23
  }
24
25
  public function __destruct() {
26
    foreach( $this->fileHandles as $handle ) {
27
      if( is_resource( $handle ) ) {
28
        fclose( $handle );
29
      }
30
    }
31
  }
32
33
  public function peek( string $sha, int $len = 12 ): string {
34
    $info   = $this->findPackInfo( $sha );
35
    $result = '';
36
37
    if( $info['offset'] !== 0 ) {
38
      $handle = $this->getHandle( $info['file'] );
39
40
      if( $handle ) {
41
        $result = $this->readPackEntry(
42
          $handle,
43
          $info['offset'],
44
          $len,
45
          $len
46
        );
47
      }
48
    }
49
50
    return $result;
51
  }
52
53
  public function read( string $sha ): string {
54
    $info   = $this->findPackInfo( $sha );
55
    $result = '';
56
57
    if( $info['offset'] !== 0 ) {
58
      $size = $this->extractPackedSize( $info['file'], $info['offset'] );
59
60
      if( $size <= self::MAX_RAM ) {
61
        $handle = $this->getHandle( $info['file'] );
62
63
        if( $handle ) {
64
          $result = $this->readPackEntry(
65
            $handle,
66
            $info['offset'],
67
            $size
68
          );
69
        }
70
      }
71
    }
72
73
    return $result;
74
  }
75
76
  public function stream( string $sha, callable $callback ): bool {
77
    return $this->streamInternal( $sha, $callback, 0 );
78
  }
79
80
  public function streamGenerator( string $sha ): Generator {
81
    $info = $this->findPackInfo( $sha );
82
83
    if( $info['offset'] !== 0 ) {
84
      $handle = $this->getHandle( $info['file'] );
85
86
      if( $handle ) {
87
        yield from $this->streamPackEntryGenerator(
88
          $handle,
89
          $info['offset'],
90
          0
91
        );
92
      }
93
    }
94
  }
95
96
  private function streamInternal(
97
    string $sha,
98
    callable $callback,
99
    int $depth
100
  ): bool {
101
    $info   = $this->findPackInfo( $sha );
102
    $result = false;
103
104
    if( $info['offset'] !== 0 ) {
105
      $size   = $this->extractPackedSize( $info['file'], $info['offset'] );
106
      $handle = $this->getHandle( $info['file'] );
107
108
      if( $handle ) {
109
        $result = $this->streamPackEntry(
110
          $handle,
111
          $info['offset'],
112
          $size,
113
          $callback,
114
          $depth
115
        );
116
      }
117
    }
118
119
    return $result;
120
  }
121
122
  public function getSize( string $sha ): int {
123
    $info   = $this->findPackInfo( $sha );
124
    $result = 0;
125
126
    if( $info['offset'] !== 0 ) {
127
      $result = $this->extractPackedSize( $info['file'], $info['offset'] );
128
    }
129
130
    return $result;
131
  }
132
133
  private function findPackInfo( string $sha ): array {
134
    $result = [ 'offset' => 0, 'file' => '' ];
135
136
    if( strlen( $sha ) === 40 && ctype_xdigit( $sha ) ) {
137
      $binarySha = hex2bin( $sha );
138
139
      if( $this->lastPack !== '' ) {
140
        $offset = $this->findInIdx( $this->lastPack, $binarySha );
141
142
        if( $offset !== 0 ) {
143
          $result = [
144
            'file'   => str_replace( '.idx', '.pack', $this->lastPack ),
145
            'offset' => $offset
146
          ];
147
        }
148
      }
149
150
      if( $result['offset'] === 0 ) {
151
        foreach( $this->packFiles as $indexFile ) {
152
          if( $indexFile !== $this->lastPack ) {
153
            $offset = $this->findInIdx( $indexFile, $binarySha );
154
155
            if( $offset !== 0 ) {
156
              $this->lastPack = $indexFile;
157
              $result         = [
158
                'file'   => str_replace( '.idx', '.pack', $indexFile ),
159
                'offset' => $offset
160
              ];
161
              break;
162
            }
163
          }
164
        }
165
      }
166
    }
167
168
    return $result;
169
  }
170
171
  private function findInIdx( string $indexFile, string $binarySha ): int {
172
    $handle = $this->getHandle( $indexFile );
173
    $result = 0;
174
175
    if( $handle ) {
176
      if( !isset( $this->fanoutCache[$indexFile] ) ) {
177
        fseek( $handle, 0 );
178
        $head = fread( $handle, 8 );
179
180
        if( $head === "\377tOc\0\0\0\2" ) {
181
          $this->fanoutCache[$indexFile] = array_values(
182
            unpack( 'N*', fread( $handle, 1024 ) )
183
          );
184
        }
185
      }
186
187
      if( isset( $this->fanoutCache[$indexFile] ) ) {
188
        $fanout = $this->fanoutCache[$indexFile];
189
        $byte   = ord( $binarySha[0] );
190
        $start  = $byte === 0 ? 0 : $fanout[$byte - 1];
191
        $end    = $fanout[$byte];
192
193
        if( $end > $start ) {
194
          $result = $this->binarySearchIdx(
195
            $indexFile,
196
            $handle,
197
            $start,
198
            $end,
199
            $binarySha,
200
            $fanout[255]
201
          );
202
        }
203
      }
204
    }
205
206
    return $result;
207
  }
208
209
  private function binarySearchIdx(
210
    string $indexFile,
211
    $handle,
212
    int $start,
213
    int $end,
214
    string $binarySha,
215
    int $total
216
  ): int {
217
    $key    = "$indexFile:$start";
218
    $count  = $end - $start;
219
    $result = 0;
220
221
    if( !isset( $this->shaBucketCache[$key] ) ) {
222
      fseek( $handle, 1032 + ($start * 20) );
223
      $this->shaBucketCache[$key] = fread( $handle, $count * 20 );
224
225
      fseek( $handle, 1032 + ($total * 24) + ($start * 4) );
226
      $this->offsetBucketCache[$key] = fread( $handle, $count * 4 );
227
    }
228
229
    $shaBlock = $this->shaBucketCache[$key];
230
    $low      = 0;
231
    $high     = $count - 1;
232
    $found    = -1;
233
234
    while( $low <= $high ) {
235
      $mid = ($low + $high) >> 1;
236
      $cmp = substr( $shaBlock, $mid * 20, 20 );
237
238
      if( $cmp < $binarySha ) {
239
        $low = $mid + 1;
240
      } elseif( $cmp > $binarySha ) {
241
        $high = $mid - 1;
242
      } else {
243
        $found = $mid;
244
        break;
245
      }
246
    }
247
248
    if( $found !== -1 ) {
249
      $packed = substr( $this->offsetBucketCache[$key], $found * 4, 4 );
250
      $offset = unpack( 'N', $packed )[1];
251
252
      if( $offset & 0x80000000 ) {
253
        $pos64 = 1032 + ($total * 28) + (($offset & 0x7FFFFFFF) * 8);
254
        fseek( $handle, $pos64 );
255
        $offset = unpack( 'J', fread( $handle, 8 ) )[1];
256
      }
257
      $result = (int)$offset;
258
    }
259
260
    return $result;
261
  }
262
263
  private function readPackEntry(
264
    $handle,
265
    int $offset,
266
    int $size,
267
    int $cap = 0
268
  ): string {
269
    fseek( $handle, $offset );
270
    $header = $this->readVarInt( $handle );
271
    $type   = ($header['byte'] >> 4) & 7;
272
273
    return ($type === 6)
274
      ? $this->handleOfsDelta( $handle, $offset, $size, $cap )
275
      : (($type === 7)
276
        ? $this->handleRefDelta( $handle, $size, $cap )
277
        : $this->decompressToString( $handle, $cap ));
278
  }
279
280
  private function streamPackEntry(
281
    $handle,
282
    int $offset,
283
    int $size,
284
    callable $callback,
285
    int $depth = 0
286
  ): bool {
287
    fseek( $handle, $offset );
288
    $header = $this->readVarInt( $handle );
289
    $type   = ($header['byte'] >> 4) & 7;
290
291
    return ($type === 6 || $type === 7)
292
      ? $this->streamDeltaObject( $handle, $offset, $type, $callback, $depth )
293
      : $this->streamDecompression( $handle, $callback );
294
  }
295
296
  private function streamDeltaObject(
297
    $handle,
298
    int $offset,
299
    int $type,
300
    callable $callback,
301
    int $depth = 0
302
  ): bool {
303
    if( $depth >= self::MAX_DEPTH ) {
304
      error_log( "[GitPacks] delta depth limit exceeded at offset $offset" );
305
      return false;
306
    }
307
308
    fseek( $handle, $offset );
309
    $this->readVarInt( $handle );
310
    $result = false;
311
312
    if( $type === 6 ) {
313
      $neg      = $this->readOffsetDelta( $handle );
314
      $deltaPos = ftell( $handle );
315
      $base     = '';
316
      $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
317
318
      if( $baseSize > self::MAX_BASE_RAM ) {
319
        error_log( "[GitPacks] ofs-delta base too large for RAM path: $baseSize" );
320
        return false;
321
      }
322
323
      $this->streamPackEntry(
324
        $handle,
325
        $offset - $neg,
326
        0,
327
        function( $c ) use ( &$base ) { $base .= $c; },
328
        $depth + 1
329
      );
330
331
      fseek( $handle, $deltaPos );
332
      $result = $this->applyDeltaStream( $handle, $base, $callback );
333
    } else {
334
      $baseSha  = bin2hex( fread( $handle, 20 ) );
335
      $baseSize = $this->getSize( $baseSha );
336
337
      if( $baseSize > self::MAX_BASE_RAM ) {
338
        error_log( "[GitPacks] ref-delta base too large for RAM path: $baseSize (sha=$baseSha)" );
339
        return false;
340
      }
341
342
      $base = '';
343
344
      if( $this->streamInternal( $baseSha, function( $c ) use ( &$base ) {
345
        $base .= $c;
346
      }, $depth + 1 ) ) {
347
        $result = $this->applyDeltaStream( $handle, $base, $callback );
348
      }
349
    }
350
351
    return $result;
352
  }
353
354
  private function applyDeltaStream(
355
    $handle,
356
    string $base,
357
    callable $callback
358
  ): bool {
359
    $infl = inflate_init( ZLIB_ENCODING_DEFLATE );
360
    $ok   = false;
361
362
    if( $infl ) {
363
      $state  = 0;
364
      $buffer = '';
365
      $ok     = true;
366
367
      while( !feof( $handle ) ) {
368
        $chunk = fread( $handle, 8192 );
369
370
        if( $chunk === '' ) {
371
          break;
372
        }
373
374
        $data = @inflate_add( $infl, $chunk );
375
376
        if( $data === false ) {
377
          $ok = false;
378
          break;
379
        }
380
381
        $buffer .= $data;
382
383
        while( true ) {
384
          $len = strlen( $buffer );
385
386
          if( $len === 0 ) {
387
            break;
388
          }
389
390
          if( $state < 2 ) {
391
            $pos = 0;
392
            while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; }
393
394
            if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
395
              break;
396
            }
397
398
            $buffer = substr( $buffer, $pos + 1 );
399
            $state++;
400
            continue;
401
          }
402
403
          $op = ord( $buffer[0] );
404
405
          if( $op & 128 ) {
406
            $need = $this->getCopyInstructionSize( $op );
407
408
            if( $len < 1 + $need ) {
409
              break;
410
            }
411
412
            $info = $this->parseCopyInstruction( $op, $buffer, 1 );
413
414
            $callback( substr( $base, $info['off'], $info['len'] ) );
415
            $buffer = substr( $buffer, 1 + $need );
416
          } else {
417
            $ln = $op & 127;
418
419
            if( $len < 1 + $ln ) {
420
              break;
421
            }
422
423
            $callback( substr( $buffer, 1, $ln ) );
424
            $buffer = substr( $buffer, 1 + $ln );
425
          }
426
        }
427
428
        if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) {
429
          break;
430
        }
431
      }
432
    }
433
434
    return $ok;
435
  }
436
437
  private function streamPackEntryGenerator(
438
    $handle,
439
    int $offset,
440
    int $depth
441
  ): Generator {
442
    fseek( $handle, $offset );
443
    $header = $this->readVarInt( $handle );
444
    $type   = ($header['byte'] >> 4) & 7;
445
446
    if( $type === 6 || $type === 7 ) {
447
      yield from $this->streamDeltaObjectGenerator(
448
        $handle,
449
        $offset,
450
        $type,
451
        $depth
452
      );
453
    } else {
454
      yield from $this->streamDecompressionGenerator( $handle );
455
    }
456
  }
457
458
  /**
459
   * Decompresses the pack entry at $baseOffset into a temp file and returns
460
   * the open handle rewound to byte 0, or null if tmpfile() fails.
461
   * The caller is responsible for fclose()-ing the handle.
462
   */
463
  private function resolveBaseToTempFile(
464
    $packHandle,
465
    int $baseOffset,
466
    int $depth
467
  ) {
468
    $tmpHandle = tmpfile();
469
470
    if( !$tmpHandle ) {
471
      error_log(
472
        "[GitPacks] tmpfile() failed for ofs-delta base at offset $baseOffset"
473
      );
474
      return null;
475
    }
476
477
    foreach( $this->streamPackEntryGenerator( $packHandle, $baseOffset, $depth + 1 ) as $chunk ) {
478
      fwrite( $tmpHandle, $chunk );
479
    }
480
481
    rewind( $tmpHandle );
482
483
    return $tmpHandle;
484
  }
485
486
  private function streamDeltaObjectGenerator(
487
    $handle,
488
    int $offset,
489
    int $type,
490
    int $depth
491
  ): Generator {
492
    if( $depth >= self::MAX_DEPTH ) {
493
      error_log( "[GitPacks] delta depth limit exceeded at offset $offset" );
494
      return;
495
    }
496
497
    fseek( $handle, $offset );
498
    $this->readVarInt( $handle );
499
500
    if( $type === 6 ) {
501
      $neg      = $this->readOffsetDelta( $handle );
502
      $deltaPos = ftell( $handle );
503
      $baseSize = $this->extractPackedSize( $handle, $offset - $neg );
504
505
      if( $baseSize > self::MAX_BASE_RAM ) {
506
        $tmpHandle = $this->resolveBaseToTempFile(
507
          $handle,
508
          $offset - $neg,
509
          $depth
510
        );
511
512
        if( $tmpHandle === null ) {
513
          return;
514
        }
515
516
        fseek( $handle, $deltaPos );
517
        yield from $this->applyDeltaStreamFromFileGenerator(
518
          $handle,
519
          $tmpHandle
520
        );
521
        fclose( $tmpHandle );
522
      } else {
523
        $base = '';
524
        $this->streamPackEntry(
525
          $handle,
526
          $offset - $neg,
527
          0,
528
          function( $c ) use ( &$base ) { $base .= $c; },
529
          $depth + 1
530
        );
531
        fseek( $handle, $deltaPos );
532
        yield from $this->applyDeltaStreamGenerator( $handle, $base );
533
      }
534
    } else {
535
      $baseSha  = bin2hex( fread( $handle, 20 ) );
536
      $baseSize = $this->getSize( $baseSha );
537
538
      if( $baseSize > self::MAX_BASE_RAM ) {
539
        $tmpHandle = tmpfile();
540
541
        if( !$tmpHandle ) {
542
          error_log(
543
            "[GitPacks] tmpfile() failed for ref-delta base (sha=$baseSha)"
544
          );
545
          return;
546
        }
547
548
        $written = $this->streamInternal(
549
          $baseSha,
550
          function( $c ) use ( $tmpHandle ) { fwrite( $tmpHandle, $c ); },
551
          $depth + 1
552
        );
553
554
        if( $written ) {
555
          rewind( $tmpHandle );
556
          yield from $this->applyDeltaStreamFromFileGenerator(
557
            $handle,
558
            $tmpHandle
559
          );
560
        }
561
562
        fclose( $tmpHandle );
563
      } else {
564
        $base = '';
565
566
        if( $this->streamInternal(
567
          $baseSha,
568
          function( $c ) use ( &$base ) { $base .= $c; },
569
          $depth + 1
570
        ) ) {
571
          yield from $this->applyDeltaStreamGenerator( $handle, $base );
572
        }
573
      }
574
    }
575
  }
576
577
  private function applyDeltaStreamGenerator(
578
    $handle,
579
    string $base
580
  ): Generator {
581
    $infl = inflate_init( ZLIB_ENCODING_DEFLATE );
582
583
    if( !$infl ) {
584
      return;
585
    }
586
587
    $state  = 0;
588
    $buffer = '';
589
590
    while( !feof( $handle ) ) {
591
      $chunk = fread( $handle, 8192 );
592
593
      if( $chunk === '' ) {
594
        break;
595
      }
596
597
      $data = @inflate_add( $infl, $chunk );
598
599
      if( $data === false ) {
600
        break;
601
      }
602
603
      $buffer .= $data;
604
605
      while( true ) {
606
        $len = strlen( $buffer );
607
608
        if( $len === 0 ) {
609
          break;
610
        }
611
612
        if( $state < 2 ) {
613
          $pos = 0;
614
          while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; }
615
616
          if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
617
            break;
618
          }
619
620
          $buffer = substr( $buffer, $pos + 1 );
621
          $state++;
622
          continue;
623
        }
624
625
        $op = ord( $buffer[0] );
626
627
        if( $op & 128 ) {
628
          $need = $this->getCopyInstructionSize( $op );
629
630
          if( $len < 1 + $need ) {
631
            break;
632
          }
633
634
          $info = $this->parseCopyInstruction( $op, $buffer, 1 );
635
          yield substr( $base, $info['off'], $info['len'] );
636
          $buffer = substr( $buffer, 1 + $need );
637
        } else {
638
          $ln = $op & 127;
639
640
          if( $len < 1 + $ln ) {
641
            break;
642
          }
643
644
          yield substr( $buffer, 1, $ln );
645
          $buffer = substr( $buffer, 1 + $ln );
646
        }
647
      }
648
649
      if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) {
650
        break;
651
      }
652
    }
653
  }
654
655
  private function applyDeltaStreamFromFileGenerator(
656
    $deltaHandle,
657
    $baseHandle
658
  ): Generator {
659
    $infl = inflate_init( ZLIB_ENCODING_DEFLATE );
660
661
    if( !$infl ) {
662
      return;
663
    }
664
665
    $state  = 0;
666
    $buffer = '';
667
668
    while( !feof( $deltaHandle ) ) {
669
      $chunk = fread( $deltaHandle, 8192 );
670
671
      if( $chunk === '' ) {
672
        break;
673
      }
674
675
      $data = @inflate_add( $infl, $chunk );
676
677
      if( $data === false ) {
678
        break;
679
      }
680
681
      $buffer .= $data;
682
683
      while( true ) {
684
        $len = strlen( $buffer );
685
686
        if( $len === 0 ) {
687
          break;
688
        }
689
690
        if( $state < 2 ) {
691
          $pos = 0;
692
          while( $pos < $len && (ord( $buffer[$pos] ) & 128) ) { $pos++; }
693
694
          if( $pos === $len && (ord( $buffer[$pos - 1] ) & 128) ) {
695
            break;
696
          }
697
698
          $buffer = substr( $buffer, $pos + 1 );
699
          $state++;
700
          continue;
701
        }
702
703
        $op = ord( $buffer[0] );
704
705
        if( $op & 128 ) {
706
          $need = $this->getCopyInstructionSize( $op );
707
708
          if( $len < 1 + $need ) {
709
            break;
710
          }
711
712
          $info = $this->parseCopyInstruction( $op, $buffer, 1 );
713
          fseek( $baseHandle, $info['off'] );
714
          $remaining = $info['len'];
715
716
          while( $remaining > 0 ) {
717
            $slice = fread( $baseHandle, min( 65536, $remaining ) );
718
719
            if( $slice === false || $slice === '' ) {
720
              break;
721
            }
722
723
            yield $slice;
724
            $remaining -= strlen( $slice );
725
          }
726
727
          $buffer = substr( $buffer, 1 + $need );
728
        } else {
729
          $ln = $op & 127;
730
731
          if( $len < 1 + $ln ) {
732
            break;
733
          }
734
735
          yield substr( $buffer, 1, $ln );
736
          $buffer = substr( $buffer, 1 + $ln );
737
        }
738
      }
739
740
      if( inflate_get_status( $infl ) === ZLIB_STREAM_END ) {
741
        break;
742
      }
743
    }
744
  }
745
746
  private function streamDecompressionGenerator( $handle ): Generator {
747
    $infl = inflate_init( ZLIB_ENCODING_DEFLATE );
748
749
    if( !$infl ) {
750
      return;
751
    }
752
753
    while( !feof( $handle ) ) {
754
      $chunk = fread( $handle, 8192 );
755
756
      if( $chunk === '' ) {
757
        break;
758
      }
759
760
      $data = @inflate_add( $infl, $chunk );
761
762
      if( $data !== false && $data !== '' ) {
763
        yield $data;
764
      }
765
766
      if( $data === false ||
767
          inflate_get_status( $infl ) === ZLIB_STREAM_END ) {
768
        break;
769
      }
770
    }
416 771
  }
417 772
M git/GitRefs.php
16 16
17 17
      if( $input === 'HEAD' && file_exists( $headFile ) ) {
18
        $head = trim( file_get_contents( $headFile ) );
18
        $head   = trim( file_get_contents( $headFile ) );
19 19
        $result = strpos( $head, 'ref: ' ) === 0
20 20
          ? $this->resolve( substr( $head, 5 ) )
...
47 47
    }
48 48
49
    if( empty( $found ) ) {
50
      $key = array_key_first( $branches );
51
      $found = $key
52
        ? [ 'name' => $key, 'hash' => $branches[$key] ]
53
        : [ 'name' => '', 'hash' => '' ];
49
    if( empty( $found ) && !empty( $branches ) ) {
50
      $key   = array_key_first( $branches );
51
      $found = [ 'name' => $key, 'hash' => $branches[$key] ];
52
    } elseif( empty( $found ) ) {
53
      $found = [ 'name' => '', 'hash' => '' ];
54 54
    }
55 55
56 56
    return $found;
57 57
  }
58 58
59 59
  public function scanRefs( string $prefix, callable $callback ): void {
60 60
    $dir = "{$this->repoPath}/$prefix";
61
62
    $this->traverseDirectory( $dir, $callback, '' );
63
  }
61 64
65
  private function traverseDirectory(
66
    string $dir,
67
    callable $callback,
68
    string $subPath
69
  ): void {
62 70
    if( is_dir( $dir ) ) {
63 71
      $files = array_diff( scandir( $dir ), ['.', '..'] );
72
64 73
      foreach( $files as $file ) {
65
        $callback( $file, trim( file_get_contents( "$dir/$file" ) ) );
74
        $path = "$dir/$file";
75
        $name = $subPath === '' ? $file : "$subPath/$file";
76
77
        if( is_dir( $path ) ) {
78
          $this->traverseDirectory( $path, $callback, $name );
79
        } elseif( is_file( $path ) ) {
80
          $sha = trim( file_get_contents( $path ) );
81
82
          if( preg_match( '/^[0-9a-f]{40}$/', $sha ) ) {
83
            $callback( $name, $sha );
84
          }
85
        }
66 86
      }
67 87
    }
...
74 94
    foreach( $paths as $ref ) {
75 95
      $path = "{$this->repoPath}/$ref";
96
76 97
      if( file_exists( $path ) ) {
77 98
        $result = trim( file_get_contents( $path ) );
78 99
        break;
79 100
      }
80 101
    }
81 102
82 103
    if( $result === '' ) {
83 104
      $packedPath = "{$this->repoPath}/packed-refs";
105
84 106
      if( file_exists( $packedPath ) ) {
85 107
        $result = $this->findInPackedRefs( $packedPath, $input );
86 108
      }
109
    }
110
111
    if( !preg_match( '/^[0-9a-f]{40}$/', $result ) ) {
112
      $result = '';
87 113
    }
88 114
...
98 124
      if( $line[0] !== '#' && $line[0] !== '^' ) {
99 125
        $parts = explode( ' ', trim( $line ) );
126
100 127
        if( count( $parts ) >= 2 && in_array( $parts[1], $targets ) ) {
101 128
          $result = $parts[0];
M pages/ClonePage.php
87 87
88 88
  private function handleUploadPack(): void {
89
    set_time_limit( 0 );
90
89 91
    header( 'Content-Type: application/x-git-upload-pack-result' );
90 92
    header( 'Cache-Control: no-cache' );
91
92
    $input   = file_get_contents( 'php://input' );
93
    $wants   = [];
94
    $haves   = [];
95
    $offset  = 0;
96
    $isGzip  = isset( $_SERVER['HTTP_CONTENT_ENCODING'] ) &&
97
               $_SERVER['HTTP_CONTENT_ENCODING'] === 'gzip';
98 93
99
    if( $isGzip ) {
100
      $decoded = gzdecode( $input );
94
    $wants  = [];
95
    $haves  = [];
96
    $handle = fopen( 'php://input', 'rb' );
101 97
102
      if( is_string( $decoded ) ) {
103
        $input = $decoded;
98
    if( $handle ) {
99
      // If the input is gzipped, we wrap the stream
100
      if( isset( $_SERVER['HTTP_CONTENT_ENCODING'] ) &&
101
          $_SERVER['HTTP_CONTENT_ENCODING'] === 'gzip' ) {
102
        stream_filter_append( $handle, 'zlib.inflate', STREAM_FILTER_READ, [
103
          'window' => 31
104
        ] );
104 105
      }
105
    }
106 106
107
    while( $offset < strlen( $input ) ) {
108
      $result = $this->readPacketLine( $input, $offset );
109
      $line   = $result[0];
110
      $next   = $result[1];
107
      while( !feof( $handle ) ) {
108
        $lenHex = fread( $handle, 4 );
111 109
112
      if( $next === $offset || $line === 'done' ) {
113
        break;
114
      }
110
        if( strlen( $lenHex ) < 4 ) {
111
          break;
112
        }
115 113
116
      $offset = $next;
114
        $len = hexdec( $lenHex );
117 115
118
      if( $line === '' ) {
119
        continue;
120
      }
116
        if( $len === 0 ) { // Flush packet
117
          break;
118
        }
121 119
122
      $trim = trim( $line );
120
        if( $len <= 4 ) {
121
          continue;
122
        }
123 123
124
      if( strpos( $trim, 'want ' ) === 0 ) {
125
        $wants[] = explode( ' ', $trim )[1];
126
      } elseif( strpos( $trim, 'have ' ) === 0 ) {
127
        $haves[] = explode( ' ', $trim )[1];
124
        $line = fread( $handle, $len - 4 );
125
        $trim = trim( $line );
126
127
        if( strpos( $trim, 'want ' ) === 0 ) {
128
          $wants[] = explode( ' ', $trim )[1];
129
        } elseif( strpos( $trim, 'have ' ) === 0 ) {
130
          $haves[] = explode( ' ', $trim )[1];
131
        }
132
133
        if( $trim === 'done' ) {
134
          break;
135
        }
128 136
      }
137
138
      fclose( $handle );
129 139
    }
130 140
131
    if( $wants ) {
141
    if( !empty( $wants ) ) {
132 142
      $this->packetWrite( "NAK\n" );
133 143
134
      $objects = $this->git->collectObjects( $wants, $haves );
135
      $pack    = $this->git->generatePackfile( $objects );
144
      $objects       = $this->git->collectObjects( $wants, $haves );
145
      $lastHeartbeat = time();
136 146
137
      $this->sendSidebandData( 1, $pack );
147
      foreach( $this->git->generatePackfile( $objects ) as $chunk ) {
148
        if( $chunk !== '' ) {
149
          $this->sendSidebandData( 1, $chunk );
150
        }
151
152
        $now = time();
153
        if( $now - $lastHeartbeat >= 5 ) {
154
          $this->sendSidebandData( 2, "\r" );
155
          $lastHeartbeat = $now;
156
        }
157
      }
138 158
    }
139 159
140 160
    $this->packetFlush();
141 161
  }
142 162
143 163
  private function sendSidebandData( int $band, string $data ): void {
144
    $chunkSize = 65000;
164
    $chunkSize = 65515;
145 165
    $len       = strlen( $data );
146 166
M robots.txt
1
user-agent: * 
2
disallow: 
3
crawl-delay: 60
1
user-agent: *
2
disallow:
3
crawl-delay: 10080
4 4
5
User-agent: Googlebot
6
Disallow: /_mobile/
7
Disallow: /*repo=
8
Disallow: /*action=
5
user-agent: Googlebot
6
disallow: /_mobile/
7
disallow: /*repo=
8
disallow: /*action=
9
9 10