from rust
This commit is contained in:
		
							parent
							
								
									32203fadc0
								
							
						
					
					
						commit
						ef491965f6
					
				
					 4 changed files with 21 additions and 326 deletions
				
			
		
							
								
								
									
										6
									
								
								dub.json
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								dub.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -12,9 +12,9 @@
 | 
			
		|||
		"xdiff"
 | 
			
		||||
	],
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"xdiff": {
 | 
			
		||||
			"repository": "git+https://git.zhirov.kz/dlang/xdiff.git",
 | 
			
		||||
			"version": "e2396bc172eba813cdcd1a96c494e35d687f576a"
 | 
			
		||||
		"libxdiff": {
 | 
			
		||||
			"repository": "git+https://git.zhirov.kz/dlang/libxdiff.git",
 | 
			
		||||
			"version": "143f39005c0e89cc57d564365e6ea61427928bc3"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										6
									
								
								dub.selections.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								dub.selections.json
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
{
 | 
			
		||||
	"fileVersion": 1,
 | 
			
		||||
	"versions": {
 | 
			
		||||
		"libxdiff": {"version":"143f39005c0e89cc57d564365e6ea61427928bc3","repository":"git+https://git.zhirov.kz/dlang/libxdiff.git"}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										32
									
								
								source/app.d
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								source/app.d
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,25 +1,17 @@
 | 
			
		|||
import std.stdio : writeln, stderr;
 | 
			
		||||
import std.exception : collectException;
 | 
			
		||||
import std.stdio;
 | 
			
		||||
import libxdiff : MMFile;
 | 
			
		||||
 | 
			
		||||
import diff;
 | 
			
		||||
 | 
			
		||||
int main(string[] args)
 | 
			
		||||
void main()
 | 
			
		||||
{
 | 
			
		||||
	if (args.length != 3)
 | 
			
		||||
	{
 | 
			
		||||
		stderr.writeln("use: ", args[0], " OLD NEW");
 | 
			
		||||
		return 1;
 | 
			
		||||
	}
 | 
			
		||||
	auto a = MMFile.fromBytes(cast(ubyte[]) "hello world\n");
 | 
			
		||||
	auto b = MMFile.fromBytes(cast(ubyte[]) "hello world!\n");
 | 
			
		||||
 | 
			
		||||
	auto eng = new DiffEngine(3);
 | 
			
		||||
	auto patch = a.computePatch(b);
 | 
			
		||||
	writeln("patch size: ", patch.size());
 | 
			
		||||
 | 
			
		||||
	string result; // сюда положим результат
 | 
			
		||||
	auto ex = collectException(result = eng.diffFiles(args[1], args[2]));
 | 
			
		||||
	if (ex !is null)
 | 
			
		||||
	{
 | 
			
		||||
		stderr.writeln(ex.msg);
 | 
			
		||||
		return 2;
 | 
			
		||||
	}
 | 
			
		||||
	writeln(result);
 | 
			
		||||
	return 0;
 | 
			
		||||
	auto res = a.applyPatch(patch);
 | 
			
		||||
	writeln("apply success: ", res.success);
 | 
			
		||||
	writeln(res.patched.asSlice()); // печатаем результат
 | 
			
		||||
	// печатаем как есть (включая заголовок @@ и строки с '-'/'+' и '\n')
 | 
			
		||||
	write(cast(string) MMFile.fromBlocksMoved(patch).asSlice());
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										303
									
								
								source/diff.d
									
										
									
									
									
								
							
							
						
						
									
										303
									
								
								source/diff.d
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,303 +0,0 @@
 | 
			
		|||
/* ============================================================
 | 
			
		||||
    Модуль: diff
 | 
			
		||||
    Назначение: тонкая ООП-обёртка над libxdiff для вычисления unified-diff
 | 
			
		||||
    Вход: строки/пути/массивы байт
 | 
			
		||||
    Выход: единая строка с патчем (включая заголовки ---/+++)
 | 
			
		||||
 | 
			
		||||
    Краткая схема работы:
 | 
			
		||||
      - Готовим два mmfile_t (формат libxdiff) из входных байтов.
 | 
			
		||||
      - Настраиваем callback, который будет собирать текст диффа в буфер.
 | 
			
		||||
      - Вызываем xdl_diff(..), который сравнивает и «льёт» вывод через callback.
 | 
			
		||||
      - Склеиваем заголовки (---/+++) и «тело» патча, отдаём как string.
 | 
			
		||||
 | 
			
		||||
    Замечания:
 | 
			
		||||
      - Всё в памяти: входы читаются целиком, результат накапливается целиком.
 | 
			
		||||
      - Для бинарного сравнения лучше использовать bdiff/rabdiff/bpatch (можно добавить).
 | 
			
		||||
      - Заголовки формируются в этом модуле (libxdiff их сам не печатает).
 | 
			
		||||
      - При необходимости можно ввести нормализацию CRLF/пробелов флагами xpp/xecfg.
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
 | 
			
		||||
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; // копирование в буферы libxdiff
 | 
			
		||||
import core.stdc.stdlib : malloc, free, realloc; // системный аллокатор для прокидывания в xdiff
 | 
			
		||||
 | 
			
		||||
import xdiff; // C-API libxdiff (xdiff.h)
 | 
			
		||||
 | 
			
		||||
/* ============================================================
 | 
			
		||||
   Аллокатор для libxdiff
 | 
			
		||||
   ------------------------------------------------------------
 | 
			
		||||
   На ряде сборок libxdiff ожидает, что клиент задаст функции
 | 
			
		||||
   выделения памяти через xdl_set_allocator(). Если этого не
 | 
			
		||||
   сделать, xdl_mmfile_writeallocate() может возвращать null.
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
 | 
			
		||||
private extern (C) void* _wrap_malloc(void*, uint size)
 | 
			
		||||
{
 | 
			
		||||
    // Проксируем к системному malloc
 | 
			
		||||
    return malloc(size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private extern (C) void _wrap_free(void*, void* ptr)
 | 
			
		||||
{
 | 
			
		||||
    // Проксируем к системному free
 | 
			
		||||
    free(ptr);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private extern (C) void* _wrap_realloc(void*, void* ptr, uint size)
 | 
			
		||||
{
 | 
			
		||||
    // Проксируем к системному realloc
 | 
			
		||||
    return realloc(ptr, size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Глобальный флаг «аллокатор уже установлен». __gshared — на уровне процесса.
 | 
			
		||||
private __gshared bool _allocatorReady = false;
 | 
			
		||||
 | 
			
		||||
private void ensureAllocator()
 | 
			
		||||
{
 | 
			
		||||
    // Важно: вызывать до любых операций с mmfile_t
 | 
			
		||||
    if (_allocatorReady)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    memallocator_t malt;
 | 
			
		||||
    malt.priv = null;
 | 
			
		||||
    malt.malloc = &_wrap_malloc;
 | 
			
		||||
    malt.free = &_wrap_free;
 | 
			
		||||
    malt.realloc = &_wrap_realloc;
 | 
			
		||||
 | 
			
		||||
    // Если вернуть не 0 — значит libxdiff отверг конфигурацию
 | 
			
		||||
    enforce(xdl_set_allocator(&malt) == 0, "xdl_set_allocator failed");
 | 
			
		||||
    _allocatorReady = true;
 | 
			
		||||
 | 
			
		||||
    /* Потокобезопасность:
 | 
			
		||||
       Это «ленивая» инициализация без синхронизации. В многопоточной среде
 | 
			
		||||
       второй параллельный вызов может повторно вызвать xdl_set_allocator(),
 | 
			
		||||
       что обычно безвредно. Если нужна строгая гарантия — оберните это место
 | 
			
		||||
       мьютексом/atomic-флагом. */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ============================================================
 | 
			
		||||
   RAII для mmfile_t
 | 
			
		||||
   ------------------------------------------------------------
 | 
			
		||||
   Упрощает корректную инициализацию/очистку mmfile_t. Метод
 | 
			
		||||
   fromBytes() создаёт mmfile, выделяет буфер и копирует данные.
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
 | 
			
		||||
private struct MmFile
 | 
			
		||||
{
 | 
			
		||||
    mmfile_t mf; // «ручка» на содержимое в терминах libxdiff
 | 
			
		||||
 | 
			
		||||
    static MmFile fromBytes(const(ubyte)[] data,
 | 
			
		||||
        long bsize = 8 * 1024,
 | 
			
		||||
        ulong flags = XDL_MMF_ATOMIC)
 | 
			
		||||
    {
 | 
			
		||||
        // Критично: аллокатор должен быть настроен до init/writeallocate
 | 
			
		||||
        ensureAllocator();
 | 
			
		||||
 | 
			
		||||
        MmFile m;
 | 
			
		||||
        // Инициализируем внутренние структуры mmfile
 | 
			
		||||
        enforce(xdl_init_mmfile(&m.mf, bsize, flags) == 0, "xdl_init_mmfile failed");
 | 
			
		||||
 | 
			
		||||
        // Копируем полезные данные в непрерывный блок, выделенный libxdiff
 | 
			
		||||
        if (data.length)
 | 
			
		||||
        {
 | 
			
		||||
            auto dst = cast(char*) xdl_mmfile_writeallocate(&m.mf, data.length);
 | 
			
		||||
            // Если тут null — проверьте flags/bsize; попробуйте flags=0, bsize=32*1024
 | 
			
		||||
            enforce(dst !is null, "xdl_mmfile_writeallocate failed");
 | 
			
		||||
            memcpy(dst, data.ptr, data.length);
 | 
			
		||||
        }
 | 
			
		||||
        return m;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ~this()
 | 
			
		||||
    {
 | 
			
		||||
        // Освобождаем ресурсы mmfile_t (всегда вызывается, даже при исключениях)
 | 
			
		||||
        xdl_free_mmfile(&mf);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ============================================================
 | 
			
		||||
   Синк-коллектор вывода libxdiff
 | 
			
		||||
   ------------------------------------------------------------
 | 
			
		||||
   libxdiff печатает результат через C-callback (outf). Мы даём
 | 
			
		||||
   свой writeCB(), который складывает куски в Appender!(ubyte[]).
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
 | 
			
		||||
private class BufferSink
 | 
			
		||||
{
 | 
			
		||||
    // Appender — дешёвая обёртка над динамическим массивом
 | 
			
		||||
    Appender!(ubyte[]) outbuf;
 | 
			
		||||
 | 
			
		||||
    this()
 | 
			
		||||
    {
 | 
			
		||||
        outbuf = appender!(ubyte[])();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Сигнатура коллбэка задана libxdiff: priv — наш контекст, mb — массив буферов
 | 
			
		||||
    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;
 | 
			
		||||
            // mmbuffer_t.ptr — char*; приводим к ubyte* и аппендим
 | 
			
		||||
            self.outbuf.put((cast(ubyte*) mb[i].ptr)[0 .. sz]);
 | 
			
		||||
        }
 | 
			
		||||
        return 0; // 0 — успех
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Возвращает независимую копию накопленных байт в виде string (UTF-8 предполагается)
 | 
			
		||||
    string toStringOwned() @trusted
 | 
			
		||||
    {
 | 
			
		||||
        // @trusted: мы знаем, что libxdiff генерирует ASCII/UTF-8
 | 
			
		||||
        return cast(string) outbuf.data.idup;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ============================================================
 | 
			
		||||
   Публичный движок unified-diff
 | 
			
		||||
   ------------------------------------------------------------
 | 
			
		||||
   Основной API:
 | 
			
		||||
     - diffText: сравнить две строки
 | 
			
		||||
     - diffFiles: сравнить два файла (читаются бинарно)
 | 
			
		||||
     - diffBytes: сравнить два массива байт
 | 
			
		||||
   Параметры:
 | 
			
		||||
     - ctxlen: длина контекста (кол-во строк вокруг изменений)
 | 
			
		||||
     - stripTrailingNewline: при true добавляет '\n', если его нет
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
 | 
			
		||||
final class DiffEngine
 | 
			
		||||
{
 | 
			
		||||
    private long _ctxlen;                 // длина контекста unified-diff
 | 
			
		||||
    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")
 | 
			
		||||
    {
 | 
			
		||||
        // representation — дешёвый способ получить «сырые» байты из string
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
        // Бинарное чтение исключает «сюрпризы» перекодировок/CRLF
 | 
			
		||||
        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")
 | 
			
		||||
    {
 | 
			
		||||
        // 1) Готовим источники для libxdiff
 | 
			
		||||
        auto a = MmFile.fromBytes(oldBytes);
 | 
			
		||||
        auto b = MmFile.fromBytes(newBytes);
 | 
			
		||||
 | 
			
		||||
        // 2) Готовим приёмник вывода: callback + приватный контекст
 | 
			
		||||
        auto sink = new BufferSink;
 | 
			
		||||
        xdemitcb_t ecb;
 | 
			
		||||
        ecb.priv = cast(void*) sink; // будет возвращено в writeCB через priv
 | 
			
		||||
        ecb.outf = &BufferSink.writeCB;
 | 
			
		||||
 | 
			
		||||
        // 3) Параметры xdiff
 | 
			
		||||
        xpparam_t xpp;
 | 
			
		||||
        xpp.flags = 0;         /* здесь можно задать доп. флаги парсера:
 | 
			
		||||
                                  - XDF_* / XDF_INDENT_HEURISTIC и т.п. (см. xdiff.h)
 | 
			
		||||
                                  - игнор пробелов/табов и др., если поддерживается */
 | 
			
		||||
 | 
			
		||||
        xdemitconf_t xecfg;
 | 
			
		||||
        xecfg.ctxlen = _ctxlen; // длина контекста в unified-diff
 | 
			
		||||
 | 
			
		||||
        // 4) Формируем заголовки unified-diff (libxdiff сам их не печатает)
 | 
			
		||||
        auto pre = formatHeaders(oldLabel, newLabel);
 | 
			
		||||
 | 
			
		||||
        // 5) Запускаем сравнение — результат «потечёт» в writeCB -> sink.outbuf
 | 
			
		||||
        auto rc = xdl_diff(&a.mf, &b.mf, &xpp, &xecfg, &ecb);
 | 
			
		||||
        enforce(rc >= 0, format("xdl_diff failed (rc=%s)", rc));
 | 
			
		||||
 | 
			
		||||
        // 6) Склеиваем заголовок и «тело» патча, отдаём как одну строку
 | 
			
		||||
        return pre ~ sink.toStringOwned();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Заголовки unified-diff в стиле `--- LABEL \t UTC` / `+++ LABEL \t UTC`
 | 
			
		||||
    static string formatHeaders(string oldLabel, string newLabel)
 | 
			
		||||
    {
 | 
			
		||||
        // Ставим ISO-подобный штамп в UTC, чтобы было стабильно и однозначно
 | 
			
		||||
        auto ts = Clock.currTime(UTC()).toISOExtString(); // YYYY-MM-DDTHH:MM:SS
 | 
			
		||||
        return "--- " ~ oldLabel ~ "\t" ~ ts ~ "\n"
 | 
			
		||||
             ~ "+++ " ~ newLabel ~ "\t" ~ ts ~ "\n";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
    // Подготовка входной строки к подаче в MmFile: по желанию добавляем завершающий '\n'
 | 
			
		||||
    const(ubyte)[] _prep(string s) @trusted
 | 
			
		||||
    {
 | 
			
		||||
        // Быстрый доступ к байтам строки (без аллокаций/копий)
 | 
			
		||||
        auto bytes = cast(const(ubyte)[]) s.representation;
 | 
			
		||||
 | 
			
		||||
        // Если включено «сглаживание» и у строки нет завершающего '\n' — добавим его
 | 
			
		||||
        if (_stripTrailingNewline && (bytes.length && bytes[$ - 1] != '\n'))
 | 
			
		||||
        {
 | 
			
		||||
            // Собираем ubyte[] на 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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* ============================================================
 | 
			
		||||
   ИДЕИ ДЛЯ РАСШИРЕНИЯ (можно реализовать позже)
 | 
			
		||||
   ------------------------------------------------------------
 | 
			
		||||
   1) Флаги сравнения:
 | 
			
		||||
      - Пробросить наружу xpp.flags (игнор пробелов, эвристики и т.п.).
 | 
			
		||||
 | 
			
		||||
   2) Нормализация перевода строк:
 | 
			
		||||
      - Добавить опцию для унификации CRLF->LF на входе, чтобы не шуметь.
 | 
			
		||||
 | 
			
		||||
   3) Вывод напрямую в FD/файл:
 | 
			
		||||
      - Реализовать методы diffToFD(int fd)/diffToFile(string path),
 | 
			
		||||
        где ecb.outf = fdOut, аналогичный твоему CLI примеру.
 | 
			
		||||
 | 
			
		||||
   4) Бинарные режимы:
 | 
			
		||||
      - Обернуть xdl_bdiff/xdl_rabdiff/xdl_bpatch в том же стиле,
 | 
			
		||||
        если потребуется компактный бинарный патч.
 | 
			
		||||
 | 
			
		||||
   5) Потокобезопасность ensureAllocator():
 | 
			
		||||
      - Заменить «ленивый флаг» на mutex/atomic, если модуль будет
 | 
			
		||||
        вызываться конкурентно из нескольких потоков на старте.
 | 
			
		||||
   ============================================================ */
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue