<?php require_once __DIR__ . '/GitPacks.php'; require_once __DIR__ . '/LooseObjects.php'; class PackfileWriter { private GitPacks $packs; private LooseObjects $loose; public function __construct( GitPacks $packs, LooseObjects $loose ) { $this->packs = $packs; $this->loose = $loose; } public function generate( array $objs ): Generator { $entries = $this->buildEntries( $objs ); $ctx = \hash_init( 'sha1' ); $head = "PACK" . \pack( 'N', 2 ) . \pack( 'N', \count( $objs ) ); \hash_update( $ctx, $head ); yield $head; $written = []; $outPos = 12; foreach( $entries as $sha => $entry ) { $written[$sha] = $outPos; $baseSha = $entry['baseSha']; $reuse = $baseSha !== '' && isset( $written[$baseSha] ); $hdr = $reuse ? $this->encodeEntryHeader( 6, $entry['deltaSize'] ) . $this->encodeOffsetDelta( $outPos - $written[$baseSha] ) : $this->encodeEntryHeader( $entry['logicalType'], $entry['size'] > 0 ? $entry['size'] : $this->getObjectSize( $sha ) ); \hash_update( $ctx, $hdr ); $outPos += \strlen( $hdr ); yield $hdr; $stream = $reuse ? $this->packs->streamRawDelta( $sha ) : $this->streamCompressed( $sha ); foreach( $stream as $chunk ) { \hash_update( $ctx, $chunk ); $outPos += \strlen( $chunk ); yield $chunk; } } yield \hash_final( $ctx, true ); } private function buildEntries( array $objs ): array { $entries = []; $offToSha = []; foreach( $objs as $sha => $logicalType ) { $meta = $this->packs->getEntryMeta( $sha ); $entries[$sha] = [ 'logicalType' => $logicalType, 'packType' => $meta['type'], 'deltaSize' => $meta['size'], 'packFile' => $meta['file'], 'offset' => $meta['offset'], 'baseOffset' => $meta['baseOffset'] ?? 0, 'baseSha' => $meta['baseSha'] ?? '', ]; if( $meta['file'] !== '' ) { $offToSha[$meta['file']][$meta['offset']] = $sha; } } foreach( $entries as &$e ) { $e['baseSha'] = $e['packType'] === 6 && $e['baseOffset'] > 0 ? ( $offToSha[$e['packFile']][$e['baseOffset']] ?? '' ) : $e['baseSha']; } unset( $e ); $files = []; $offsets = []; foreach( $entries as $e ) { $files[] = $e['packFile']; $offsets[] = $e['offset']; } \array_multisort( $files, \SORT_ASC, \SORT_STRING, $offsets, \SORT_ASC, \SORT_NUMERIC, $entries ); foreach( $entries as $sha => &$e ) { $e['size'] = $e['baseSha'] === '' ? $this->getObjectSize( $sha ) : 0; } unset( $e ); return $entries; } private function getObjectSize( string $sha ): int { return $this->packs->getSize( $sha ) ?: $this->loose->getSize( $sha ); } private function streamCompressed( string $sha ): Generator { $generator = $this->packs->streamRawCompressed( $sha ); $generator->rewind(); if( $generator->valid() ) { yield from $generator; } else { $deflate = \deflate_init( \ZLIB_ENCODING_DEFLATE ); foreach( $this->getDecompressedChunks( $sha ) as $raw ) { $compressed = \deflate_add( $deflate, $raw, \ZLIB_NO_FLUSH ); if( $compressed !== '' ) { yield $compressed; } } $final = \deflate_add( $deflate, '', \ZLIB_FINISH ); if( $final !== '' ) { yield $final; } } } private function getDecompressedChunks( string $sha ): Generator { $looseGen = $this->loose->streamChunks( $sha ); $looseGen->rewind(); if( $looseGen->valid() ) { yield from $looseGen; } else { $packGen = $this->packs->streamGenerator( $sha ); $packGen->rewind(); if( $packGen->valid() ) { yield from $packGen; } else { $data = $this->packs->read( $sha ); if( $data !== '' ) { yield $data; } } } } private function encodeEntryHeader( int $type, int $size ): string { $byte = $type << 4 | $size & 0x0f; $sz = $size >> 4; $hdr = ''; while( $sz > 0 ) { $hdr .= \chr( $byte | 0x80 ); $byte = $sz & 0x7f; $sz >>= 7; } return $hdr . \chr( $byte ); } private function encodeOffsetDelta( int $offset ): string { $buf = \chr( $offset & 0x7F ); $n = $offset >> 7; while( $n > 0 ) { $n--; $buf = \chr( 0x80 | $n & 0x7F ) . $buf; $n >>= 7; } return $buf; } }