/* ============================================================ Модуль: 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, если модуль будет вызываться конкурентно из нескольких потоков на старте. ============================================================ */