Добавлены комментарии

This commit is contained in:
Alexander Zhirov 2025-08-31 15:50:02 +03:00
parent 06b2a2a993
commit 32203fadc0
Signed by: alexander
GPG key ID: C8D8BE544A27C511

View file

@ -1,197 +1,303 @@
/* ============================================================
Модуль: diff
Назначение: тонкая ООП-обёртка над libxdiff для вычисления unified-diff
Вход: строки/пути/массивы байт
Выход: единая строка с патчем (включая заголовки ---/+++)
Краткая схема работы:
- Готовим два mmfile_t (формат libxdiff) из входных байтов.
- Настраиваем callback, который будет собирать текст диффа в буфер.
- Вызываем xdl_diff(..), который сравнивает и «льёт» вывод через callback.
- Склеиваем заголовки (---/+++) и «тело» патча, отдаём как string.
Замечания:
- Всё в памяти: входы читаются целиком, результат накапливается целиком.
- Для бинарного сравнения лучше использовать bdiff/rabdiff/bpatch (можно добавить).
- Заголовки формируются в этом модуле (libxdiff их сам не печатает).
- При необходимости можно ввести нормализацию CRLF/пробелов флагами xpp/xecfg.
============================================================ */
module diff; module diff;
import std.exception : enforce; import std.exception : enforce;
import std.file : read, exists; // бинарное чтение import std.file : read, exists; // бинарное чтение файлов без перекодирования
import std.array : Appender, appender; import std.array : Appender, appender; // накапливающий буфер для результата
import std.string : representation, format; import std.string : representation, format; // быстрый доступ к байтам строки, форматирование
import std.datetime : Clock, UTC; import std.datetime : Clock, UTC; // метка времени в заголовках
import core.stdc.string : memcpy; import core.stdc.string : memcpy; // копирование в буферы libxdiff
import core.stdc.stdlib : malloc, free, realloc; import core.stdc.stdlib : malloc, free, realloc; // системный аллокатор для прокидывания в xdiff
import xdiff; import xdiff; // C-API libxdiff (xdiff.h)
/* ============================================================
Аллокатор для libxdiff
------------------------------------------------------------
На ряде сборок libxdiff ожидает, что клиент задаст функции
выделения памяти через xdl_set_allocator(). Если этого не
сделать, xdl_mmfile_writeallocate() может возвращать null.
============================================================ */
//-----------------------------
// Аллокатор для libxdiff (обязателен на ряде сборок)
//-----------------------------
private extern (C) void* _wrap_malloc(void*, uint size) private extern (C) void* _wrap_malloc(void*, uint size)
{ {
return malloc(size); // Проксируем к системному malloc
return malloc(size);
} }
private extern (C) void _wrap_free(void*, void* ptr) private extern (C) void _wrap_free(void*, void* ptr)
{ {
free(ptr); // Проксируем к системному free
free(ptr);
} }
private extern (C) void* _wrap_realloc(void*, void* ptr, uint size) private extern (C) void* _wrap_realloc(void*, void* ptr, uint size)
{ {
return realloc(ptr, size); // Проксируем к системному realloc
return realloc(ptr, size);
} }
// Глобальный флаг «аллокатор уже установлен». __gshared — на уровне процесса.
private __gshared bool _allocatorReady = false; private __gshared bool _allocatorReady = false;
private void ensureAllocator() private void ensureAllocator()
{ {
if (_allocatorReady) // Важно: вызывать до любых операций с mmfile_t
return; if (_allocatorReady)
memallocator_t malt; return;
malt.priv = null;
malt.malloc = &_wrap_malloc; memallocator_t malt;
malt.free = &_wrap_free; malt.priv = null;
malt.realloc = &_wrap_realloc; malt.malloc = &_wrap_malloc;
enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed"); malt.free = &_wrap_free;
_allocatorReady = true; malt.realloc = &_wrap_realloc;
// Если вернуть не 0 — значит libxdiff отверг конфигурацию
enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed");
_allocatorReady = true;
/* Потокобезопасность:
Это «ленивая» инициализация без синхронизации. В многопоточной среде
второй параллельный вызов может повторно вызвать xdl_set_allocator(),
что обычно безвредно. Если нужна строгая гарантия оберните это место
мьютексом/atomic-флагом. */
} }
//----------------------------- /* ============================================================
// RAII для mmfile_t RAII для mmfile_t
//----------------------------- ------------------------------------------------------------
Упрощает корректную инициализацию/очистку mmfile_t. Метод
fromBytes() создаёт mmfile, выделяет буфер и копирует данные.
============================================================ */
private struct MmFile private struct MmFile
{ {
mmfile_t mf; mmfile_t mf; // «ручка» на содержимое в терминах libxdiff
static MmFile fromBytes(const(ubyte)[] data, static MmFile fromBytes(const(ubyte)[] data,
long bsize = 8 * 1024, long bsize = 8 * 1024,
ulong flags = XDL_MMF_ATOMIC) ulong flags = XDL_MMF_ATOMIC)
{ {
ensureAllocator(); // критично: до init/writeallocate // Критично: аллокатор должен быть настроен до init/writeallocate
MmFile m; ensureAllocator();
enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed");
if (data.length) MmFile m;
{ // Инициализируем внутренние структуры mmfile
auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, data.length); enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed");
enforce(dst !is null, "xdl_mmfile_writeallocate failed");
memcpy(dst, data.ptr, data.length);
}
return m;
}
~this() // Копируем полезные данные в непрерывный блок, выделенный libxdiff
{ if (data.length)
xdl_free_mmfile(&mf); {
} auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, data.length);
// Если тут null — проверьте flags/bsize; попробуйте flags=0, bsize=32*1024
enforce(dst !is null, "xdl_mmfile_writeallocate failed");
memcpy(dst, data.ptr, data.length);
}
return m;
}
~this()
{
// Освобождаем ресурсы mmfile_t (всегда вызывается, даже при исключениях)
xdl_free_mmfile(&mf);
}
} }
//----------------------------- /* ============================================================
// Синк-коллектор вывода libxdiff Синк-коллектор вывода libxdiff
//----------------------------- ------------------------------------------------------------
libxdiff печатает результат через C-callback (outf). Мы даём
свой writeCB(), который складывает куски в Appender!(ubyte[]).
============================================================ */
private class BufferSink private class BufferSink
{ {
Appender!(ubyte[]) outbuf; // Appender — дешёвая обёртка над динамическим массивом
Appender!(ubyte[]) outbuf;
this() this()
{ {
outbuf = appender!(ubyte[])(); outbuf = appender!(ubyte[])();
} }
extern (C) static int writeCB(void* priv, mmbuffer_t* mb, int nbuf) // Сигнатура коллбэка задана libxdiff: priv — наш контекст, mb — массив буферов
{ extern (C) static int writeCB(void* priv, mmbuffer_t* mb, int nbuf)
auto self = cast(BufferSink) priv; {
foreach (i; 0 .. nbuf) auto self = cast(BufferSink) priv;
{ foreach (i; 0 .. nbuf)
size_t sz = cast(size_t) mb[i].size; {
if (sz == 0) size_t sz = cast(size_t) mb[i].size;
continue; if (sz == 0)
self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]); continue;
} // mmbuffer_t.ptr — char*; приводим к ubyte* и аппендим
return 0; self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]);
} }
return 0; // 0 — успех
}
string toStringOwned() @trusted // Возвращает независимую копию накопленных байт в виде string (UTF-8 предполагается)
{ string toStringOwned() @trusted
return cast(string) outbuf.data.idup; {
} // @trusted: мы знаем, что libxdiff генерирует ASCII/UTF-8
return cast(string) outbuf.data.idup;
}
} }
//----------------------------- /* ============================================================
// Публичный движок unified-diff Публичный движок unified-diff
//----------------------------- ------------------------------------------------------------
Основной API:
- diffText: сравнить две строки
- diffFiles: сравнить два файла (читаются бинарно)
- diffBytes: сравнить два массива байт
Параметры:
- ctxlen: длина контекста (кол-во строк вокруг изменений)
- stripTrailingNewline: при true добавляет '\n', если его нет
============================================================ */
final class DiffEngine final class DiffEngine
{ {
private long _ctxlen; private long _ctxlen; // длина контекста unified-diff
private bool _stripTrailingNewline; private bool _stripTrailingNewline; // косметика: выравнивание окончания файла
this(long ctxlen = 3, bool stripTrailingNewline = false) this(long ctxlen = 3, bool stripTrailingNewline = false)
{ {
_ctxlen = ctxlen; _ctxlen = ctxlen;
_stripTrailingNewline = stripTrailingNewline; _stripTrailingNewline = stripTrailingNewline;
} }
/// Diff двух текстов (UTF-8 строки) /// Diff двух текстов (UTF-8 строки). Метки для заголовков можно задать явно.
string diffText(string oldText, string newText, string diffText(string oldText, string newText,
string oldLabel = "old", string newLabel = "new") string oldLabel = "old", string newLabel = "new")
{ {
const(ubyte)[] a = _prep(oldText); // representation — дешёвый способ получить «сырые» байты из string
const(ubyte)[] b = _prep(newText); const(ubyte)[] a = _prep(oldText);
return diffBytes(a, b, oldLabel, newLabel); const(ubyte)[] b = _prep(newText);
} return diffBytes(a, b, oldLabel, newLabel);
}
/// Diff по путям (файлы читаются целиком, бинарно) /// Diff по путям (файлы читаются целиком, БИНАРНО; кодировка не трогаем)
string diffFiles(string oldPath, string newPath, string diffFiles(string oldPath, string newPath,
string oldLabel = "", string newLabel = "") string oldLabel = "", string newLabel = "")
{ {
enforce(exists(oldPath), "no such file: " ~ oldPath); enforce(exists(oldPath), "no such file: " ~ oldPath);
enforce(exists(newPath), "no such file: " ~ newPath); enforce(exists(newPath), "no such file: " ~ newPath);
auto a = cast(const(ubyte)[]) read(oldPath); // бинарное чтение // Бинарное чтение исключает «сюрпризы» перекодировок/CRLF
auto b = cast(const(ubyte)[]) read(newPath); auto a = cast(const(ubyte)[]) read(oldPath);
auto b = cast(const(ubyte)[]) read(newPath);
if (oldLabel.length == 0) // Если метки не заданы — используем сами пути
oldLabel = oldPath; if (oldLabel.length == 0)
if (newLabel.length == 0) oldLabel = oldPath;
newLabel = newPath; if (newLabel.length == 0)
newLabel = newPath;
return diffBytes(a, b, oldLabel, newLabel); return diffBytes(a, b, oldLabel, newLabel);
} }
/// Базовый метод: diff двух буферов /// Базовый метод: diff двух массивов байт (ядро логики)
string diffBytes(const(ubyte)[] oldBytes, const(ubyte)[] newBytes, string diffBytes(const(ubyte)[] oldBytes, const(ubyte)[] newBytes,
string oldLabel = "old", string newLabel = "new") string oldLabel = "old", string newLabel = "new")
{ {
auto a = MmFile.fromBytes(oldBytes); // 1) Готовим источники для libxdiff
auto b = MmFile.fromBytes(newBytes); auto a = MmFile.fromBytes(oldBytes);
auto b = MmFile.fromBytes(newBytes);
auto sink = new BufferSink; // 2) Готовим приёмник вывода: callback + приватный контекст
xdemitcb_t ecb; auto sink = new BufferSink;
ecb.priv = cast(void*) sink; xdemitcb_t ecb;
ecb.outf = &BufferSink.writeCB; ecb.priv = cast(void*) sink; // будет возвращено в writeCB через priv
ecb.outf = &BufferSink.writeCB;
xpparam_t xpp; // 3) Параметры xdiff
xpp.flags = 0; xpparam_t xpp;
xpp.flags = 0; /* здесь можно задать доп. флаги парсера:
- XDF_* / XDF_INDENT_HEURISTIC и т.п. (см. xdiff.h)
- игнор пробелов/табов и др., если поддерживается */
xdemitconf_t xecfg; xdemitconf_t xecfg;
xecfg.ctxlen = _ctxlen; xecfg.ctxlen = _ctxlen; // длина контекста в unified-diff
auto pre = formatHeaders(oldLabel, newLabel); // 4) Формируем заголовки unified-diff (libxdiff сам их не печатает)
auto pre = formatHeaders(oldLabel, newLabel);
auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb); // 5) Запускаем сравнение — результат «потечёт» в writeCB -> sink.outbuf
enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc)); auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb);
enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc));
return pre ~ sink.toStringOwned(); // 6) Склеиваем заголовок и «тело» патча, отдаём как одну строку
} return pre ~ sink.toStringOwned();
}
static string formatHeaders(string oldLabel, string newLabel) /// Заголовки unified-diff в стиле `--- LABEL \t UTC` / `+++ LABEL \t UTC`
{ static string formatHeaders(string oldLabel, string newLabel)
auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS {
return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n" // Ставим ISO-подобный штамп в UTC, чтобы было стабильно и однозначно
~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n"; auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS
} return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n"
~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n";
}
private: private:
const(ubyte)[] _prep(string s) @trusted // Подготовка входной строки к подаче в MmFile: по желанию добавляем завершающий '\n'
{ const(ubyte)[] _prep(string s) @trusted
auto bytes = cast(const(ubyte)[]) s.representation; {
if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n')) // Быстрый доступ к байтам строки (без аллокаций/копий)
{ auto bytes = cast(const(ubyte)[]) s.representation;
ubyte[] tmp;
tmp.length = bytes.length + 1; // Если включено «сглаживание» и у строки нет завершающего '\n' — добавим его
if (bytes.length) if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n'))
memcpy(tmp.ptr, bytes.ptr, bytes.length); {
tmp[$ - 1] = cast(ubyte) '\n'; // Собираем ubyte[] на 1 байт длиннее, копируем исходные данные и дописываем '\n'
return cast(const(ubyte)[]) tmp; ubyte[] tmp;
} tmp.length = bytes.length + 1;
return bytes; if (bytes.length)
} memcpy(tmp.ptr, bytes.ptr, bytes.length);
tmp[$ - 1] = cast(ubyte) '\n';
return cast(const(ubyte)[]) tmp;
}
return bytes;
}
} }
/* ============================================================
ИДЕИ ДЛЯ РАСШИРЕНИЯ (можно реализовать позже)
------------------------------------------------------------
1) Флаги сравнения:
- Пробросить наружу xpp.flags (игнор пробелов, эвристики и т.п.).
2) Нормализация перевода строк:
- Добавить опцию для унификации CRLF->LF на входе, чтобы не шуметь.
3) Вывод напрямую в FD/файл:
- Реализовать методы diffToFD(int fd)/diffToFile(string path),
где ecb.outf = fdOut, аналогичный твоему CLI примеру.
4) Бинарные режимы:
- Обернуть xdl_bdiff/xdl_rabdiff/xdl_bpatch в том же стиле,
если потребуется компактный бинарный патч.
5) Потокобезопасность ensureAllocator():
- Заменить «ленивый флаг» на mutex/atomic, если модуль будет
вызываться конкурентно из нескольких потоков на старте.
============================================================ */