// SPDX-FileCopyrightText: 2025 Romain Maneschi // // SPDX-License-Identifier: EUPL-1.2 package plugin import ( "bytes" "context" "crypto/rand" "errors" "fmt" "os" "path/filepath" "time" "unicode/utf16" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/protocol/packp" "github.com/samber/oops" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/assemblyscript" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/tetratelabs/wazero/sys" pluginLib "gitroot.dev/libs/golang/plugin/model" grfs "gitroot.dev/server/fs" "gitroot.dev/server/logger" "gitroot.dev/server/repository" "gitroot.dev/server/user" ) type runtimeInputsKind int const ( runtimeInputsKindDiff runtimeInputsKind = iota runtimeInputsKindWorktree runtimeInputsKind = iota ) type runtimeInputs struct { ctx context.Context repoName string kind runtimeInputsKind plugins []Plugin commands []CommandForDiff } type runtime struct { ctx context.Context manager *Manager logger *logger.Logger repo *repository.GitRootRepository repoWriter *repository.GitRootRepositoryWrite plugin Plugin pluginRun PluginRun command *CommandForDiff wazRun wazero.Runtime fsWorktree grfs.UpdatableFs fsWebcontent grfs.UpdatableFs fsCache grfs.UpdatableFs commitHook func(h plumbing.Hash) mergeHook func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string) } func newRuntime(ctx context.Context, manager *Manager, logger *logger.Logger) (*runtime, error) { r := &runtime{ ctx: ctx, manager: manager, logger: logger, wazRun: wazero.NewRuntime(ctx), fsWorktree: grfs.NewUpdatableFs(ctx, nil), fsWebcontent: grfs.NewUpdatableFs(ctx, nil), fsCache: grfs.NewUpdatableFs(ctx, nil), commitHook: nil, mergeHook: nil, } _, err := assemblyscript.Instantiate(ctx, r.wazRun) if err != nil { return nil, err } _, err = r.wazRun. NewHostModuleBuilder("gitroot"). NewFunctionBuilder().WithFunc(r.ForgeConf).Export("forgeConf"). NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(false)).Export("modifyContent"). NewFunctionBuilder().WithFunc(r.ModifyContentBuilder(true)).Export("modifyContentAS"). NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(false)).Export("modifyWebContent"). NewFunctionBuilder().WithFunc(r.ModifyWebContentBuilder(true)).Export("modifyWebContentAS"). NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(false)).Export("replaceWebContent"). NewFunctionBuilder().WithFunc(r.ReplaceWebContentBuilder(true)).Export("replaceWebContentAS"). NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(false)).Export("modifyCacheContent"). NewFunctionBuilder().WithFunc(r.ModifyCacheContentBuilder(true)).Export("modifyCacheContentAS"). NewFunctionBuilder().WithFunc(r.CommitAllBuilder(false)).Export("commitAll"). NewFunctionBuilder().WithFunc(r.CommitAllBuilder(true)).Export("commitAllAS"). NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(false)).Export("diffWithParent"). NewFunctionBuilder().WithFunc(r.DiffWithParentBuilder(true)).Export("diffWithParentAS"). NewFunctionBuilder().WithFunc(r.LogBuilder(false)).Export("log"). NewFunctionBuilder().WithFunc(r.LogBuilder(true)).Export("logAS"). NewFunctionBuilder().WithFunc(r.LogErrorBuilder(false)).Export("logError"). NewFunctionBuilder().WithFunc(r.LogErrorBuilder(true)).Export("logErrorAS"). NewFunctionBuilder().WithFunc(r.MergeBuilder(false)).Export("merge"). NewFunctionBuilder().WithFunc(r.MergeBuilder(true)).Export("mergeAS"). NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(false)).Export("commits"). NewFunctionBuilder().WithFunc(r.GetCommitsBuilder(true)).Export("commitsAS"). Instantiate(ctx) if err != nil { return nil, err } _, err = wasi_snapshot_preview1.Instantiate(ctx, r.wazRun) if err != nil { return nil, err } return r, nil } func (r *runtime) listen(c chan runtimeInputs) { for i := range c { r.logger.Debug("listen call", logger.NewLoggerPair("repo", i.repoName), logger.NewLoggerPair("isDiff", i.kind == runtimeInputsKindDiff), logger.NewLoggerPair("isWorktree", i.kind == runtimeInputsKindWorktree)) repo, err := r.manager.repoManager.Open(logger.AddCaller(r.ctx, "runtime.listen"), i.repoName) if err != nil { r.logger.Error("open error in listen", err) repo.Close() continue } repoWriter, err := repo.WillWrite(plumbing.HEAD) //TODO should mount good branc directly inside i.commands if err != nil { r.logger.Error("will write error in listen", err, logger.NewLoggerPair("repo", repo.Name())) repo.Close() continue } if i.kind == runtimeInputsKindDiff { timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins", repo.Name())) err := r.start(i.ctx, repo, repoWriter, i.plugins, i.commands, func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string) { if cmd != nil { go func() { time.Sleep(10 * time.Millisecond) //TODO find better to defer it until repo.close() is called (to be sur transactional writer has writen in disk) r.manager.backgroundManager.DeleteBranch(i.repoName, toDeleteBranchName) r.manager.backgroundManager.PostPush(pusher, i.repoName, []*packp.Command{cmd}) }() } }) if err != nil { r.logger.Error("start error", err) } timerStop() } else if i.kind == runtimeInputsKindWorktree { r.logger.Info("start worktree", logger.NewLoggerPair("repo", repo.Name())) timerStop := r.logger.Time(fmt.Sprintf("Timer %s all plugins worktree", repo.Name())) if err := r.worktree(i.ctx, repo, repoWriter, i.plugins, i.commands); err != nil { r.logger.Error("start error", err) } timerStop() } repo.Close() } } func (r *runtime) conf(ctx context.Context, plugin Plugin) ([]pluginLib.PluginRun, error) { r.fsWorktree.Clear() r.fsWebcontent.Clear() r.fsCache.Clear() r.repo = nil r.plugin = plugin r.command = nil timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name)) r.logger.Debug("start plugin conf", logger.NewLoggerPair("name", plugin.Name)) m := r.wazRun.Module(plugin.uuid()) if m == nil { r.logger.Debug("instantiate plugin conf", logger.NewLoggerPair("name", plugin.Name)) config := wazero.NewModuleConfig(). WithStdout(os.Stdout).WithStderr(os.Stderr). WithSysWalltime(). WithSysNanotime(). WithRandSource(rand.Reader). WithFSConfig( wazero.NewFSConfig(). WithFSMount(r.fsWorktree, "worktree"). WithFSMount(r.fsWebcontent, "webcontent"). WithFSMount(r.fsCache, "cache")) mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.uuid()).WithStartFunctions("_initialize", "install")) if err != nil { if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode()) return nil, err } else if !ok { return nil, err } } m = mod } else { r.logger.Debug("already exist conf", logger.NewLoggerPair("name", plugin.Name)) } conf, err := r.callPluginForConf(ctx, m, r.logger.NewSubLogger(plugin.Name)) if err != nil { r.logger.Error("finish plugin conf with error", err, logger.NewLoggerPair("name", plugin.Name)) } r.logger.Debug("finish plugin conf", logger.NewLoggerPair("name", plugin.Name)) timerStop() return conf, err } type runAfter struct { cmd *packp.Command pusher user.SimpleUser } func (r *runtime) start(ctx context.Context, repo *repository.GitRootRepository, repoWriter *repository.GitRootRepositoryWrite, plugins []Plugin, commands []CommandForDiff, mergeHook func(cmd *packp.Command, pusher user.SimpleUser, toDeleteBranchName string)) error { r.fsWorktree.Update(repoWriter.ToFs(ctx)) r.fsWebcontent.Update(r.manager.conf.DataWeb(repo.Name())) r.repo = repo r.repoWriter = repoWriter r.command = nil r.commitHook = func(hash plumbing.Hash) { command, err := repoWriter.GetLastCommit(hash) if err != nil { r.logger.Error("can't GetLastCommit in start runtime", err) return } newCmd, err := CommandForDiffFromCommitCmd(ctx, r.plugin.commiter.SimpleUser, command, r.command.branch) if err != nil { r.logger.Error("can't CommandForDiffFromCommitCmd in start runtime", err) return } commands = append(commands, newCmd) } r.mergeHook = mergeHook defer func() { r.commitHook = nil r.mergeHook = nil }() for _, plugin := range plugins { r.fsCache.Update(r.manager.conf.Cache(repo.Name(), plugin.Name)) r.plugin = plugin timerStop := r.logger.Time(fmt.Sprintf("Timer %s", plugin.Name)) r.logger.Debug("start plugin", logger.NewLoggerPair("repo", repo.Name()), logger.NewLoggerPair("name", plugin.Name)) m := r.wazRun.Module(plugin.uuid()) if m == nil { r.logger.Debug("instantiate plugin", logger.NewLoggerPair("name", plugin.Name)) config := wazero.NewModuleConfig(). WithSysWalltime(). WithSysNanotime(). WithRandSource(rand.Reader). WithStdout(os.Stdout).WithStderr(os.Stderr). WithFSConfig(wazero.NewFSConfig(). WithFSMount(r.fsWorktree, "worktree"). WithFSMount(r.fsWebcontent, "webcontent"). WithFSMount(r.fsCache, "cache")) mod, err := r.wazRun.InstantiateWithConfig(ctx, plugin.content, config.WithName(plugin.uuid()).WithStartFunctions("_initialize", "install")) if err != nil { if exitErr, ok := err.(*sys.ExitError); ok && exitErr.ExitCode() != 0 { fmt.Fprintf(os.Stderr, "exit_code: %d\n", exitErr.ExitCode()) return nil } else if !ok { return err } } m = mod } else { r.logger.Debug("already exist", logger.NewLoggerPair("name", plugin.Name)) } l := logger.NewLogger(logger.WASM) l.Debug("memory before", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name)) cp := callPlugin{ manager: r.manager, plugin: plugin, repo: repo, repoWriter: repoWriter, module: m, logger: r.logger.NewSubLogger(plugin.Name), } if err := cp.callPluginForDiff(ctx, r, commands); err != nil { r.logger.Error("finish plugin with error", err, logger.NewLoggerPair("name", plugin.Name)) } l.Debug("memory after", logger.NewLoggerPair("size", m.Memory().Size()), logger.NewLoggerPair("plugin", plugin.Name)) r.logger.Debug("finish plugin", logger.NewLoggerPair("name", plugin.Name)) timerStop() } return nil } func (r *runtime) ForgeConf(_ context.Context, m api.Module) uint64 { // TODO need issues/a716-conf-external-addr.md to work _, ptrSize, err := r.sendData(m, "{\"domain\": \"\"}") //TODO free ptr (first arg) if err != nil { r.logger.Error("ForgeConf can't send data", err) return 0 } return ptrSize } func (r *runtime) ModifyContentBuilder(forAS bool) interface{} { f := func(filename string, content string) { if ok, err := checkWrite(r.pluginRun.write.git, r.fsWorktree, filename); !ok { r.logger.Error("plugin can't write in git", err, logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename)) return } r.logger.Debug("modifyContent", logger.NewLoggerPair("file", filename)) if err := r.repoWriter.Write(filename, []byte(content)); err != nil { r.logger.Error("modifyContent can't open file", err, logger.NewLoggerPair("filepath", filename)) return } } if !forAS { return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) { filename, err := r.readString(m, "ModifyContent filename", filenamePtr, filenameSize) if err != nil { return } content, err := r.readString(m, "ModifyContent content", contentPtr, contentSize) if err != nil { return } f(filename, content) } } else { return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) { filename, err := r.readASString(m, "ModifyContent filename", filenamePtr) if err != nil { return } content, err := r.readASString(m, "ModifyContent content", contentPtr) if err != nil { return } f(filename, content) } } } func (r *runtime) ModifyWebContentBuilder(forAS bool) interface{} { f := func(filename string, content string) { if ok, err := checkWrite(r.pluginRun.write.web, r.fsWebcontent, filename); !ok { r.logger.Warn("plugin can't write in web", logger.NewLoggerPair("err", err), logger.NewLoggerPair("plugin", r.repo.Name()), logger.NewLoggerPair("branch", r.command.branch), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename)) return } fullPath := r.repo.PathDataWeb(filename) dir, _ := filepath.Split(fullPath) if err := os.MkdirAll(dir, os.ModePerm); err != nil { r.logger.Error("modifyWebContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename)) return } if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil { r.logger.Error("modifyWebContent can't open file", err, logger.NewLoggerPair("filepath", filename)) return } r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath)) } if !forAS { return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) { filename, err := r.readString(m, "ModifyWebContent filename", filenamePtr, filenameSize) if err != nil { return } content, err := r.readString(m, "ModifyWebContent content", contentPtr, contentSize) if err != nil { return } f(filename, content) } } else { return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) { filename, err := r.readASString(m, "ModifyWebContent filename", filenamePtr) if err != nil { return } content, err := r.readASString(m, "ModifyWebContent content", contentPtr) if err != nil { return } f(filename, content) } } } func (r *runtime) ReplaceWebContentBuilder(forAS bool) interface{} { f := func(filename string, oldContent string, content string) { if ok, err := checkWrite(r.pluginRun.write.web, r.fsWebcontent, filename); !ok { r.logger.Warn("plugin can't write in web for replace", logger.NewLoggerPair("err", err), logger.NewLoggerPair("plugin", r.plugin.Name), logger.NewLoggerPair("path", filename)) return } fullPath := r.repo.PathDataWeb(filename) dir, _ := filepath.Split(fullPath) if err := os.MkdirAll(dir, os.ModePerm); err != nil { r.logger.Error("ReplaceWebContentBuilder can't mkdirAll", err, logger.NewLoggerPair("filepath", filename)) return } fileContent, err := os.ReadFile(fullPath) if err != nil { r.logger.Error("ReplaceWebContentBuilder can't ReadFile", err, logger.NewLoggerPair("filepath", filename)) return } if err := os.WriteFile(fullPath, bytes.Replace(fileContent, []byte(oldContent), []byte(content), 1), 0666); err != nil { r.logger.Error("ReplaceWebContentBuilder can't open file", err, logger.NewLoggerPair("filepath", filename)) return } r.logger.Debug("Write in web", logger.NewLoggerPair("fullPath", fullPath)) } if !forAS { return func(_ context.Context, m api.Module, filenamePtr, filenameSize, oldContentPtr, oldContentSize, contentPtr, contentSize uint32) { filename, err := r.readString(m, "ReplaceWebContentBuilder filename", filenamePtr, filenameSize) if err != nil { return } oldContent, err := r.readString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr, oldContentSize) if err != nil { return } content, err := r.readString(m, "ReplaceWebContentBuilder content", contentPtr, contentSize) if err != nil { return } f(filename, oldContent, content) } } else { return func(_ context.Context, m api.Module, filenamePtr, oldContentPtr, contentPtr uint32) { filename, err := r.readASString(m, "ReplaceWebContentBuilder filename", filenamePtr) if err != nil { return } oldContent, err := r.readASString(m, "ReplaceWebContentBuilder oldContent", oldContentPtr) if err != nil { return } content, err := r.readASString(m, "ReplaceWebContentBuilder content", contentPtr) if err != nil { return } f(filename, oldContent, content) } } } func (r *runtime) ModifyCacheContentBuilder(forAS bool) interface{} { f := func(filename string, content string) { fullPath := filepath.Join(r.manager.conf.PathCache(), r.repo.Name(), r.plugin.Name, filename) dir, _ := filepath.Split(fullPath) if err := os.MkdirAll(dir, os.ModePerm); err != nil { r.logger.Error("ModifyCacheContent can't mkdirAll", err, logger.NewLoggerPair("filepath", filename)) return } if err := os.WriteFile(fullPath, []byte(content), 0666); err != nil { r.logger.Error("ModifyCacheContent can't open file", err, logger.NewLoggerPair("filepath", filename)) return } r.logger.Debug("Write in cache", logger.NewLoggerPair("fullPath", fullPath)) } if !forAS { return func(_ context.Context, m api.Module, filenamePtr, filenameSize, contentPtr, contentSize uint32) { filename, err := r.readString(m, "ModifyCacheContent filename", filenamePtr, filenameSize) if err != nil { return } content, err := r.readString(m, "ModifyCacheContent content", contentPtr, contentSize) if err != nil { return } f(filename, content) } } else { return func(_ context.Context, m api.Module, filenamePtr, contentPtr uint32) { filename, err := r.readASString(m, "ModifyCacheContent filename", filenamePtr) if err != nil { return } content, err := r.readASString(m, "ModifyCacheContent content", contentPtr) if err != nil { return } f(filename, content) } } } func (r *runtime) CommitAllBuilder(forAS bool) interface{} { f := func(msg string, err error) { if err != nil { return } if h, err := r.repoWriter.CommitAll(msg, r.plugin.commiter); err != nil { r.logger.Error("commitAll can't commit", err) return } else { r.logger.Debug("Commit", logger.NewLoggerPair("message", msg), logger.NewLoggerPair("hash", h.String())) if r.commitHook != nil && !h.IsZero() { r.commitHook(h) } } } if !forAS { return func(_ context.Context, m api.Module, offset, byteCount uint32) { f(r.readString(m, "commitAll", offset, byteCount)) } } else { return func(_ context.Context, m api.Module, offset uint32) { f(r.readASString(m, "commitAll", offset)) } } } func (r *runtime) DiffWithParentBuilder(forAS bool) interface{} { f := func(m api.Module, hash string, oldfile string, newfile string) uint64 { r.logger.Info("In diffWithParent before found ancestor") diffStr, err := r.repoWriter.GetDiff(plumbing.NewHash(string(hash)), string(oldfile), string(newfile)) if err != nil { r.logger.Error("GetDiff", err) return 0 } r.logger.Debug("In diffWithParent send diff", logger.NewLoggerPair("diff", diffStr)) _, ptrSize, err := r.sendData(m, diffStr) //TODO free ptr (first arg) if err != nil { r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", diffStr)) return 0 } return ptrSize } if !forAS { return func(_ context.Context, m api.Module, hashPtr, hashSize, oldFilenamePtr, oldFilenameSize, newFilenamePtr, newFilenameSize uint32) uint64 { hash, err := r.readString(m, "DiffWithParent hash", hashPtr, hashSize) if err != nil { return 0 } oldFilename, err := r.readString(m, "DiffWithParent oldFilename", oldFilenamePtr, oldFilenameSize) if err != nil { return 0 } newFilename, err := r.readString(m, "DiffWithParent newFilename", newFilenamePtr, newFilenameSize) if err != nil { return 0 } return f(m, hash, oldFilename, newFilename) } } else { return func(_ context.Context, m api.Module, hashPtr, oldFilenamePtr, newFilenamePtr uint32) uint64 { hash, err := r.readASString(m, "DiffWithParent hash", hashPtr) if err != nil { return 0 } oldFilename, err := r.readASString(m, "DiffWithParent oldFilenamePtr", oldFilenamePtr) if err != nil { return 0 } newFilename, err := r.readASString(m, "DiffWithParent newFilename", newFilenamePtr) if err != nil { return 0 } return f(m, hash, oldFilename, newFilename) } } } func (r *runtime) LogBuilder(forAS bool) interface{} { f := func(msg string, err error) { if err != nil { return } r.logger.Debug(msg, logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name)) } if !forAS { return func(_ context.Context, m api.Module, offset, byteCount uint32) { f(r.readString(m, "log", offset, byteCount)) } } else { return func(_ context.Context, m api.Module, offset uint32) { f(r.readASString(m, "log", offset)) } } } func (r *runtime) LogErrorBuilder(forAS bool) interface{} { f := func(msg string, err string) { r.logger.Error(msg, errors.New(err), logger.NewLoggerPair("repo", r.repo.Name()), logger.NewLoggerPair("plugin", r.plugin.Name)) } if !forAS { return func(_ context.Context, m api.Module, offset, byteCount, errPtr, errSize uint32) { msg, err := r.readString(m, "LogError msg", offset, byteCount) if err != nil { return } errMsg, err := r.readString(m, "LogError err", errPtr, errSize) if err != nil { return } f(msg, errMsg) } } else { return func(_ context.Context, m api.Module, offset, errPtr uint32) { msg, err := r.readASString(m, "LogError msg", offset) if err != nil { return } errMsg, err := r.readASString(m, "LogError err", errPtr) if err != nil { return } f(msg, errMsg) } } } func (r *runtime) MergeBuilder(forAS bool) interface{} { f := func(from string, to string) { r.logger.Debug("try to merge", logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) cmd, err := r.repoWriter.Merge(from, to, r.plugin.commiter, r.command.pusher) if err != nil { r.logger.Error("can't Merge", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) } if r.mergeHook != nil { r.mergeHook(cmd, r.command.pusher, to) } } if !forAS { return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) { from, err := r.readString(m, "Merge from", fromPtr, fromSize) if err != nil { return } to, err := r.readString(m, "Merge to", toPtr, toSize) if err != nil { return } f(from, to) } } else { return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) { from, err := r.readASString(m, "Merge from", fromPtr) if err != nil { return } to, err := r.readASString(m, "Merge to", toPtr) if err != nil { return } f(from, to) } } } func (r *runtime) GetCommitsBuilder(forAS bool) interface{} { f := func(m api.Module, from string, to string) uint64 { commits := []commitForDiffCommit{} if err := r.repoWriter.WalkCommit(plumbing.NewHash(to), plumbing.NewHash(from), func(c *object.Commit) error { r.logger.Debug("found commit", logger.NewLoggerPair("hash", c.Hash.String()), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) commits = append(commits, commitToCommitForDiff(c, nil)) return nil }); err != nil { r.logger.Error("can't WalkCommit", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) } r.logger.Debug("found commits", logger.NewLoggerPair("nb", len(commits)), logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) j, err := Marshall("//TODO", commits) if err != nil { r.logger.Error("can't Marshall", err, logger.NewLoggerPair("from", from), logger.NewLoggerPair("to", to)) } _, ptrSize, err := r.sendData(m, j) //TODO free ptr (first arg) if err != nil { r.logger.Error("can't sendData", err, logger.NewLoggerPair("message", j)) return 0 } return ptrSize } if !forAS { return func(_ context.Context, m api.Module, fromPtr, fromSize, toPtr, toSize uint32) uint64 { from, err := r.readString(m, "Merge from", fromPtr, fromSize) if err != nil { return 0 } to, err := r.readString(m, "Merge to", toPtr, toSize) if err != nil { return 0 } return f(m, from, to) } } else { return func(_ context.Context, m api.Module, fromPtr, toPtr uint32) uint64 { from, err := r.readASString(m, "Merge from", fromPtr) if err != nil { return 0 } to, err := r.readASString(m, "Merge to", toPtr) if err != nil { return 0 } return f(m, from, to) } } } func (r *runtime) Close() error { return r.wazRun.Close(r.ctx) } func (r *runtime) sendData(module api.Module, message string) (ptr uint32, ptrSize uint64, err error) { malloc := module.ExportedFunction("malloc") s := uint64(len(message)) results, err := malloc.Call(r.ctx, s) if err != nil { return 0, 0, oops.Wrapf(err, "can't malloc memory") } ptrA := results[0] // The pointer is a linear memory offset, which is where we write the name. if !module.Memory().WriteString(uint32(ptrA), message) { return 0, 0, oops.Errorf("can't write memory") } return uint32(ptrA), (ptrA << uint64(32)) | s, nil } func (r *runtime) readString(m api.Module, errorMsg string, ptr uint32, size uint32) (string, error) { txt, ok := m.Memory().Read(ptr, size) if !ok { err := errors.New("Memory.Read fail") r.logger.Error(errorMsg, err, logger.NewLoggerPair("ptr", ptr), logger.NewLoggerPair("size", size)) return "", err } return string(txt), nil } func (r *runtime) readASString(m api.Module, errorMsg string, offset uint32) (string, error) { // Length is four bytes before pointer. byteCount, ok := m.Memory().ReadUint32Le(offset - 4) if !ok || byteCount%2 != 0 { err := errors.New("Memory.ReadUint32Le fail") r.logger.Error(errorMsg, err, logger.NewLoggerPair("byteCount", byteCount), logger.NewLoggerPair("offset", offset)) return "", err } buf, ok := m.Memory().Read(offset, byteCount) if !ok { err := errors.New("Memory.Read fail") r.logger.Error(errorMsg, err, logger.NewLoggerPair("offset", offset), logger.NewLoggerPair("byteCount", byteCount)) return "", err } u16s := make([]uint16, len(buf)/2) lb := len(buf) for i := 0; i < lb; i += 2 { u16s[i/2] = uint16(buf[i]) + (uint16(buf[i+1]) << 8) } return string(utf16.Decode(u16s)), nil }