from rust

This commit is contained in:
Alexander Zhirov 2025-08-31 16:47:55 +03:00
parent 32203fadc0
commit 1f00cff032
Signed by: alexander
GPG key ID: C8D8BE544A27C511
8 changed files with 598 additions and 323 deletions

View file

@ -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"
]
}
}
}

6
dub.selections.json Normal file
View file

@ -0,0 +1,6 @@
{
"fileVersion": 1,
"versions": {
"xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"}
}
}

View file

@ -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();
}

View file

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

33
source/xdiff_init.d Normal file
View file

@ -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;
}

108
source/xdiff_mmblocks.d Normal file
View file

@ -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;
}
}

166
source/xdiff_mmfile.d Normal file
View file

@ -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;
}
}

246
source/xdiff_unittests.d Normal file
View file

@ -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);
}