Compare commits
2 commits
06b2a2a993
...
1f00cff032
Author | SHA1 | Date | |
---|---|---|---|
1f00cff032 | |||
32203fadc0 |
8 changed files with 598 additions and 217 deletions
28
dub.json
28
dub.json
|
@ -16,5 +16,33 @@
|
||||||
"repository": "git+https://git.zhirov.kz/dlang/xdiff.git",
|
"repository": "git+https://git.zhirov.kz/dlang/xdiff.git",
|
||||||
"version": "e2396bc172eba813cdcd1a96c494e35d687f576a"
|
"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
6
dub.selections.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"fileVersion": 1,
|
||||||
|
"versions": {
|
||||||
|
"xdiff": {"version":"e2396bc172eba813cdcd1a96c494e35d687f576a","repository":"git+https://git.zhirov.kz/dlang/xdiff.git"}
|
||||||
|
}
|
||||||
|
}
|
31
source/app.d
31
source/app.d
|
@ -1,25 +1,16 @@
|
||||||
import std.stdio : writeln, stderr;
|
import std.stdio;
|
||||||
import std.exception : collectException;
|
import xdiff_mmfile : MMFile;
|
||||||
|
|
||||||
import diff;
|
void main()
|
||||||
|
|
||||||
int main(string[] args)
|
|
||||||
{
|
{
|
||||||
if (args.length != 3)
|
auto a = MMFile.fromBytes(cast(ubyte[])"hello world\n");
|
||||||
{
|
auto b = MMFile.fromBytes(cast(ubyte[])"hello world!\n");
|
||||||
stderr.writeln("use: ", args[0], " OLD NEW");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto eng = new DiffEngine(3);
|
auto patch = a.computePatch(b);
|
||||||
|
writeln("patch size: ", patch.size());
|
||||||
|
|
||||||
string result; // сюда положим результат
|
auto res = a.applyPatch(patch);
|
||||||
auto ex = collectException(result = eng.diffFiles(args[1], args[2]));
|
writeln("apply success: ", res.success);
|
||||||
if (ex !is null)
|
write(res.patched.asSlice()); // печатаем результат
|
||||||
{
|
writeln();
|
||||||
stderr.writeln(ex.msg);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
writeln(result);
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
197
source/diff.d
197
source/diff.d
|
@ -1,197 +0,0 @@
|
||||||
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;
|
|
||||||
import core.stdc.stdlib : malloc, free, realloc;
|
|
||||||
|
|
||||||
import xdiff;
|
|
||||||
|
|
||||||
//-----------------------------
|
|
||||||
// Аллокатор для libxdiff (обязателен на ряде сборок)
|
|
||||||
//-----------------------------
|
|
||||||
private extern (C) void* _wrap_malloc(void*, uint size)
|
|
||||||
{
|
|
||||||
return malloc(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
private extern (C) void _wrap_free(void*, void* ptr)
|
|
||||||
{
|
|
||||||
free(ptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
private extern (C) void* _wrap_realloc(void*, void* ptr, uint size)
|
|
||||||
{
|
|
||||||
return realloc(ptr, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------
|
|
||||||
// RAII для mmfile_t
|
|
||||||
//-----------------------------
|
|
||||||
private struct MmFile
|
|
||||||
{
|
|
||||||
mmfile_t mf;
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
~this()
|
|
||||||
{
|
|
||||||
xdl_free_mmfile(&mf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------
|
|
||||||
// Синк-коллектор вывода libxdiff
|
|
||||||
//-----------------------------
|
|
||||||
private class BufferSink
|
|
||||||
{
|
|
||||||
Appender!(ubyte[]) outbuf;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
string toStringOwned() @trusted
|
|
||||||
{
|
|
||||||
return cast(string) outbuf.data.idup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//-----------------------------
|
|
||||||
// Публичный движок unified-diff
|
|
||||||
//-----------------------------
|
|
||||||
final class DiffEngine
|
|
||||||
{
|
|
||||||
private long _ctxlen;
|
|
||||||
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")
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
|
|
||||||
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")
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
xpparam_t xpp;
|
|
||||||
xpp.flags = 0;
|
|
||||||
|
|
||||||
xdemitconf_t xecfg;
|
|
||||||
xecfg.ctxlen = _ctxlen;
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
33
source/xdiff_init.d
Normal file
33
source/xdiff_init.d
Normal 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
108
source/xdiff_mmblocks.d
Normal 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
166
source/xdiff_mmfile.d
Normal 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
246
source/xdiff_unittests.d
Normal 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);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue