Добавлены комментарии
This commit is contained in:
parent
06b2a2a993
commit
32203fadc0
1 changed files with 246 additions and 140 deletions
168
source/diff.d
168
source/diff.d
|
@ -1,67 +1,114 @@
|
||||||
|
/* ============================================================
|
||||||
|
Модуль: 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)
|
||||||
{
|
{
|
||||||
|
// Проксируем к системному malloc
|
||||||
return malloc(size);
|
return malloc(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
private extern (C) void _wrap_free(void*, void* ptr)
|
private extern (C) void _wrap_free(void*, void* ptr)
|
||||||
{
|
{
|
||||||
|
// Проксируем к системному free
|
||||||
free(ptr);
|
free(ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
private extern (C) void* _wrap_realloc(void*, void* ptr, uint size)
|
private extern (C) void* _wrap_realloc(void*, void* ptr, uint size)
|
||||||
{
|
{
|
||||||
|
// Проксируем к системному realloc
|
||||||
return realloc(ptr, size);
|
return realloc(ptr, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Глобальный флаг «аллокатор уже установлен». __gshared — на уровне процесса.
|
||||||
private __gshared bool _allocatorReady = false;
|
private __gshared bool _allocatorReady = false;
|
||||||
|
|
||||||
private void ensureAllocator()
|
private void ensureAllocator()
|
||||||
{
|
{
|
||||||
|
// Важно: вызывать до любых операций с mmfile_t
|
||||||
if (_allocatorReady)
|
if (_allocatorReady)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
memallocator_t malt;
|
memallocator_t malt;
|
||||||
malt.priv = null;
|
malt.priv = null;
|
||||||
malt.malloc = &_wrap_malloc;
|
malt.malloc = &_wrap_malloc;
|
||||||
malt.free = &_wrap_free;
|
malt.free = &_wrap_free;
|
||||||
malt.realloc = &_wrap_realloc;
|
malt.realloc = &_wrap_realloc;
|
||||||
|
|
||||||
|
// Если вернуть не 0 — значит libxdiff отверг конфигурацию
|
||||||
enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed");
|
enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed");
|
||||||
_allocatorReady = true;
|
_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
|
||||||
|
ensureAllocator();
|
||||||
|
|
||||||
MmFile m;
|
MmFile m;
|
||||||
|
// Инициализируем внутренние структуры mmfile
|
||||||
enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed");
|
enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed");
|
||||||
|
|
||||||
|
// Копируем полезные данные в непрерывный блок, выделенный libxdiff
|
||||||
if (data.length)
|
if (data.length)
|
||||||
{
|
{
|
||||||
auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, 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");
|
enforce(dst !is null, "xdl_mmfile_writeallocate failed");
|
||||||
memcpy(dst, data.ptr, data.length);
|
memcpy(dst, data.ptr, data.length);
|
||||||
}
|
}
|
||||||
|
@ -70,15 +117,21 @@ private struct MmFile
|
||||||
|
|
||||||
~this()
|
~this()
|
||||||
{
|
{
|
||||||
|
// Освобождаем ресурсы mmfile_t (всегда вызывается, даже при исключениях)
|
||||||
xdl_free_mmfile(&mf);
|
xdl_free_mmfile(&mf);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//-----------------------------
|
/* ============================================================
|
||||||
// Синк-коллектор вывода libxdiff
|
Синк-коллектор вывода libxdiff
|
||||||
//-----------------------------
|
------------------------------------------------------------
|
||||||
|
libxdiff печатает результат через C-callback (outf). Мы даём
|
||||||
|
свой writeCB(), который складывает куски в Appender!(ubyte[]).
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
private class BufferSink
|
private class BufferSink
|
||||||
{
|
{
|
||||||
|
// Appender — дешёвая обёртка над динамическим массивом
|
||||||
Appender!(ubyte[]) outbuf;
|
Appender!(ubyte[]) outbuf;
|
||||||
|
|
||||||
this()
|
this()
|
||||||
|
@ -86,6 +139,7 @@ private class BufferSink
|
||||||
outbuf = appender!(ubyte[])();
|
outbuf = appender!(ubyte[])();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сигнатура коллбэка задана libxdiff: priv — наш контекст, mb — массив буферов
|
||||||
extern (C) static int writeCB(void* priv, mmbuffer_t* mb, int nbuf)
|
extern (C) static int writeCB(void* priv, mmbuffer_t* mb, int nbuf)
|
||||||
{
|
{
|
||||||
auto self = cast(BufferSink) priv;
|
auto self = cast(BufferSink) priv;
|
||||||
|
@ -94,24 +148,36 @@ private class BufferSink
|
||||||
size_t sz = cast(size_t) mb[i].size;
|
size_t sz = cast(size_t) mb[i].size;
|
||||||
if (sz == 0)
|
if (sz == 0)
|
||||||
continue;
|
continue;
|
||||||
|
// mmbuffer_t.ptr — char*; приводим к ubyte* и аппендим
|
||||||
self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]);
|
self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0; // 0 — успех
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Возвращает независимую копию накопленных байт в виде string (UTF-8 предполагается)
|
||||||
string toStringOwned() @trusted
|
string toStringOwned() @trusted
|
||||||
{
|
{
|
||||||
|
// @trusted: мы знаем, что libxdiff генерирует ASCII/UTF-8
|
||||||
return cast(string) outbuf.data.idup;
|
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)
|
||||||
{
|
{
|
||||||
|
@ -119,25 +185,28 @@ final class DiffEngine
|
||||||
_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")
|
||||||
{
|
{
|
||||||
|
// representation — дешёвый способ получить «сырые» байты из string
|
||||||
const(ubyte)[] a = _prep(oldText);
|
const(ubyte)[] a = _prep(oldText);
|
||||||
const(ubyte)[] b = _prep(newText);
|
const(ubyte)[] b = _prep(newText);
|
||||||
return diffBytes(a, b, oldLabel, newLabel);
|
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 a = cast(const(ubyte)[]) read(oldPath);
|
||||||
auto b = cast(const(ubyte)[]) read(newPath);
|
auto b = cast(const(ubyte)[]) read(newPath);
|
||||||
|
|
||||||
|
// Если метки не заданы — используем сами пути
|
||||||
if (oldLabel.length == 0)
|
if (oldLabel.length == 0)
|
||||||
oldLabel = oldPath;
|
oldLabel = oldPath;
|
||||||
if (newLabel.length == 0)
|
if (newLabel.length == 0)
|
||||||
|
@ -146,45 +215,60 @@ final class DiffEngine
|
||||||
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")
|
||||||
{
|
{
|
||||||
|
// 1) Готовим источники для libxdiff
|
||||||
auto a = MmFile.fromBytes(oldBytes);
|
auto a = MmFile.fromBytes(oldBytes);
|
||||||
auto b = MmFile.fromBytes(newBytes);
|
auto b = MmFile.fromBytes(newBytes);
|
||||||
|
|
||||||
|
// 2) Готовим приёмник вывода: callback + приватный контекст
|
||||||
auto sink = new BufferSink;
|
auto sink = new BufferSink;
|
||||||
xdemitcb_t ecb;
|
xdemitcb_t ecb;
|
||||||
ecb.priv = cast(void*) sink;
|
ecb.priv = cast(void*) sink; // будет возвращено в writeCB через priv
|
||||||
ecb.outf = &BufferSink.writeCB;
|
ecb.outf = &BufferSink.writeCB;
|
||||||
|
|
||||||
|
// 3) Параметры xdiff
|
||||||
xpparam_t xpp;
|
xpparam_t xpp;
|
||||||
xpp.flags = 0;
|
xpp.flags = 0; /* здесь можно задать доп. флаги парсера:
|
||||||
|
- XDF_* / XDF_INDENT_HEURISTIC и т.п. (см. xdiff.h)
|
||||||
|
- игнор пробелов/табов и др., если поддерживается */
|
||||||
|
|
||||||
xdemitconf_t xecfg;
|
xdemitconf_t xecfg;
|
||||||
xecfg.ctxlen = _ctxlen;
|
xecfg.ctxlen = _ctxlen; // длина контекста в unified-diff
|
||||||
|
|
||||||
|
// 4) Формируем заголовки unified-diff (libxdiff сам их не печатает)
|
||||||
auto pre = formatHeaders(oldLabel, newLabel);
|
auto pre = formatHeaders(oldLabel, newLabel);
|
||||||
|
|
||||||
|
// 5) Запускаем сравнение — результат «потечёт» в writeCB -> sink.outbuf
|
||||||
auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb);
|
auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb);
|
||||||
enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc));
|
enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc));
|
||||||
|
|
||||||
|
// 6) Склеиваем заголовок и «тело» патча, отдаём как одну строку
|
||||||
return pre ~ sink.toStringOwned();
|
return pre ~ sink.toStringOwned();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Заголовки unified-diff в стиле `--- LABEL \t UTC` / `+++ LABEL \t UTC`
|
||||||
static string formatHeaders(string oldLabel, string newLabel)
|
static string formatHeaders(string oldLabel, string newLabel)
|
||||||
{
|
{
|
||||||
|
// Ставим ISO-подобный штамп в UTC, чтобы было стабильно и однозначно
|
||||||
auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS
|
auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS
|
||||||
return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n"
|
return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n"
|
||||||
~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n";
|
~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Подготовка входной строки к подаче в MmFile: по желанию добавляем завершающий '\n'
|
||||||
const(ubyte)[] _prep(string s) @trusted
|
const(ubyte)[] _prep(string s) @trusted
|
||||||
{
|
{
|
||||||
|
// Быстрый доступ к байтам строки (без аллокаций/копий)
|
||||||
auto bytes = cast(const(ubyte)[]) s.representation;
|
auto bytes = cast(const(ubyte)[]) s.representation;
|
||||||
|
|
||||||
|
// Если включено «сглаживание» и у строки нет завершающего '\n' — добавим его
|
||||||
if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n'))
|
if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n'))
|
||||||
{
|
{
|
||||||
|
// Собираем ubyte[] на 1 байт длиннее, копируем исходные данные и дописываем '\n'
|
||||||
ubyte[] tmp;
|
ubyte[] tmp;
|
||||||
tmp.length = bytes.length + 1;
|
tmp.length = bytes.length + 1;
|
||||||
if (bytes.length)
|
if (bytes.length)
|
||||||
|
@ -195,3 +279,25 @@ private:
|
||||||
return bytes;
|
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, если модуль будет
|
||||||
|
вызываться конкурентно из нескольких потоков на старте.
|
||||||
|
============================================================ */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue