* @license MIT */ namespace Pterodactyl\Addons\AdvancedAdmin\Services\Revisions; class DiffService { /** * Generate a unified diff between old and new content. * Returns array of lines with type: 'context' | 'added' | 'removed' | 'header' */ public function diff(string $oldContent, string $newContent, string $fileName = 'file'): array { if (class_exists(\SebastianBergmann\Diff\Differ::class)) { return $this->diffViaSebastian($oldContent, $newContent, $fileName); } return $this->diffNative($oldContent, $newContent); } /** * Compute a structured diff using SebastianBergmann\Diff. */ private function diffViaSebastian(string $old, string $new, string $fileName): array { $differ = new \SebastianBergmann\Diff\Differ( new \SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder("--- a/{$fileName}\n+++ b/{$fileName}\n") ); $rawDiff = $differ->diff($old, $new); return $this->parseUnifiedDiff($rawDiff); } /** * Simple pure-PHP line-level diff (fallback). */ private function diffNative(string $old, string $new): array { $oldLines = explode("\n", $old); $newLines = explode("\n", $new); $result = []; // LCS-based diff $lcs = $this->lcs($oldLines, $newLines); $i = 0; $j = 0; $k = 0; while ($i < count($oldLines) || $j < count($newLines)) { if ($k < count($lcs) && $i < count($oldLines) && $oldLines[$i] === $lcs[$k] && $j < count($newLines) && $newLines[$j] === $lcs[$k]) { $result[] = ['type' => 'context', 'content' => $oldLines[$i], 'line_old' => $i + 1, 'line_new' => $j + 1]; $i++; $j++; $k++; } elseif ($j < count($newLines) && ($k >= count($lcs) || $newLines[$j] !== $lcs[$k])) { $result[] = ['type' => 'added', 'content' => $newLines[$j], 'line_new' => $j + 1]; $j++; } else { $result[] = ['type' => 'removed', 'content' => $oldLines[$i], 'line_old' => $i + 1]; $i++; } } return $result; } private function parseUnifiedDiff(string $diff): array { $lines = explode("\n", $diff); $result = []; $oldLine = 0; $newLine = 0; foreach ($lines as $line) { if (str_starts_with($line, '---') || str_starts_with($line, '+++')) { $result[] = ['type' => 'header', 'content' => $line]; } elseif (str_starts_with($line, '@@')) { // Parse @@ -old +new @@ preg_match('/@@ -(\d+).*\+(\d+)/', $line, $m); $oldLine = (int) ($m[1] ?? 0); $newLine = (int) ($m[2] ?? 0); $result[] = ['type' => 'header', 'content' => $line]; } elseif (str_starts_with($line, '-')) { $result[] = ['type' => 'removed', 'content' => substr($line, 1), 'line_old' => $oldLine++]; } elseif (str_starts_with($line, '+')) { $result[] = ['type' => 'added', 'content' => substr($line, 1), 'line_new' => $newLine++]; } elseif (str_starts_with($line, ' ')) { $result[] = ['type' => 'context', 'content' => substr($line, 1), 'line_old' => $oldLine++, 'line_new' => $newLine++]; } } return $result; } /** * Longest Common Subsequence (for native diff). */ private function lcs(array $a, array $b): array { $m = count($a); $n = count($b); $dp = array_fill(0, $m + 1, array_fill(0, $n + 1, 0)); for ($i = 1; $i <= $m; $i++) { for ($j = 1; $j <= $n; $j++) { $dp[$i][$j] = $a[$i-1] === $b[$j-1] ? $dp[$i-1][$j-1] + 1 : max($dp[$i-1][$j], $dp[$i][$j-1]); } } $result = []; $i = $m; $j = $n; while ($i > 0 && $j > 0) { if ($a[$i-1] === $b[$j-1]) { array_unshift($result, $a[$i-1]); $i--; $j--; } elseif ($dp[$i-1][$j] > $dp[$i][$j-1]) { $i--; } else { $j--; } } return $result; } }