diff --git a/source/app.d b/source/app.d index 7b3a7ed..30e9f34 100644 --- a/source/app.d +++ b/source/app.d @@ -1,322 +1,25 @@ -import std.stdio : writeln, stderr, File; -import std.getopt; -import std.file : read, exists; -import std.exception : enforce, collectException; -import core.stdc.stdlib : malloc, free, realloc; -import core.stdc.string : memcpy; -import core.sys.posix.unistd : posix_write = write; +import std.stdio : writeln, stderr; +import std.exception : collectException; -import xdiff; - -// ------------------------------ -// RAII для mmfile_t -// ------------------------------ -struct MmFile -{ - mmfile_t mf; - - // фабрика вместо конструктора по умолчанию - static MmFile create(long bsize = 8 * 1024, ulong flags = XDL_MMF_ATOMIC) - { - MmFile m; - enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed"); - return m; - } - - void deinit() - { - xdl_free_mmfile(&mf); - } -} - -// ------------------------------ -// Коллбэки вывода -// ------------------------------ -extern (C) int mmfileOut(void* priv, mmbuffer_t* mb, int nbuf) -{ - auto mmfOut = cast(mmfile_t*) priv; - return xdl_writem_mmfile(mmfOut, mb, nbuf) < 0 ? -1 : 0; -} - -extern (C) int fdOut(void* priv, mmbuffer_t* mb, int nbuf) -{ - const fd = cast(int) cast(size_t) priv; // 1=stdout, 2=stderr - foreach (i; 0 .. nbuf) - { - auto ptr = mb[i].ptr; - auto left = cast(size_t) mb[i].size; - while (left) - { - auto n = posix_write(fd, ptr, left); - if (n < 0) - return -1; - ptr += n; - left -= cast(size_t) n; - } - } - return 0; -} - -// ------------------------------ -// Загрузка/выгрузка -// ------------------------------ -void loadMmFile(ref MmFile m, string path) -{ - try - { - enforce(exists(path), "no such file: " ~ path); - auto data = cast(const(char)[]) read(path); // читает весь файл - auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, data.length); - enforce(dst !is null, "xdl_mmfile_writeallocate failed (null)"); - if (data.length) - memcpy(dst, data.ptr, data.length); - } - catch (Exception e) - { - // пробрасываем с контекстом пути - throw new Exception("loadMmFile(" ~ path ~ "): " ~ e.msg); - } -} - -void dumpMmFile(const ref MmFile m, string path) -{ - File f = File(path, "wb"); - scope (exit) - f.close(); - long sz = 0; - auto blk = cast(char*) xdl_mmfile_first(cast(mmfile_t*)&m.mf, &sz); - while (blk !is null) - { - f.rawWrite(blk[0 .. cast(size_t) sz]); - blk = cast(char*) xdl_mmfile_next(cast(mmfile_t*)&m.mf, &sz); - } -} - -// ------------------------------ -// Аллокатор -// ------------------------------ -extern (C) void* wrap_malloc(void*, uint size) -{ - return malloc(size); -} - -extern (C) void wrap_free(void*, void* ptr) -{ - free(ptr); -} - -extern (C) void* wrap_realloc(void*, void* ptr, uint size) -{ - return realloc(ptr, size); -} - -void installAllocator() -{ - 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"); -} - -// ------------------------------ -// Команды -// ------------------------------ -int doDiff(ref MmFile a, ref MmFile b, long ctxlen) -{ - xpparam_t xpp; - xpp.flags = 0; - xdemitconf_t xecfg; - xecfg.ctxlen = ctxlen; - xdemitcb_t ecb; - ecb.priv = cast(void*) cast(size_t) 1; - ecb.outf = &fdOut; - return xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb); -} - -int doPatch(ref MmFile orig, ref MmFile patch, int mode) -{ - xdemitcb_t outcb; - outcb.priv = cast(void*) cast(size_t) 1; - outcb.outf = &fdOut; - xdemitcb_t rejcb; - rejcb.priv = cast(void*) cast(size_t) 2; - rejcb.outf = &fdOut; - return xdl_patch(&orig.mf, &patch.mf, mode, &outcb, &rejcb); -} - -int doBDiff(ref MmFile a, ref MmFile b, long bsize) -{ - bdiffparam_t bdp; - bdp.bsize = bsize; - xdemitcb_t ecb; - ecb.priv = cast(void*) cast(size_t) 1; - ecb.outf = &fdOut; - return xdl_bdiff(&a.mf, &b.mf, &bdp, &ecb); -} - -int doRaBDiff(ref MmFile a, ref MmFile b) -{ - xdemitcb_t ecb; - ecb.priv = cast(void*) cast(size_t) 1; - ecb.outf = &fdOut; - return xdl_rabdiff(&a.mf, &b.mf, &ecb); -} - -int doBPatch(ref MmFile base, ref MmFile binPatch) -{ - xdemitcb_t ecb; - ecb.priv = cast(void*) cast(size_t) 1; - ecb.outf = &fdOut; - return xdl_bpatch(&base.mf, &binPatch.mf, &ecb); -} - -// ------------------------------ -// CLI -// ------------------------------ -enum Cmd -{ - diff, - patch, - bdiff, - rabdiff, - bpatch -} - -private Cmd parseCmd(string s) -{ - final switch (s) - { - case "diff": - return Cmd.diff; - case "patch": - return Cmd.patch; - case "bdiff": - return Cmd.bdiff; - case "rabdiff": - return Cmd.rabdiff; - case "bpatch": - return Cmd.bpatch; - } -} - -/// Короткая справка -void printUsage(string prg) -{ - stderr.writefln( - "use: %1$s diff [-C N] FROM TO\n" ~ - " %1$s patch ORIG PATCH\n" ~ - " %1$s bdiff [-B N] FROM TO\n" ~ - " %1$s rabdiff FROM TO\n" ~ - " %1$s bpatch ORIG PATCH\n", - prg); -} - -/// Парсинг подкоманды + её опций. Возвращаем уже «очищенный» список позиционных. -struct Parsed -{ - Cmd cmd; - long ctxlen = 3; // для diff - long bsize = 16; // для bdiff - string[] positional; // оставшиеся позиционные (пути) -} - -Parsed parseArgs(string[] args) -{ - enforce(args.length >= 2, "no subcommand"); - auto sub = args[1]; - auto cmd = parseCmd(sub); - - long ctxlen = 3; - long bsize = 16; - bool helpWanted = false; - - // Опции и позиционные ТОЛЬКО после подкоманды - string[] optArgs = args.length > 2 ? args[2 .. $].dup : []; - - // getopt выкинет опции из optArgs и оставит только позиционные - auto r = getopt(optArgs, - "h|help", &helpWanted, - "C", &ctxlen, // актуально для `diff` - "B", &bsize // актуально для `bdiff` - - - - ); - enforce(!helpWanted, "help"); - - Parsed p; - p.cmd = cmd; - p.ctxlen = ctxlen; - p.bsize = bsize; - p.positional = optArgs; // здесь уже только пути - return p; -} +import diff; int main(string[] args) { - // 1) Парсим - Parsed p; - try + if (args.length != 3) { - p = parseArgs(args); - } - catch (Exception) - { - printUsage(args[0]); - return 1; - } - if (p.positional.length != 2) - { - // каждая подкоманда требует ровно 2 пути - printUsage(args[0]); - return 1; - } - auto p1 = p.positional[0]; - auto p2 = p.positional[1]; - - // 2) Аллокатор libxdiff - installAllocator(); - - // 3) Готовим входы - auto mf1 = MmFile.create(); - scope (exit) - mf1.deinit(); - auto mf2 = MmFile.create(); - scope (exit) - mf2.deinit(); - - try - { - loadMmFile(mf1, p1); - loadMmFile(mf2, p2); - } - catch (Exception e) - { - stderr.writeln(e.msg); + stderr.writeln("use: ", args[0], " OLD NEW"); return 1; } - // 4) Запускаем действие - int rc = 0; - final switch (p.cmd) + auto eng = new DiffEngine(3); + + string result; // сюда положим результат + auto ex = collectException(result = eng.diffFiles(args[1], args[2])); + if (ex !is null) { - case Cmd.diff: - rc = doDiff(mf1, mf2, p.ctxlen); - break; - case Cmd.patch: - rc = doPatch(mf1, mf2, XDL_PATCH_NORMAL); - break; - case Cmd.bdiff: - rc = doBDiff(mf1, mf2, p.bsize); - break; - case Cmd.rabdiff: - rc = doRaBDiff(mf1, mf2); - break; - case Cmd.bpatch: - rc = doBPatch(mf1, mf2); - break; + stderr.writeln(ex.msg); + return 2; } - return rc < 0 ? 2 : 0; + writeln(result); + return 0; } diff --git a/source/diff.d b/source/diff.d new file mode 100644 index 0000000..d3b1310 --- /dev/null +++ b/source/diff.d @@ -0,0 +1,197 @@ +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; + } +}