| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-10 12:25:50 GMT-0800 |
| Commit | 0e3e558b9dfcdca6367cf8abef2f554400609d4d |
| Parent | 021d1af |
| Delta | 80 lines added, 22 lines removed, 58-line increase |
| class Git { | ||
| private const CHUNK_SIZE = 128; | ||
| + private const MAX_READ_SIZE = 1048576; | ||
| private string $repoPath; | ||
| public function read( string $sha ): string { | ||
| - $loosePath = $this->getLoosePath( $sha ); | ||
| - | ||
| - if( file_exists( $loosePath ) ) { | ||
| - $rawContent = file_get_contents( $loosePath ); | ||
| - $inflated = $rawContent ? @gzuncompress( $rawContent ) : false; | ||
| + $size = $this->getObjectSize( $sha ); | ||
| - return $inflated ? explode( "\0", $inflated, 2 )[1] : ''; | ||
| + if( $size > self::MAX_READ_SIZE ) { | ||
| + return ''; | ||
| } | ||
| - return $this->packs->read( $sha ) ?? ''; | ||
| + $content = ''; | ||
| + | ||
| + $this->slurp( $sha, function( $chunk ) use ( &$content ) { | ||
| + $content .= $chunk; | ||
| + } ); | ||
| + | ||
| + return $content; | ||
| } | ||
| public function stream( string $sha, callable $callback ): void { | ||
| - $data = $this->read( $sha ); | ||
| + $this->slurp( $sha, $callback ); | ||
| + } | ||
| - if( $data !== '' ) { | ||
| + private function slurp( string $sha, callable $callback ): void { | ||
| + $loosePath = $this->getLoosePath( $sha ); | ||
| + | ||
| + if( file_exists( $loosePath ) ) { | ||
| + $fileHandle = @fopen( $loosePath, 'rb' ); | ||
| + | ||
| + if( !$fileHandle ) return; | ||
| + | ||
| + $inflator = inflate_init( ZLIB_ENCODING_DEFLATE ); | ||
| + $buffer = ''; | ||
| + $headerFound = false; | ||
| + | ||
| + while( !feof( $fileHandle ) ) { | ||
| + $chunk = fread( $fileHandle, 16384 ); | ||
| + $inflatedChunk = @inflate_add( $inflator, $chunk ); | ||
| + | ||
| + if( $inflatedChunk === false ) break; | ||
| + | ||
| + if( !$headerFound ) { | ||
| + $buffer .= $inflatedChunk; | ||
| + $nullPos = strpos( $buffer, "\0" ); | ||
| + | ||
| + if( $nullPos !== false ) { | ||
| + $body = substr( $buffer, $nullPos + 1 ); | ||
| + | ||
| + if( $body !== '' ) { | ||
| + $callback( $body ); | ||
| + } | ||
| + | ||
| + $headerFound = true; | ||
| + $buffer = ''; | ||
| + } | ||
| + } else { | ||
| + $callback( $inflatedChunk ); | ||
| + } | ||
| + } | ||
| + | ||
| + fclose( $fileHandle ); | ||
| + return; | ||
| + } | ||
| + | ||
| + $data = $this->packs->read( $sha ); | ||
| + | ||
| + if( $data !== null && $data !== '' ) { | ||
| $callback( $data ); | ||
| } | ||
| class GitDiff { | ||
| private $git; | ||
| + private const MAX_DIFF_SIZE = 1048576; | ||
| public function __construct(Git $git) { | ||
| private function createChange($type, $path, $oldSha, $newSha) { | ||
| + // Check file sizes before reading content to prevent OOM | ||
| + $oldSize = $oldSha ? $this->git->getObjectSize($oldSha) : 0; | ||
| + $newSize = $newSha ? $this->git->getObjectSize($newSha) : 0; | ||
| + | ||
| + // If file is too large, skip diffing and treat as binary | ||
| + if ($oldSize > self::MAX_DIFF_SIZE || $newSize > self::MAX_DIFF_SIZE) { | ||
| + return [ | ||
| + 'type' => $type, | ||
| + 'path' => $path, | ||
| + 'is_binary' => true, | ||
| + 'hunks' => [] | ||
| + ]; | ||
| + } | ||
| + | ||
| $oldContent = $oldSha ? $this->git->read($oldSha) : ''; | ||
| $newContent = $newSha ? $this->git->read($newSha) : ''; | ||
| private function skipSize( string $data, int &$position ): void { | ||
| - while( ord( $data[$position++] ) & 128 ) { | ||
| - // Empty loop body | ||
| + $length = strlen( $data ); | ||
| + | ||
| + while( $position < $length && (ord( $data[$position++] ) & 128) ) { | ||
| + // Loop continues while MSB is 1 | ||
| } | ||
| } |
| 'rmd' => [self::CAT_TEXT, 'text/r-markdown'], | ||
| 'txt' => [self::CAT_TEXT, 'text/plain'], | ||
| - 'yaml' => [self::CAT_TEXT, 'text/yaml'], | ||
| - 'yml' => [self::CAT_TEXT, 'text/yaml'], | ||
| 'gradle' => [self::CAT_TEXT, 'text/plain'], | ||
| 'gitignore' => [self::CAT_TEXT, 'text/plain'], | ||
| 'tex' => [self::CAT_TEXT, 'application/x-tex'], | ||
| 'lyx' => [self::CAT_TEXT, 'application/x-lyx'], | ||
| 'bat' => [self::CAT_TEXT, 'application/x-msdos-program'], | ||
| 'ts' => [self::CAT_TEXT, 'application/typescript'], | ||
| 'log' => [self::CAT_TEXT, 'text/plain'], | ||
| 'ini' => [self::CAT_TEXT, 'text/plain'], | ||
| 'conf' => [self::CAT_TEXT, 'text/plain'], | ||
| - 'zip' => [self::CAT_ARCHIVE, 'application/zip'], | ||
| 'jpg' => [self::CAT_IMAGE, 'image/jpeg'], | ||
| 'jpeg' => [self::CAT_IMAGE, 'image/jpeg'], | ||
| // Config formats | ||
| + 'yaml' => [self::CAT_TEXT, 'text/yaml'], | ||
| + 'yml' => [self::CAT_TEXT, 'text/yaml'], | ||
| 'toml' => [self::CAT_TEXT, 'application/toml'], | ||
| 'env' => [self::CAT_TEXT, 'text/plain'], | ||
| 'sql' => [self::CAT_TEXT, 'application/sql'], | ||
| 'html' => [self::CAT_TEXT, 'text/html'], | ||
| + 'xhtml' => [self::CAT_TEXT, 'text/xhtml'], | ||
| 'css' => [self::CAT_TEXT, 'text/css'], | ||
| 'js' => [self::CAT_TEXT, 'application/javascript'], | ||
| // Build / DevOps | ||
| 'dockerfile'=> [self::CAT_TEXT, 'text/plain'], | ||
| + 'containerfile'=> [self::CAT_TEXT, 'text/plain'], | ||
| 'makefile' => [self::CAT_TEXT, 'text/x-makefile'], | ||
| 'cmake' => [self::CAT_TEXT, 'text/x-cmake'], | ||
| - 'gitmodules'=> [self::CAT_TEXT, 'text/plain'], | ||
| - 'editorconfig'=> [self::CAT_TEXT, 'text/plain'], | ||
| - | ||
| - // Dependency / package files | ||
| - 'lock' => [self::CAT_TEXT, 'text/plain'], | ||
| - 'pipfile' => [self::CAT_TEXT, 'text/plain'], | ||
| - 'pipfile.lock' => [self::CAT_TEXT, 'application/json'], | ||
| - 'requirements.txt' => [self::CAT_TEXT, 'text/plain'], | ||
| // Misc text | ||