burterm/internal/comparer/diff.go
2026-09-14 10:55:07 +03:00

118 lines
4.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package comparer реализует построчно-словесное сравнение двух текстов
// (аналог вкладки Comparer в Burp) на основе классического LCS-алгоритма.
package comparer
import "regexp"
// tokenPattern разбивает текст на чередующиеся куски пробелов и
// непробельных последовательностей — так diff получается на уровне
// "слов" (примерно как режим "Words" в Burp Comparer), но при этом
// исходный текст полностью восстанавливается конкатенацией токенов,
// включая переносы строк и пробелы между ними.
var tokenPattern = regexp.MustCompile(`\s+|\S+`)
func tokenize(s string) []string {
return tokenPattern.FindAllString(s, -1)
}
// Op — тип операции над токеном при переходе от a к b.
type Op int
const (
OpEqual Op = iota
OpDelete
OpInsert
)
// Chunk — последовательность токенов с одной и той же операцией,
// склеенных в одну строку для компактного вывода.
type Chunk struct {
Op Op
Text string
}
// Diff сравнивает a и b на уровне токенов (слов и пробельных
// промежутков) через LCS и возвращает последовательность чанков:
// OpEqual — совпадающие куски, OpDelete — есть только в a,
// OpInsert — есть только в b.
//
// Сложность O(n·m) по числу токенов в a и b — для вставки HTTP-запросов
// (сотни-тысячи токенов) это доли миллисекунды; для вставки многомегабайтных
// текстов такой алгоритм не годится, но это осознанно не наш случай.
func Diff(a, b string) []Chunk {
ta := tokenize(a)
tb := tokenize(b)
lcs := lcsTable(ta, tb)
ops := backtrack(lcs, ta, tb, len(ta), len(tb))
return mergeChunks(ops)
}
type tokenOp struct {
op Op
text string
}
// lcsTable строит стандартную DP-таблицу длин наибольшей общей
// подпоследовательности для токенов ta и tb.
func lcsTable(ta, tb []string) [][]int {
n, m := len(ta), len(tb)
table := make([][]int, n+1)
for i := range table {
table[i] = make([]int, m+1)
}
for i := 1; i <= n; i++ {
for j := 1; j <= m; j++ {
if ta[i-1] == tb[j-1] {
table[i][j] = table[i-1][j-1] + 1
} else if table[i-1][j] >= table[i][j-1] {
table[i][j] = table[i-1][j]
} else {
table[i][j] = table[i][j-1]
}
}
}
return table
}
// backtrack восстанавливает последовательность операций из DP-таблицы,
// двигаясь от (n, m) к (0, 0), и разворачивает её в порядок a→b.
func backtrack(table [][]int, ta, tb []string, i, j int) []tokenOp {
var reversed []tokenOp
for i > 0 || j > 0 {
switch {
case i > 0 && j > 0 && ta[i-1] == tb[j-1]:
reversed = append(reversed, tokenOp{OpEqual, ta[i-1]})
i--
j--
case j > 0 && (i == 0 || table[i][j-1] >= table[i-1][j]):
reversed = append(reversed, tokenOp{OpInsert, tb[j-1]})
j--
default:
reversed = append(reversed, tokenOp{OpDelete, ta[i-1]})
i--
}
}
ops := make([]tokenOp, len(reversed))
for k, v := range reversed {
ops[len(reversed)-1-k] = v
}
return ops
}
// mergeChunks склеивает подряд идущие токены с одинаковой операцией
// в один Chunk — иначе на каждое слово приходился бы отдельный чанк,
// что нечитаемо в выводе.
func mergeChunks(ops []tokenOp) []Chunk {
var chunks []Chunk
for _, o := range ops {
if len(chunks) > 0 && chunks[len(chunks)-1].Op == o.op {
chunks[len(chunks)-1].Text += o.text
continue
}
chunks = append(chunks, Chunk{Op: o.op, Text: o.text})
}
return chunks
}