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