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