go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit 7de399c30f03f832466bd8d438beabba150d2076
parent b4386fa0eaddb33c431c384f58edeed9a636b747
Author: lash <dev@holbrook.no>
Date:   Sun,  1 Sep 2024 05:27:52 +0100

WIP fix missing language key translation handling in db

Diffstat:
Mdb/db.go | 30++++++++++++++++++++++--------
Mdb/fs.go | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mdb/gdbm.go | 22+++++++++++++++++-----
Mdb/mem.go | 44++++++++++++++++++++++++++++++++++----------
Mdb/pg.go | 27+++++++++++++++++++++------
Mexamples/gdbm/main.go | 11++++++++---
Mresource/resource.go | 6+++---
Atestdata/testdata_legacy.go | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 393 insertions(+), 62 deletions(-)

diff --git a/db/db.go b/db/db.go @@ -47,6 +47,11 @@ type Db interface { SetSession(sessionId string) } +type LookupKey struct { + Default []byte + Translation []byte +} + // ToDbKey generates a key to use Db to store a value for a particular context. // // If language is nil, then default language storage context will be used. @@ -54,7 +59,7 @@ type Db interface { // If language is not nil, and the context does not support language, the language value will silently will be ignored. func ToDbKey(typ uint8, b []byte, l *lang.Language) []byte { k := []byte{typ} - if l != nil && l.Code != "" && typ & (DATATYPE_MENU | DATATYPE_TEMPLATE) > 0 { + if l != nil && l.Code != "" && typ & (DATATYPE_MENU | DATATYPE_TEMPLATE | DATATYPE_STATICLOAD) > 0 { b = append(b, []byte("_" + l.Code)...) //s += "_" + l.Code } @@ -66,6 +71,7 @@ type baseDb struct { pfx uint8 sid []byte lock uint8 + lang *lang.Language } // ensures default locking of read-only entries @@ -96,20 +102,28 @@ func(db *baseDb) checkPut() bool { return db.pfx & db.lock == 0 } +func(db *baseDb) SetLanguage(ln *lang.Language) { + db.lang = ln +} + // ToKey creates a DbKey within the current session context. -func(db *baseDb) ToKey(ctx context.Context, key []byte) ([]byte, error) { +func(db *baseDb) ToKey(ctx context.Context, key []byte) (LookupKey, error) { + var lk LookupKey var b []byte if db.pfx == DATATYPE_UNKNOWN { - return nil, errors.New("datatype prefix cannot be UNKNOWN") + return lk, errors.New("datatype prefix cannot be UNKNOWN") } if (db.pfx > datatype_sessioned_threshold) { b = append(db.sid, key...) } else { b = key } - ln, ok := ctx.Value("Language").(lang.Language) - if ok { - return ToDbKey(db.pfx, b, &ln), nil - } - return ToDbKey(db.pfx, b, nil), nil + lk.Default = ToDbKey(db.pfx, b, nil) + if db.pfx & (DATATYPE_MENU | DATATYPE_TEMPLATE | DATATYPE_STATICLOAD) > 0 { + ln, ok := ctx.Value("Language").(lang.Language) + if ok { + lk.Translation = ToDbKey(db.pfx, b, &ln) + } + } + return lk, nil } diff --git a/db/fs.go b/db/fs.go @@ -3,11 +3,17 @@ package db import ( "context" "errors" + "io/fs" "io/ioutil" "os" "path" ) +type fsLookupKey struct { + Default string + Translation string +} + // pure filesystem backend implementation if the Db interface. type fsDb struct { baseDb @@ -36,22 +42,35 @@ func(fdb *fsDb) Connect(ctx context.Context, connStr string) error { // Get implements the Db interface. func(fdb *fsDb) Get(ctx context.Context, key []byte) ([]byte, error) { - fp, err := fdb.pathFor(ctx, key) + var f *os.File + lk, err := fdb.ToKey(ctx, key) if err != nil { return nil, err } - logg.TraceCtxf(ctx, "trying fs get", "key", key, "path", fp) - f, err := os.Open(fp) + flk, err := fdb.pathFor(ctx, &lk) if err != nil { - fp, err = fdb.altPathFor(ctx, key) - if err != nil { - return nil, err + return nil, err + } + flka, err := fdb.altPathFor(ctx, &lk) + if err != nil { + return nil, err + } + for i, fp := range([]string{flk.Translation, flka.Translation, flk.Default, flka.Default}) { + if fp == "" { + logg.TraceCtxf(ctx, "fs get skip missing", "i", i) + continue } - logg.TraceCtxf(ctx, "trying fs get alt", "key", key, "path", fp) + logg.TraceCtxf(ctx, "trying fs get", "i", i, "key", key, "path", fp) f, err = os.Open(fp) - if err != nil { - return nil, NewErrNotFound([]byte(fp)) + if err == nil { + break } + if !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + } + if f == nil { + return nil, NewErrNotFound(key) } defer f.Close() b, err := ioutil.ReadAll(f) @@ -66,37 +85,56 @@ func(fdb *fsDb) Put(ctx context.Context, key []byte, val []byte) error { if !fdb.checkPut() { return errors.New("unsafe put and safety set") } - fp, err := fdb.pathFor(ctx, key) + lk, err := fdb.ToKey(ctx, key) + if err != nil { + return err + } + flk, err := fdb.pathFor(ctx, &lk) if err != nil { return err } - return ioutil.WriteFile(fp, val, 0600) + if flk.Translation != "" { + err = ioutil.WriteFile(flk.Translation, val, 0600) + if err != nil { + return err + } + } + return ioutil.WriteFile(flk.Default, val, 0600) } -// Close implements the Db interface.. +// Close implements the Db interface. func(fdb *fsDb) Close() error { return nil -} +} // create a key safe for the filesystem. -func(fdb *fsDb) pathFor(ctx context.Context, key []byte) (string, error) { - kb, err := fdb.ToKey(ctx, key) - if err != nil { - return "", err +func(fdb *fsDb) pathFor(ctx context.Context, lk *LookupKey) (fsLookupKey, error) { + var flk fsLookupKey + lk.Default[0] += 0x30 + flk.Default = path.Join(fdb.dir, string(lk.Default)) + if lk.Translation != nil { + lk.Translation[0] += 0x30 + flk.Translation = path.Join(fdb.dir, string(lk.Translation)) } - kb[0] += 0x30 - return path.Join(fdb.dir, string(kb)), nil + return flk, nil } // create a key safe for the filesystem, matching legacy resource.FsResource name. -func(fdb *fsDb) altPathFor(ctx context.Context, key []byte) (string, error) { - kb, err := fdb.ToKey(ctx, key) - if err != nil { - return "", err - } - fb := string(kb[1:]) +func(fdb *fsDb) altPathFor(ctx context.Context, lk *LookupKey) (fsLookupKey, error) { + var flk fsLookupKey + fb := string(lk.Default[1:]) if fdb.pfx == DATATYPE_BIN { fb += ".bin" - } - return path.Join(fdb.dir, fb), nil + } + flk.Default = path.Join(fdb.dir, fb) + + if lk.Translation != nil { + fb = string(lk.Translation[1:]) + if fdb.pfx == DATATYPE_BIN { + fb += ".bin" + } + flk.Translation = path.Join(fdb.dir, fb) + } + + return flk, nil } diff --git a/db/gdbm.go b/db/gdbm.go @@ -50,23 +50,35 @@ func(gdb *gdbmDb) Put(ctx context.Context, key []byte, val []byte) error { if !gdb.checkPut() { return errors.New("unsafe put and safety set") } - k, err := gdb.ToKey(ctx, key) + lk, err := gdb.ToKey(ctx, key) if err != nil { return err } - return gdb.conn.Store(k, val, true) + if lk.Translation != nil { + return gdb.conn.Store(lk.Translation, val, true) + } + return gdb.conn.Store(lk.Default, val, true) } // Get implements Db func(gdb *gdbmDb) Get(ctx context.Context, key []byte) ([]byte, error) { - k, err := gdb.ToKey(ctx, key) + var v []byte + lk, err := gdb.ToKey(ctx, key) if err != nil { return nil, err } - v, err := gdb.conn.Fetch(k) + if lk.Translation != nil { + v, err = gdb.conn.Fetch(lk.Translation) + if err != nil { + if !errors.Is(gdbm.ErrItemNotFound, err) { + return nil, err + } + } + } + v, err = gdb.conn.Fetch(lk.Default) if err != nil { if errors.Is(gdbm.ErrItemNotFound, err) { - return nil, NewErrNotFound(k) + return nil, NewErrNotFound(key) } return nil, err } diff --git a/db/mem.go b/db/mem.go @@ -6,6 +6,11 @@ import ( "errors" ) +type memLookupKey struct { + Default string + Translation string +} + // memDb is a memory backend implementation of the Db interface. type memDb struct { baseDb @@ -29,37 +34,56 @@ func(mdb *memDb) Connect(ctx context.Context, connStr string) error { } // convert to a supported map key type -func(mdb *memDb) toHexKey(ctx context.Context, key []byte) (string, error) { - k, err := mdb.ToKey(ctx, key) - return hex.EncodeToString(k), err +func(mdb *memDb) toHexKey(ctx context.Context, key []byte) (memLookupKey, error) { + var mk memLookupKey + lk, err := mdb.ToKey(ctx, key) + mk.Default = hex.EncodeToString(lk.Default) + if lk.Translation != nil { + mk.Translation = hex.EncodeToString(lk.Translation) + } + return mk, err } // Get implements Db func(mdb *memDb) Get(ctx context.Context, key []byte) ([]byte, error) { - k, err := mdb.toHexKey(ctx, key) + var v []byte + var ok bool + mk, err := mdb.toHexKey(ctx, key) if err != nil { return nil, err } - logg.TraceCtxf(ctx, "mem get", "k", k) - v, ok := mdb.store[k] + logg.TraceCtxf(ctx, "mem get", "k", mk) + if mk.Translation != "" { + v, ok = mdb.store[mk.Translation] + if ok { + return v, nil + } + } + v, ok = mdb.store[mk.Default] if !ok { - b, _ := hex.DecodeString(k) - return nil, NewErrNotFound(b) + //b, _ := hex.DecodeString(k) + return nil, NewErrNotFound(key) } return v, nil } // Put implements Db func(mdb *memDb) Put(ctx context.Context, key []byte, val []byte) error { + var k string if !mdb.checkPut() { return errors.New("unsafe put and safety set") } - k, err := mdb.toHexKey(ctx, key) + mk, err := mdb.toHexKey(ctx, key) if err != nil { return err } + if mk.Translation != "" { + k = mk.Translation + } else { + k = mk.Default + } mdb.store[k] = val - logg.TraceCtxf(ctx, "mem put", "k", k, "v", val) + logg.TraceCtxf(ctx, "mem put", "k", k, "v", val) return nil } diff --git a/db/pg.go b/db/pg.go @@ -70,27 +70,42 @@ func(pdb *pgDb) Put(ctx context.Context, key []byte, val []byte) error { // Get implements Db. func(pdb *pgDb) Get(ctx context.Context, key []byte) ([]byte, error) { - k, err := pdb.ToKey(ctx, key) + lk, err := pdb.ToKey(ctx, key) if err != nil { return nil, err } + if lk.Translation != nil { + tx, err := pdb.conn.Begin(ctx) + if err != nil { + return nil, err + } + query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema) + rs, err := tx.Query(ctx, query, lk.Translation) + if err != nil { + return nil, err + } + defer rs.Close() + if rs.Next() { + r := rs.RawValues() + return r[0], nil + } + } + tx, err := pdb.conn.Begin(ctx) if err != nil { return nil, err } query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema) - rs, err := tx.Query(ctx, query, k) + rs, err := tx.Query(ctx, query, lk.Translation) if err != nil { return nil, err } defer rs.Close() if !rs.Next() { - return nil, NewErrNotFound(k) - + return nil, NewErrNotFound(key) } r := rs.RawValues() - b := r[0] - return b, nil + return r[0], nil } // Close implements Db. diff --git a/examples/gdbm/main.go b/examples/gdbm/main.go @@ -48,10 +48,15 @@ func main() { rs = rs.WithTemplateGetter(tg.GetTemplate) rs = rs.WithCodeGetter(tg.GetCode) - rsf := resource.NewFsResource(scriptDir) + fsStore := db.NewFsDb() + fsStore.Connect(ctx, scriptDir) + rsf, err := resource.NewDbResource(fsStore, db.DATATYPE_MENU) + if err != nil { + panic(err) + } rsf.AddLocalFunc("do", do) - rs = rs.WithMenuGetter(rsf.GetMenu) - rs = rs.WithEntryFuncGetter(rsf.FuncFor) + rs.WithMenuGetter(rsf.GetMenu) + rs.WithEntryFuncGetter(rsf.FuncFor) ca := cache.NewCache() if err != nil { diff --git a/resource/resource.go b/resource/resource.go @@ -27,7 +27,7 @@ type MenuFunc func(ctx context.Context, menuSym string) (string, error) // TemplateFunc is the function signature for retrieving a render template for a given symbol. type TemplateFunc func(ctx context.Context, nodeSym string) (string, error) // FuncForFunc is a function that returns an EntryFunc associated with a LOAD instruction symbol. -type FuncForFunc func(loadSym string) (EntryFunc, error) +type FuncForFunc func(ctx context.Context, loadSym string) (EntryFunc, error) // Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. // @@ -88,7 +88,7 @@ func(m *MenuResource) WithMenuGetter(menuGetter MenuFunc) *MenuResource { // FuncFor implements Resource interface. func(m MenuResource) FuncFor(ctx context.Context, sym string) (EntryFunc, error) { - return m.funcFunc(sym) + return m.funcFunc(ctx, sym) } // GetCode implements Resource interface. @@ -115,7 +115,7 @@ func(m *MenuResource) AddLocalFunc(sym string, fn EntryFunc) { } // FallbackFunc returns the default handler function for a given external function symbol. -func(m *MenuResource) FallbackFunc(sym string) (EntryFunc, error) { +func(m *MenuResource) FallbackFunc(ctx context.Context, sym string) (EntryFunc, error) { fn, ok := m.fns[sym] if !ok { return nil, fmt.Errorf("unknown function: %s", sym) diff --git a/testdata/testdata_legacy.go b/testdata/testdata_legacy.go @@ -0,0 +1,223 @@ +package testdata + +import ( + "fmt" + "io/ioutil" + "os" + "path" + + testdataloader "github.com/peteole/testdata-loader" + + "git.defalsify.org/vise.git/vm" +) + +type genFunc func() error + +var ( + BaseDir = testdataloader.GetBasePath() + DataDir = "" + dirLock = false +) + +func outLegacy(sym string, b []byte, tpl string, data map[string]string) error { + fp := path.Join(DataDir, sym) + err := ioutil.WriteFile(fp, []byte(tpl), 0644) + if err != nil { + return err + } + + fb := sym + ".bin" + fp = path.Join(DataDir, fb) + err = ioutil.WriteFile(fp, b, 0644) + if err != nil { + return err + } + + if data == nil { + return nil + } + + for k, v := range data { + fb := k + ".txt" + fp = path.Join(DataDir, fb) + err = ioutil.WriteFile(fp, []byte(v), 0644) + if err != nil { + return err + } + } + + return nil +} + +func root() error { + b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"do the foo", "1"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"go to the bar", "2"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"language template", "3"}, nil, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"foo", "1"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"bar", "2"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"lang", "3"}, nil, nil) + + tpl := "hello world" + + return out("root", b, tpl, nil) +} + +func foo() error { + b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"to foo", "0"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"go bar", "1"}, nil, nil) + b = vm.NewLine(b, vm.MOUT, []string{"see long", "2"}, nil, nil) + b = vm.NewLine(b, vm.LOAD, []string{"inky"}, []byte{20}, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"_", "0"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"baz", "1"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"long", "2"}, nil, nil) + + data := make(map[string]string) + data["inky"] = "one" + + tpl := `this is in foo + +it has more lines` + + return out("foo", b, tpl, data) +} + +func bar() error { + b := []byte{} + b = vm.NewLine(b, vm.LOAD, []string{"pinky"}, []byte{0}, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"^", "*"}, nil, nil) + + tpl := "this is bar - any input will return to top" + + data := make(map[string]string) + data["pinky"] = "two" + + return out("bar", b, tpl, data) +} + +func baz() error { + b := []byte{} + b = vm.NewLine(b, vm.MAP, []string{"inky"}, nil, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + + tpl := "this is baz which uses the var {{.inky}} in the template." + + return out("baz", b, tpl, nil) +} + +func long() error { + b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) + b = vm.NewLine(b, vm.MNEXT, []string{"nexxt", "00"}, nil, nil) + b = vm.NewLine(b, vm.MPREV, []string{"prevvv", "11"}, nil, nil) + b = vm.NewLine(b, vm.LOAD, []string{"longdata"}, []byte{0x00}, nil) + b = vm.NewLine(b, vm.MAP, []string{"longdata"}, nil, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"_", "0"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{">", "00"}, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"<", "11"}, nil, nil) + + tpl := `data +{{.longdata}}` + + data := make(map[string]string) + data["longdata"] = `INKY 12 +PINKY 5555 +BLINKY 3t7 +CLYDE 11 +TINKYWINKY 22 +DIPSY 666 +LALA 111 +POO 222 +` + + return out("long", b, tpl, data) +} + +func defaultCatch() error { + b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"_", "*"}, nil, nil) + + tpl := "invalid input" + + return out("_catch", b, tpl, nil) +} + +func lang() error { + b := []byte{} + b = vm.NewLine(b, vm.MOUT, []string{"back", "0"}, nil, nil) + b = vm.NewLine(b, vm.LOAD, []string{"inky"}, []byte{20}, nil) + b = vm.NewLine(b, vm.MAP, []string{"inky"}, nil, nil) + b = vm.NewLine(b, vm.HALT, nil, nil, nil) + b = vm.NewLine(b, vm.INCMP, []string{"_", "*"}, nil, nil) + + tpl := "this changes with language {{.inky}}" + + err := out("lang", b, tpl, nil) + if err != nil { + return err + } + + tpl = "dette endrer med språket {{.inky}}" + fp := path.Join(DataDir, "lang_nor") + err = os.WriteFile(fp, []byte(tpl), 0600) + if err != nil { + return err + } + + menu := "tilbake" + fp = path.Join(DataDir, "back_menu_nor") + return os.WriteFile(fp, []byte(menu), 0600) +} + +func generateLegacy() error { + out = outLegacy + err := os.MkdirAll(DataDir, 0755) + if err != nil { + return err + } + + fns := []genFunc{root, foo, bar, baz, long, lang, defaultCatch} + for _, fn := range fns { + err = fn() + if err != nil { + return err + } + } + return nil +} + +// Generate outputs bytecode, templates and content symbols to a temporary directory. +// +// This directory can in turn be used as data source for the the resource.FsResource object. +func GenerateLegacy() (string, error) { + dir, err := ioutil.TempDir("", "vise_testdata_") + if err != nil { + return "", err + } + DataDir = dir + dirLock = true + err = generateLegacy() + return dir, err +} + + +// Generate outputs bytecode, templates and content symbols to a specified directory. +// +// The directory must exist, and must not have been used already in the same code execution. +// +// This directory can in turn be used as data source for the the resource.FsResource object. +func GenerateLegacyTo(dir string) error { + if dirLock { + return fmt.Errorf("directory already overridden") + } + DataDir = dir + dirLock = true + return generateLegacy() +}