This commit is contained in:
Alexander Zhirov 2025-08-31 18:42:22 +03:00
commit 5fe468a947
Signed by: alexander
GPG key ID: C8D8BE544A27C511
10 changed files with 730 additions and 0 deletions

44
source/libxdiff/init.d Normal file
View file

@ -0,0 +1,44 @@
module libxdiff.init;
import core.stdc.stdlib : malloc, free, realloc;
import std.exception : enforce;
import xdiff;
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;
}

117
source/libxdiff/mmblocks.d Normal file
View file

@ -0,0 +1,117 @@
module libxdiff.mmblocks;
import std.exception : enforce;
import libxdiff.init : ensureInit, initMmfile;
import xdiff;
final class MMBlocks
{
private:
mmfile_t _inner;
bool _owned = true;
enum DEFAULT_BSIZE = 8 * 1024;
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* 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(cast(mmfile_t*)&_inner, dst);
return dst;
}
void toCompact()
{
if (isCompact())
return;
auto dst = new MMBlocks;
deepCopyAll(&_inner, dst);
if (_owned && _inner.head !is null)
xdl_free_mmfile(&_inner);
_inner = *dst.mmfilePtr();
dst._owned = false;
dst._inner.head = null;
dst._inner.tail = null;
dst._inner.rcur = null;
dst._inner.wcur = null;
dst._inner.fsize = 0;
dst._inner.bsize = 0;
}
void disarm()
{
_owned = false;
_inner.head = null;
_inner.tail = null;
_inner.rcur = null;
_inner.wcur = null;
_inner.fsize = 0;
_inner.bsize = 0;
}
}

199
source/libxdiff/mmfile.d Normal file
View file

@ -0,0 +1,199 @@
module libxdiff.mmfile;
import std.exception : enforce;
import libxdiff.init : ensureInit, initMmfile;
import libxdiff.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;
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;
}
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);
}
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");
}
static MMFile fromBlocksMoved(MMBlocks b)
{
b.toCompact();
auto f = new MMFile;
f._inner = *b.mmfilePtr();
b.disarm();
return f;
}
}

253
source/libxdiff/unittests.d Normal file
View file

@ -0,0 +1,253 @@
module libxdiff.unittests;
import std.algorithm : equal;
import std.array : appender;
import std.exception : assertThrown;
import std.range : iota, cycle, take;
import libxdiff.mmfile : MMFile;
import libxdiff.mmblocks : MMBlocks;
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;
}
ubyte[] blocksToBytes(const MMBlocks b)
{
auto tmp = b.clone();
auto mf = MMFile.fromBlocksMoved(tmp);
return (cast(const(ubyte)[]) mf.asSlice()).dup;
}
bool sameBytes(const MMFile a, const MMFile b)
{
return a.asSlice().equal(b.asSlice());
}
unittest
{
auto f = new MMFile;
assert(f.size() == 0);
assert(f.isCompact());
}
unittest
{
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
{
auto data = genCycled(15_000);
auto f = MMFile.fromBytes(data);
assert(f.size() == data.length);
assert(f.isCompact());
assert(f.asSlice().equal(data));
}
unittest
{
auto data = genCycled(15_000);
auto f = MMFile.fromBytes(data);
auto f2 = MMFile.fromBytes(f.asSlice().dup);
assert(sameBytes(f, f2));
}
unittest
{
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
{
auto data = genCycled(15_000);
auto f = MMFile.fromBytes(data);
auto f2 = MMFile.fromBytes(data.dup);
assert(sameBytes(f, f2));
}
unittest
{
assert((new MMFile).asSlice().length == 0);
auto data = genCycled(15_000);
auto f = MMFile.fromBytes(data);
assert(f.asSlice().equal(data));
}
unittest
{
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
{
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
{
auto f = MMFile.fromBytes(cast(ubyte[]) "hello world\n");
auto f2 = MMFile.fromBytes(cast(ubyte[]) "hello world!\n");
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';
auto p2 = f.computePatch(f2);
auto r2 = f.applyPatch(p2);
assert(r2.success);
assert(r2.patched.asSlice().equal(f2.asSlice()));
}
unittest
{
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
{
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
{
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
{
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
{
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);
}