dxdiff/source/diff.d

303 lines
14 KiB
D
Raw 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.

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