init
This commit is contained in:
commit
143f39005c
11 changed files with 734 additions and 0 deletions
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
.dub
|
||||
docs.json
|
||||
__dummy.html
|
||||
docs/
|
||||
/libxdiff
|
||||
libxdiff.so
|
||||
libxdiff.dylib
|
||||
libxdiff.dll
|
||||
libxdiff.a
|
||||
libxdiff.lib
|
||||
libxdiff-test-*
|
||||
*.exe
|
||||
*.pdb
|
||||
*.o
|
||||
*.obj
|
||||
*.lst
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"editor.insertSpaces": false,
|
||||
"editor.tabSize": 4,
|
||||
"editor.detectIndentation": false
|
||||
}
|
23
LICENSE
Normal file
23
LICENSE
Normal file
|
@ -0,0 +1,23 @@
|
|||
Boost Software License - Version 1.0 - August 17th, 2003
|
||||
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
obtaining a copy of the software and accompanying documentation covered by
|
||||
this license (the "Software") to use, reproduce, display, distribute,
|
||||
execute, and transmit the Software, and to prepare derivative works of the
|
||||
Software, and to permit third-parties to whom the Software is furnished to
|
||||
do so, all subject to the following:
|
||||
|
||||
The copyright notices in the Software and this entire statement, including
|
||||
the above license grant, this restriction and the following disclaimer,
|
||||
must be included in all copies of the Software, in whole or in part, and
|
||||
all derivative works of the Software, unless such copies or derivative
|
||||
works are solely in the form of machine-executable object code generated by
|
||||
a source language processor.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
39
README.md
Normal file
39
README.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
# libxdiff
|
||||
|
||||
A D wrapper around the C **libxdiff**: thin bindings ([xdiff.d](https://git.zhirov.kz/dlang/xdiff)) + a small OO layer (`MMFile`, `MMBlocks`) for diff/patch/merge with clear ownership.
|
||||
|
||||
## Features
|
||||
|
||||
* **MMFile** — compact in-memory file (single buffer).
|
||||
* **MMBlocks** — native libxdiff block chain with compaction.
|
||||
* `computePatch`, `applyPatch` (returns accepted + rejected parts).
|
||||
* `merge3Raw` (three-way merge) with `nothrow` callbacks.
|
||||
* Deep copies where needed, explicit move of ownership, no double-free traps.
|
||||
|
||||
## Installation
|
||||
|
||||
Add to `dub.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"libxdiff": "~>0.1.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or to `dub.sdl`:
|
||||
|
||||
```
|
||||
dependency "libxdiff" version="~>0.1.0"
|
||||
```
|
||||
|
||||
Run `dub build`.
|
||||
|
||||
## Usage
|
||||
|
||||
Import the module: `import libxdiff;`
|
||||
|
||||
## License
|
||||
|
||||
Boost Software License 1.0. See [LICENSE](LICENSE).
|
28
dub.json
Normal file
28
dub.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"authors": [
|
||||
"Alexander Zhirov"
|
||||
],
|
||||
"copyright": "Copyright © 2025, Alexander Zhirov",
|
||||
"description": "An OO wrapper over xdiff.d (thin libxdiff bindings) with safe ownership and a simple diff/patch/merge API.",
|
||||
"license": "BSL-1.0",
|
||||
"name": "libxdiff",
|
||||
"targetType": "library",
|
||||
"dependencies": {
|
||||
"xdiff": {
|
||||
"repository": "git+https://git.zhirov.kz/dlang/xdiff.git",
|
||||
"version": "e2396bc172eba813cdcd1a96c494e35d687f576a"
|
||||
}
|
||||
},
|
||||
"buildTypes": {
|
||||
"unittest": {
|
||||
"dflags": [
|
||||
"-unittest",
|
||||
"-g"
|
||||
],
|
||||
"libs": [
|
||||
"xdiff"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
}
|
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"}
|
||||
}
|
||||
}
|
44
source/libxdiff/init.d
Normal file
44
source/libxdiff/init.d
Normal 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
117
source/libxdiff/mmblocks.d
Normal 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
199
source/libxdiff/mmfile.d
Normal 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;
|
||||
}
|
||||
}
|
4
source/libxdiff/package.d
Normal file
4
source/libxdiff/package.d
Normal file
|
@ -0,0 +1,4 @@
|
|||
module libxdiff;
|
||||
|
||||
public import libxdiff.mmblocks;
|
||||
public import libxdiff.mmfile;
|
253
source/libxdiff/unittests.d
Normal file
253
source/libxdiff/unittests.d
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue