From 32203fadc0f4adee00f7f06380200f97cab1f2f1 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 31 Aug 2025 15:50:02 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/diff.d | 386 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 246 insertions(+), 140 deletions(-) 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, если модуль будет + вызываться конкурентно из нескольких потоков на старте. + ============================================================ */ From 1f00cff0327e777708448c73de72a31236085647 Mon Sep 17 00:00:00 2001 From: Alexander Zhirov Date: Sun, 31 Aug 2025 16:47:55 +0300 Subject: [PATCH 2/2] from rust --- dub.json | 28 ++++ dub.selections.json | 6 + source/app.d | 31 ++-- source/diff.d | 303 --------------------------------------- source/xdiff_init.d | 33 +++++ source/xdiff_mmblocks.d | 108 ++++++++++++++ source/xdiff_mmfile.d | 166 +++++++++++++++++++++ source/xdiff_unittests.d | 246 +++++++++++++++++++++++++++++++ 8 files changed, 598 insertions(+), 323 deletions(-) create mode 100644 dub.selections.json delete mode 100644 source/diff.d create mode 100644 source/xdiff_init.d create mode 100644 source/xdiff_mmblocks.d create mode 100644 source/xdiff_mmfile.d create mode 100644 source/xdiff_unittests.d diff --git a/dub.json b/dub.json index d9c5233..90807f2 100644 --- a/dub.json +++ b/dub.json @@ -16,5 +16,33 @@ "repository": "git+https://git.zhirov.kz/dlang/xdiff.git", "version": "e2396bc172eba813cdcd1a96c494e35d687f576a" } + }, + "configurations": [ + { + "name": "lib", + "targetType": "library", + "sourcePaths": [ + "source" + ], + "excludedSourceFiles": [ + "source/app.d" + ] + }, + { + "name": "app", + "targetType": "executable", + "mainSourceFile": "source/app.d", + "sourcePaths": [ + "source" + ] + } + ], + "buildTypes": { + "unittest": { + "dflags": [ + "-unittest", + "-g" + ] + } } } \ No newline at end of file diff --git a/dub.selections.json b/dub.selections.json new file mode 100644 index 0000000..0b7884b --- /dev/null +++ b/dub.selections.json @@ -0,0 +1,6 @@ +{ + "fileVersion": 1, + "versions": { + "xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"} + } +} diff --git a/source/app.d b/source/app.d index 30e9f34..aaec5e6 100644 --- a/source/app.d +++ b/source/app.d @@ -1,25 +1,16 @@ -import std.stdio : writeln, stderr; -import std.exception : collectException; +import std.stdio; +import xdiff_mmfile : MMFile; -import diff; - -int main(string[] args) +void main() { - if (args.length != 3) - { - stderr.writeln("use: ", args[0], " OLD NEW"); - return 1; - } + auto a = MMFile.fromBytes(cast(ubyte[])"hello world\n"); + auto b = MMFile.fromBytes(cast(ubyte[])"hello world!\n"); - auto eng = new DiffEngine(3); + auto patch = a.computePatch(b); + writeln("patch size: ", patch.size()); - string result; // сюда положим результат - auto ex = collectException(result = eng.diffFiles(args[1], args[2])); - if (ex !is null) - { - stderr.writeln(ex.msg); - return 2; - } - writeln(result); - return 0; + auto res = a.applyPatch(patch); + writeln("apply success: ", res.success); + write(res.patched.asSlice()); // печатаем результат + writeln(); } diff --git a/source/diff.d b/source/diff.d deleted file mode 100644 index eea1a55..0000000 --- a/source/diff.d +++ /dev/null @@ -1,303 +0,0 @@ -/* ============================================================ - Модуль: 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 core.stdc.string : memcpy; // копирование в буферы libxdiff -import core.stdc.stdlib : malloc, free, realloc; // системный аллокатор для прокидывания в xdiff - -import xdiff; // C-API libxdiff (xdiff.h) - -/* ============================================================ - Аллокатор для libxdiff - ------------------------------------------------------------ - На ряде сборок libxdiff ожидает, что клиент задаст функции - выделения памяти через xdl_set_allocator(). Если этого не - сделать, xdl_mmfile_writeallocate() может возвращать null. - ============================================================ */ - -private extern (C) void* _wrap_malloc(void*, uint size) -{ - // Проксируем к системному malloc - return malloc(size); -} - -private extern (C) void _wrap_free(void*, void* ptr) -{ - // Проксируем к системному free - free(ptr); -} - -private extern (C) void* _wrap_realloc(void*, void* ptr, uint size) -{ - // Проксируем к системному realloc - return realloc(ptr, size); -} - -// Глобальный флаг «аллокатор уже установлен». __gshared — на уровне процесса. -private __gshared bool _allocatorReady = false; - -private void ensureAllocator() -{ - // Важно: вызывать до любых операций с 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 - ------------------------------------------------------------ - Упрощает корректную инициализацию/очистку mmfile_t. Метод - fromBytes() создаёт mmfile, выделяет буфер и копирует данные. - ============================================================ */ - -private struct MmFile -{ - mmfile_t mf; // «ручка» на содержимое в терминах libxdiff - - static MmFile fromBytes(const(ubyte)[] data, - long bsize = 8 * 1024, - ulong flags = XDL_MMF_ATOMIC) - { - // Критично: аллокатор должен быть настроен до init/writeallocate - ensureAllocator(); - - MmFile m; - // Инициализируем внутренние структуры mmfile - enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed"); - - // Копируем полезные данные в непрерывный блок, выделенный 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 печатает результат через C-callback (outf). Мы даём - свой writeCB(), который складывает куски в Appender!(ubyte[]). - ============================================================ */ - -private class BufferSink -{ - // Appender — дешёвая обёртка над динамическим массивом - Appender!(ubyte[]) outbuf; - - this() - { - outbuf = appender!(ubyte[])(); - } - - // Сигнатура коллбэка задана 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 (UTF-8 предполагается) - string toStringOwned() @trusted - { - // @trusted: мы знаем, что libxdiff генерирует ASCII/UTF-8 - return cast(string) outbuf.data.idup; - } -} - -/* ============================================================ - Публичный движок unified-diff - ------------------------------------------------------------ - Основной API: - - diffText: сравнить две строки - - diffFiles: сравнить два файла (читаются бинарно) - - diffBytes: сравнить два массива байт - Параметры: - - ctxlen: длина контекста (кол-во строк вокруг изменений) - - stripTrailingNewline: при true добавляет '\n', если его нет - ============================================================ */ - -final class DiffEngine -{ - private long _ctxlen; // длина контекста unified-diff - private bool _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") - { - // 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); - - // Бинарное чтение исключает «сюрпризы» перекодировок/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; - - return diffBytes(a, b, oldLabel, newLabel); - } - - /// Базовый метод: 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); - - // 2) Готовим приёмник вывода: callback + приватный контекст - auto sink = new BufferSink; - xdemitcb_t ecb; - ecb.priv = cast(void*) sink; // будет возвращено в writeCB через priv - ecb.outf = &BufferSink.writeCB; - - // 3) Параметры xdiff - xpparam_t xpp; - xpp.flags = 0; /* здесь можно задать доп. флаги парсера: - - XDF_* / XDF_INDENT_HEURISTIC и т.п. (см. xdiff.h) - - игнор пробелов/табов и др., если поддерживается */ - - xdemitconf_t xecfg; - xecfg.ctxlen = _ctxlen; // длина контекста в unified-diff - - // 4) Формируем заголовки unified-diff (libxdiff сам их не печатает) - auto pre = formatHeaders(oldLabel, newLabel); - - // 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)); - - // 6) Склеиваем заголовок и «тело» патча, отдаём как одну строку - return pre ~ sink.toStringOwned(); - } - - /// Заголовки 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: - // Подготовка входной строки к подаче в 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, если модуль будет - вызываться конкурентно из нескольких потоков на старте. - ============================================================ */ diff --git a/source/xdiff_init.d b/source/xdiff_init.d new file mode 100644 index 0000000..d73cc09 --- /dev/null +++ b/source/xdiff_init.d @@ -0,0 +1,33 @@ +module xdiff_init; + +import core.stdc.stdlib : malloc, free, realloc; +import std.exception : enforce; +import xdiff; // твой низкоуровневый биндинг (xdiff.d) + +extern (C) private void* wrap_malloc(void*, uint size) { return malloc(size); } +extern (C) private void wrap_free (void*, void* p) { free(p); } +extern (C) private void* wrap_realloc(void*, void* p, uint size){ return realloc(p, size); } + +void ensureInit() +{ + // локальная статика инициализируется один раз на процесс + static bool done = false; + if (done) return; + + memallocator_t m; + m.priv = null; + m.malloc = &wrap_malloc; + m.free = &wrap_free; + m.realloc = &wrap_realloc; + + enforce(xdl_set_allocator(&m) == 0, "xdl_set_allocator failed"); + done = true; +} + +mmfile_t initMmfile(size_t len) +{ + ensureInit(); + mmfile_t mf; + enforce(xdl_init_mmfile(&mf, cast(long)len, XDL_MMF_ATOMIC) == 0, "xdl_init_mmfile failed"); + return mf; +} diff --git a/source/xdiff_mmblocks.d b/source/xdiff_mmblocks.d new file mode 100644 index 0000000..81eeec0 --- /dev/null +++ b/source/xdiff_mmblocks.d @@ -0,0 +1,108 @@ +module xdiff_mmblocks; + +import std.exception : enforce; +import xdiff_init : ensureInit, initMmfile; +import xdiff; + +final class MMBlocks +{ +private: + mmfile_t _inner; + bool _owned = true; // владеем ли блоками? + enum DEFAULT_BSIZE = 8 * 1024; + + // Глубокая копия: читаем все блоки и записываем в dst + static void deepCopyAll(mmfile_t* src, MMBlocks dst) + { + enforce(xdl_seek_mmfile(src, 0) == 0, "seek(0) failed"); + + long sz = 0; + auto p = xdl_mmfile_first(src, &sz); + while (p !is null && sz > 0) + { + auto wrote = xdl_write_mmfile(dst.mmfilePtr(), p, sz); + enforce(wrote == sz, "write failed during deep copy"); + p = xdl_mmfile_next(src, &sz); + } + } + +public: + this() + { + _inner = initMmfile(DEFAULT_BSIZE); + } + + static MMBlocks fromBytes(const(ubyte)[] data) + { + auto b = new MMBlocks; + if (data.length > 0) + { + auto wrote = xdl_write_mmfile(&b._inner, data.ptr, cast(long)data.length); + enforce(wrote == cast(long)data.length, "xdl_write_mmfile wrote less than requested"); + } + return b; + } + + ~this() + { + if (_owned && _inner.head !is null) + xdl_free_mmfile(&_inner); + + // обнулим на всякий случай + _inner.head = null; _inner.tail = null; + _inner.rcur = null; _inner.wcur = null; + _inner.fsize = 0; _inner.bsize = 0; + } + + // доступ к внутреннему mmfile_t* + mmfile_t* mmfilePtr() @trusted { return &_inner; } + + size_t size() { return cast(size_t)xdl_mmfile_size(&_inner); } + bool isCompact() const { return xdl_mmfile_iscompact(cast(mmfile_t*)&_inner) != 0; } + + int writeBuf(const(ubyte)[] buf) + { + auto wrote = xdl_write_mmfile(&_inner, buf.ptr, cast(long)buf.length); + return (wrote == cast(long)buf.length) ? 0 : -1; + } + + /// Глубокая копия (новое владение) + MMBlocks clone() const + { + auto dst = new MMBlocks; + // deepCopyAll принимает mmfile_t*, поэтому аккуратно снимаем const с заголовка: + deepCopyAll(cast(mmfile_t*)&_inner, dst); + return dst; + } + + /// Компактизация через пересборку содержимого + void toCompact() + { + if (isCompact()) return; + + auto dst = new MMBlocks; + deepCopyAll(&_inner, dst); + + // перехватываем владение содержимым dst + if (_owned && _inner.head !is null) + xdl_free_mmfile(&_inner); + + _inner = *dst.mmfilePtr(); + dst._owned = false; // dst больше не владеет + dst._inner.head = null; // чтобы финалайзер dst ничего не освободил + dst._inner.tail = null; + dst._inner.rcur = null; + dst._inner.wcur = null; + dst._inner.fsize = 0; + dst._inner.bsize = 0; + } + + /// Снять владение (используется при move в MMFile) + void disarm() + { + _owned = false; + _inner.head = null; _inner.tail = null; + _inner.rcur = null; _inner.wcur = null; + _inner.fsize = 0; _inner.bsize = 0; + } +} diff --git a/source/xdiff_mmfile.d b/source/xdiff_mmfile.d new file mode 100644 index 0000000..bbd5251 --- /dev/null +++ b/source/xdiff_mmfile.d @@ -0,0 +1,166 @@ +module xdiff_mmfile; + +import std.exception : enforce; +import xdiff_init : ensureInit, initMmfile; +import xdiff_mmblocks : MMBlocks; +import xdiff; + +final class MMFile +{ +private: + mmfile_t _inner; + bool _owned = true; + +public: + this() + { + _inner = initMmfile(0); + } + + static MMFile fromBytes(const(ubyte)[] data) + { + auto f = new MMFile; + if (data.length > 0) + { + auto wrote = xdl_write_mmfile(&f._inner, data.ptr, cast(long)data.length); + enforce(wrote == cast(long)data.length, "xdl_write_mmfile wrote less than requested"); + } + return f; + } + + ~this() + { + if (_owned && _inner.head !is null) + xdl_free_mmfile(&_inner); + + _inner.head = null; _inner.tail = null; + _inner.rcur = null; _inner.wcur = null; + _inner.fsize = 0; _inner.bsize = 0; + } + + size_t size() { return cast(size_t)xdl_mmfile_size(&_inner); } + bool isCompact() const { return xdl_mmfile_iscompact(cast(mmfile_t*)&_inner) != 0; } + + const(ubyte)[] asSlice() const + { + enforce(isCompact(), "MMFile must be compact for asSlice"); + auto h = _inner.head; + if (h is null || h.size <= 0) return (cast(ubyte*)null)[0 .. 0]; + return (cast(const(ubyte)*)h.ptr)[0 .. cast(size_t)h.size]; + } + + ubyte[] asSliceMut() + { + enforce(isCompact(), "MMFile must be compact for asSliceMut"); + auto h = _inner.head; + if (h is null || h.size <= 0) return (cast(ubyte*)null)[0 .. 0]; + return (cast(ubyte*)h.ptr)[0 .. cast(size_t)h.size]; + } + + alias MMPatch = MMBlocks; + + /// Сформировать patch (diff self → other) + MMPatch computePatch(ref MMFile other) + { + auto patch = new MMBlocks(); + + static extern(C) int emitToPatch(void* priv, mmbuffer_t* bufs, int num) + { + auto target = cast(MMBlocks)priv; // класс + foreach (i; 0 .. num) + { + auto b = bufs + i; + auto wrote = xdl_writem_mmfile(target.mmfilePtr(), b, 1); + if (wrote != (*b).size) return -1; + } + return 0; + } + + xpparam_t xpp; xpp.flags = 0; + xdemitconf_t xec; xec.ctxlen = 3; + xdemitcb_t ecb; ecb.priv = cast(void*)patch; ecb.outf = &emitToPatch; + + auto rc = xdl_diff(&_inner, &other._inner, &xpp, &xec, &ecb); + enforce(rc == 0, "xdl_diff failed"); + return patch; + } + + struct PatchResult { bool success; MMFile patched; MMFile rejected; } + + /// Применить patch к self + PatchResult applyPatch(ref MMPatch patch) + { + patch.toCompact(); + + auto acc = new MMBlocks(); + auto rej = new MMBlocks(); + + static extern(C) int emitTo(void* priv, mmbuffer_t* bufs, int num) + { + auto target = cast(MMBlocks)priv; // класс + long expect = 0; foreach (i; 0 .. num) expect += (bufs + i).size; + auto wrote = xdl_writem_mmfile(target.mmfilePtr(), bufs, num); + return (wrote == expect) ? 0 : -1; + } + + xdemitcb_t accCb; accCb.priv = cast(void*)acc; accCb.outf = &emitTo; + xdemitcb_t rejCb; rejCb.priv = cast(void*)rej; rejCb.outf = &emitTo; + + auto rc = xdl_patch(&_inner, patch.mmfilePtr(), XDL_PATCH_NORMAL, &accCb, &rejCb); + enforce(rc == 0 || rc == 1, "xdl_patch failed"); + + auto accFile = MMFile.fromBlocksMoved(acc); + auto rejFile = MMFile.fromBlocksMoved(rej); + + bool ok = (rejFile.size() == 0); + return PatchResult(ok, accFile, rejFile); + } + + /// 3-way merge: коллбэки nothrow, чтобы не бросать через C + void merge3Raw( + ref MMFile f1, + ref MMFile f2, + scope int delegate(const(ubyte)[]) nothrow acceptCb, + scope int delegate(const(ubyte)[]) nothrow rejectCb) + { + static extern(C) int emitAccept(void* priv, mmbuffer_t* bufs, int num) + { + auto d = cast(int delegate(const(ubyte)[]) nothrow*)priv; + foreach (i; 0 .. num) + { + auto b = bufs + i; + auto slice = (cast(const(ubyte)*)((*b).ptr))[0 .. cast(size_t)((*b).size)]; + auto r = (*d)(slice); if (r != 0) return r; + } + return 0; + } + static extern(C) int emitReject(void* priv, mmbuffer_t* bufs, int num) + { + auto d = cast(int delegate(const(ubyte)[]) nothrow*)priv; + foreach (i; 0 .. num) + { + auto b = bufs + i; + auto slice = (cast(const(ubyte)*)((*b).ptr))[0 .. cast(size_t)((*b).size)]; + auto r = (*d)(slice); if (r != 0) return r; + } + return 0; + } + + auto a = acceptCb, r = rejectCb; + xdemitcb_t accCb; accCb.priv = cast(void*)&a; accCb.outf = &emitAccept; + xdemitcb_t rejCb; rejCb.priv = cast(void*)&r; rejCb.outf = &emitReject; + + auto rc = xdl_merge3(&_inner, &f1._inner, &f2._inner, &accCb, &rejCb); + enforce(rc == 0, "xdl_merge3 failed"); + } + + /// «Move» из MMBlocks: забрать владение внутренними блоками + static MMFile fromBlocksMoved(MMBlocks b) + { + b.toCompact(); + auto f = new MMFile; + f._inner = *b.mmfilePtr(); // копируем заголовок (указатели на блоки) + b.disarm(); // источник больше не владеет + return f; + } +} diff --git a/source/xdiff_unittests.d b/source/xdiff_unittests.d new file mode 100644 index 0000000..8646f42 --- /dev/null +++ b/source/xdiff_unittests.d @@ -0,0 +1,246 @@ +module xdiff_unittests; + +import std.algorithm : equal; +import std.array : appender; +import std.exception : assertThrown; +import std.range : iota, cycle, take; +import xdiff_mmfile : MMFile; +import xdiff_mmblocks : MMBlocks; + +// -------------------- Вспомогалки -------------------- + +/// Сгенерировать массив длиной n, цикляя значения 0..239 +ubyte[] genCycled(size_t n) +{ + auto acc = appender!(ubyte[])(); + foreach (v; iota(0, 240).cycle.take(n)) + acc.put(cast(ubyte)v); + return acc.data; +} + +/// Вытянуть контент MMBlocks в байты (не разрушая оригинал) +ubyte[] blocksToBytes(const MMBlocks b) +{ + auto tmp = b.clone(); // глубокая копия + auto mf = MMFile.fromBlocksMoved(tmp); + return (cast(const(ubyte)[]) mf.asSlice()).dup; +} + +/// Сравнить содержимое двух MMFile по байтам +bool sameBytes(const MMFile a, const MMFile b) +{ + return a.asSlice().equal(b.asSlice()); +} + +// -------------------- ТЕСТЫ -------------------- + +unittest // new_empty +{ + auto f = new MMFile; + assert(f.size() == 0); + assert(f.isCompact()); +} + +unittest // new_from_bytes +{ + auto data = cast(ubyte[])"hello world"; + auto f = MMFile.fromBytes(data); + assert(f.size() == data.length); + assert(f.isCompact()); + assert(f.asSlice().equal(data)); +} + +unittest // large_from_bytes +{ + auto data = genCycled(15_000); + auto f = MMFile.fromBytes(data); + assert(f.size() == data.length); + assert(f.isCompact()); + assert(f.asSlice().equal(data)); +} + +unittest // "clone" для MMFile: через байты +{ + auto data = genCycled(15_000); + auto f = MMFile.fromBytes(data); + auto f2 = MMFile.fromBytes(f.asSlice().dup); + assert(sameBytes(f, f2)); +} + +unittest // clone_blocks +{ + auto data = genCycled(15_000); + auto b = MMBlocks.fromBytes(data); + auto b2 = b.clone(); // глубокий + auto bytes = blocksToBytes(b); + auto bytes2 = blocksToBytes(b2); + assert(bytes.equal(bytes2)); +} + +unittest // eq для MMFile (по байтам) +{ + auto data = genCycled(15_000); + auto f = MMFile.fromBytes(data); + auto f2 = MMFile.fromBytes(data.dup); + assert(sameBytes(f, f2)); +} + +unittest // as_slice +{ + assert((new MMFile).asSlice().length == 0); + + auto data = genCycled(15_000); + auto f = MMFile.fromBytes(data); + assert(f.asSlice().equal(data)); +} + +unittest // as_slice_mut +{ + auto empty = new MMFile; + assert(empty.asSlice().length == 0); + + auto data = genCycled(15_000); + auto f = MMFile.fromBytes(data); + assert(f.asSlice()[0] == data[0]); + + auto mut = f.asSliceMut(); + mut[0] = cast(ubyte) (mut[0] + 1); + assert(f.asSlice()[0] == data[0] + 1); +} + +unittest // diff_simple → проверяем applyPatch +{ + auto data = cast(ubyte[])"hello world\n"; + auto data2 = cast(ubyte[])"hello world!\n"; + auto f = MMFile.fromBytes(data); + auto f2 = MMFile.fromBytes(data2); + + auto patch = f.computePatch(f2); + auto r = f.applyPatch(patch); + assert(r.success); + assert(r.patched.asSlice().equal(data2)); + assert(r.rejected.asSlice().length == 0); +} + +unittest // diff_with_mutation +{ + auto f = MMFile.fromBytes(cast(ubyte[])"hello world\n"); + auto f2 = MMFile.fromBytes(cast(ubyte[])"hello world!\n"); + + // 1-й прогон + auto p1 = f.computePatch(f2); + auto r1 = f.applyPatch(p1); + assert(r1.success); + assert(r1.patched.asSlice().equal(f2.asSlice())); + + // Меняем первую букву во втором файле + auto m = f2.asSliceMut(); + m[0] = cast(ubyte)'j'; + + // 2-й прогон + auto p2 = f.computePatch(f2); + auto r2 = f.applyPatch(p2); + assert(r2.success); + assert(r2.patched.asSlice().equal(f2.asSlice())); +} + +unittest // merge3_simple (без конфликтов) +{ + auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n"); + auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n"); + auto b = MMFile.fromBytes(cast(ubyte[])"header_changed\nline2\nline3\nline4\nhello world\n"); + + auto lines = appender!(string[])(); + auto rej = appender!(string[])(); + + int acceptCb(const(ubyte)[] s) nothrow + { + lines.put(cast(string) s.idup); + return 0; + } + int rejectCb(const(ubyte)[] s) nothrow + { + rej.put(cast(string) s.idup); + return 0; + } + + base.merge3Raw(a, b, &acceptCb, &rejectCb); + + auto expected = [ + "header_changed\n", + "line2\n", + "line3\n", + "line4\n", + "hello world changed\n", + ]; + assert(lines.data.equal(expected)); + assert(rej.data.length == 0); +} + +unittest // merge3_conflicts (есть rejected) +{ + auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n"); + auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n"); + auto b = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world also changed\n"); + + auto lines = appender!(string[])(); + auto rej = appender!(string[])(); + + int acceptCb(const(ubyte)[] s) nothrow { lines.put(cast(string)s.idup); return 0; } + int rejectCb(const(ubyte)[] s) nothrow { rej.put(cast(string)s.idup); return 0; } + + base.merge3Raw(a, b, &acceptCb, &rejectCb); + + assert(lines.data.length > 0); + assert(rej.data.length > 0); +} + +unittest // merge3_panic_* в Rust → у нас error-ветка через nothrow-коды: вернём -1 +{ + auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n"); + auto a = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n"); + auto b = MMFile.fromBytes(cast(ubyte[])"header_changed\nline2\nline3\nline4\nhello world\n"); + + auto lines = appender!(string[])(); + + int acceptCb(const(ubyte)[] s) nothrow + { + static size_t cnt = 0; + ++cnt; + if (cnt > 3) return -1; // сигнализируем об ошибке + lines.put(cast(string)s.idup); + return 0; + } + int rejectCb(const(ubyte)[] s) nothrow { return 0; } + + assertThrown!Exception( base.merge3Raw(a, b, &acceptCb, &rejectCb) ); +} + +unittest // patch_simple (computePatch→applyPatch) +{ + auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n"); + auto want = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world changed\n"); + + auto patch = base.computePatch(want); + auto res = base.applyPatch(patch); + + assert(res.success); + assert(sameBytes(res.patched, want)); + assert(res.rejected.asSlice().length == 0); +} + +unittest // patch_reject (искажаем базу перед apply) +{ + auto base = MMFile.fromBytes(cast(ubyte[])"header\nline2\nline3\nline4\nhello world\n"); + auto want = MMFile.fromBytes(cast(ubyte[])"header changed\nline2\nline3\nline4\nhello world changed\n"); + + auto patch = base.computePatch(want); + + auto m = base.asSliceMut(); + m[0] = cast(ubyte)'b'; + + auto res = base.applyPatch(patch); + + assert(!res.success); + assert(res.rejected.asSlice().length > 0); +}