From 4a9143932032033294d8223f1c43c1b99ea1422f Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 6 Apr 2023 15:21:26 +0100 Subject: [PATCH] WIP Add size checker to resource render --- go/engine/engine.go | 2 +- go/resource/fs.go | 6 +-- go/resource/render.go | 4 +- go/resource/resource.go | 69 +++++++++++++++++++++++++- go/resource/resource_test.go | 9 ++++ go/resource/size.go | 53 ++++++++++++++++++++ go/resource/size_test.go | 96 ++++++++++++++++++++++++++++++++++++ go/state/state.go | 51 +++++++++++++++++-- go/vm/runner_test.go | 2 +- 9 files changed, 279 insertions(+), 13 deletions(-) create mode 100644 go/resource/resource_test.go create mode 100644 go/resource/size.go create mode 100644 go/resource/size_test.go diff --git a/go/engine/engine.go b/go/engine/engine.go index c883423..18dc782 100644 --- a/go/engine/engine.go +++ b/go/engine/engine.go @@ -16,7 +16,7 @@ var ( inputRegexStr = "^[a-zA-Z0-9].*$" inputRegex = regexp.MustCompile(inputRegexStr) ) -// + //type Config struct { // FlagCount uint32 // CacheSize uint32 diff --git a/go/resource/fs.go b/go/resource/fs.go index afcf6f2..4031604 100644 --- a/go/resource/fs.go +++ b/go/resource/fs.go @@ -23,17 +23,13 @@ func NewFsResource(path string) (FsResource) { } } -func(fs FsResource) GetTemplate(sym string) (string, error) { +func(fs FsResource) GetTemplate(sym string, sizer *Sizer) (string, error) { fp := path.Join(fs.Path, sym) r, err := ioutil.ReadFile(fp) s := string(r) return strings.TrimSpace(s), err } -func(fs FsResource) RenderTemplate(sym string, values map[string]string) (string, error) { - return DefaultRenderTemplate(&fs, sym, values) -} - func(fs FsResource) GetCode(sym string) ([]byte, error) { fb := sym + ".bin" fp := path.Join(fs.Path, fb) diff --git a/go/resource/render.go b/go/resource/render.go index 712dcf4..debf701 100644 --- a/go/resource/render.go +++ b/go/resource/render.go @@ -5,9 +5,10 @@ import ( "text/template" ) + // DefaultRenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. func DefaultRenderTemplate(r Resource, sym string, values map[string]string) (string, error) { - v, err := r.GetTemplate(sym) + v, err := r.GetTemplate(sym, nil) if err != nil { return "", err } @@ -23,4 +24,3 @@ func DefaultRenderTemplate(r Resource, sym string, values map[string]string) (st } return b.String(), err } - diff --git a/go/resource/resource.go b/go/resource/resource.go index 02c405a..5d7b17a 100644 --- a/go/resource/resource.go +++ b/go/resource/resource.go @@ -8,16 +8,20 @@ import ( // EntryFunc is a function signature for retrieving value for a key type EntryFunc func(ctx context.Context) (string, error) +type CodeFunc func(sym string) ([]byte, error) +type TemplateFunc func(sym string, sizer *Sizer) (string, error) +type FuncForFunc func(sym string) (EntryFunc, error) // Resource implementation are responsible for retrieving values and templates for symbols, and can render templates from value dictionaries. type Resource interface { - GetTemplate(sym string) (string, error) // Get the template for a given symbol. + GetTemplate(sym string, sizer *Sizer) (string, error) // Get the template for a given symbol. GetCode(sym string) ([]byte, error) // Get the bytecode for the given symbol. PutMenu(string, string) error // Add a menu item. ShiftMenu() (string, string, error) // Remove and return the first menu item in list. SetMenuBrowse(string, string, bool) error // Set menu browser display details. RenderTemplate(sym string, values map[string]string) (string, error) // Render the given data map using the template of the symbol. RenderMenu() (string, error) // Render the current state of menu + Render(sym string, values map[string]string, sizer *Sizer) (string, error) // Render full output. FuncFor(sym string) (EntryFunc, error) // Resolve symbol content point for. } @@ -25,6 +29,28 @@ type MenuResource struct { menu [][2]string next [2]string prev [2]string + codeFunc CodeFunc + templateFunc TemplateFunc + funcFunc FuncForFunc +} + +func NewMenuResource() *MenuResource { + return &MenuResource{} +} + +func(m *MenuResource) WithCodeGetter(codeGetter CodeFunc) *MenuResource { + m.codeFunc = codeGetter + return m +} + +func(m *MenuResource) WithEntryFuncGetter(entryFuncGetter FuncForFunc) *MenuResource { + m.funcFunc = entryFuncGetter + return m +} + +func(m *MenuResource) WithTemplateGetter(templateGetter TemplateFunc) *MenuResource { + m.templateFunc = templateGetter + return m } // SetMenuBrowse defines the how pagination menu options should be displayed. @@ -79,3 +105,44 @@ func(m *MenuResource) RenderMenu() (string, error) { } return r, nil } + +func(m *MenuResource) RenderTemplate(sym string, values map[string]string) (string, error) { + return DefaultRenderTemplate(m, sym, values) +} + +func(m *MenuResource) FuncFor(sym string) (EntryFunc, error) { + return m.funcFunc(sym) +} + +func(m *MenuResource) GetCode(sym string) ([]byte, error) { + return m.codeFunc(sym) +} + +func(m *MenuResource) GetTemplate(sym string, sizer *Sizer) (string, error) { + return m.templateFunc(sym, sizer) +} + +func(m *MenuResource) Render(sym string, values map[string]string, sizer *Sizer) (string, error) { + r := "" + s, err := m.RenderTemplate(sym, values) + if err != nil { + return "", err + } + log.Printf("rendered %v bytes for template", len(s)) + r += s + l, ok := sizer.Check(r) + if !ok { + return "", fmt.Errorf("rendered size %v exceeds limits: %v", l, sizer) + } + s, err = m.RenderMenu() + if err != nil { + return "", err + } + log.Printf("rendered %v bytes for menu", len(s)) + r += s + l, ok = sizer.Check(r) + if !ok { + return "", fmt.Errorf("rendered size %v exceeds limits: %v", l, sizer) + } + return r, nil +} diff --git a/go/resource/resource_test.go b/go/resource/resource_test.go new file mode 100644 index 0000000..1de71f3 --- /dev/null +++ b/go/resource/resource_test.go @@ -0,0 +1,9 @@ +package resource + +import ( + "testing" +) + +func TestRenderLimit(t *testing.T) { + +} diff --git a/go/resource/size.go b/go/resource/size.go new file mode 100644 index 0000000..7a2807e --- /dev/null +++ b/go/resource/size.go @@ -0,0 +1,53 @@ +package resource + +import ( + "fmt" + "log" + + "git.defalsify.org/festive/state" +) + +type Sizer struct { + outputSize uint32 + menuSize uint16 + memberSizes map[string]uint16 + totalMemberSize uint32 + sink string +} + +func SizerFromState(st *state.State) (Sizer, error){ + sz := Sizer{ + outputSize: st.GetOutputSize(), + menuSize: st.GetMenuSize(), + memberSizes: make(map[string]uint16), + } + sizes, err := st.Sizes() + if err != nil { + return sz, err + } + for k, v := range sizes { + if v == 0 { + sz.sink = k + } + sz.memberSizes[k] = v + sz.totalMemberSize += uint32(v) + } + return sz, nil +} + +func(szr *Sizer) Check(s string) (uint32, bool) { + l := uint32(len(s)) + if szr.outputSize > 0 && l > szr.outputSize { + log.Printf("sizer check fails with length %v: %s", l, szr) + return l, false + } + return l, true +} + +func(szr *Sizer) String() string { + var diff uint32 + if szr.outputSize > 0 { + diff = szr.outputSize - szr.totalMemberSize - uint32(szr.menuSize) + } + return fmt.Sprintf("output: %v, member: %v, menu: %v, diff: %v", szr.outputSize, szr.totalMemberSize, szr.menuSize, diff) +} diff --git a/go/resource/size_test.go b/go/resource/size_test.go new file mode 100644 index 0000000..5958a26 --- /dev/null +++ b/go/resource/size_test.go @@ -0,0 +1,96 @@ +package resource + +import ( + "fmt" + "context" + "testing" + + "git.defalsify.org/festive/state" +) + +type TestSizeResource struct { + *MenuResource +} + +func getTemplate(sym string, sizer *Sizer) (string, error) { + var tpl string + switch sym { + case "small": + tpl = "one {{.foo}} two {{.bar}} three {{.baz}}" + case "toobig": + tpl = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus in mattis lorem. Aliquam erat volutpat. Ut vitae metus." + + } + return tpl, nil +} + +func funcFor(sym string) (EntryFunc, error) { + switch sym { + case "foo": + return getFoo, nil + case "bar": + return getBar, nil + case "baz": + return getBaz, nil + } + return nil, fmt.Errorf("unknown func: %s", sym) +} + +func getFoo(ctx context.Context) (string, error) { + return "inky", nil +} + +func getBar(ctx context.Context) (string, error) { + return "pinky", nil +} + +func getBaz(ctx context.Context) (string, error) { + return "blinky", nil +} + + +func TestSizeLimit(t *testing.T) { + st := state.NewState(0).WithOutputSize(128) + mrs := NewMenuResource().WithEntryFuncGetter(funcFor).WithTemplateGetter(getTemplate) + rs := TestSizeResource{ + mrs, + } + st.Add("foo", "inky", 4) + st.Add("bar", "pinky", 10) + st.Add("baz", "blinky", 0) + st.Map("foo") + st.Map("bar") + st.Map("baz") + st.SetMenuSize(32) + szr, err := SizerFromState(&st) + if err != nil { + t.Fatal(err) + } + + rs.PutMenu("1", "foo the foo") + rs.PutMenu("2", "go to bar") + + tpl, err := rs.GetTemplate("small", &szr) + if err != nil { + t.Fatal(err) + } + + vals, err := st.Get() + if err != nil { + t.Fatal(err) + } + _ = tpl + + _, err = rs.Render("small", vals, &szr) + if err != nil { + t.Fatal(err) + } + + rs.PutMenu("1", "foo the foo") + rs.PutMenu("2", "go to bar") + + _, err = rs.Render("toobig", vals, &szr) + if err == nil { + t.Fatalf("expected size exceeded") + } +} diff --git a/go/state/state.go b/go/state/state.go index 835aa67..09ead09 100644 --- a/go/state/state.go +++ b/go/state/state.go @@ -28,13 +28,15 @@ type State struct { CacheUseSize uint32 // Currently used bytes by all values (not code) in cache Cache []map[string]string // All loaded cache items CacheMap map[string]string // Mapped + menuSize uint16 // Max size of menu + outputSize uint32 // Max size of output input []byte // Last input code []byte // Pending bytecode to execute execPath []string // Command symbols stack arg *string // Optional argument. Nil if not set. sizes map[string]uint16 // Size limits for all loaded symbols. - sink *string // Sink symbol set for level bitSize uint32 // size of (32-bit capacity) bit flag byte array + sink *string //sizeIdx uint16 } @@ -195,6 +197,12 @@ func(st State) WithCacheSize(cacheSize uint32) State { return st } +// WithCacheSize applies a cumulative cache size limitation for all cached items. +func(st State) WithOutputSize(outputSize uint32) State { + st.outputSize = outputSize + return st +} + // Where returns the current active rendering symbol. func(st State) Where() string { if len(st.execPath) == 0 { @@ -268,7 +276,7 @@ func(st *State) Add(key string, value string, sizeLimit uint16) error { if sz == 0 { return fmt.Errorf("Cache capacity exceeded %v of %v", st.CacheUseSize + sz, st.CacheSize) } - log.Printf("add key %s value size %v", key, sz) + log.Printf("add key %s value size %v limit %v", key, sz, sizeLimit) st.Cache[len(st.Cache)-1][key] = value st.CacheUseSize += sz st.sizes[key] = sizeLimit @@ -346,6 +354,42 @@ func(st *State) Get() (map[string]string, error) { return st.Cache[len(st.Cache)-1], nil } +func(st *State) Sizes() (map[string]uint16, error) { + if len(st.Cache) == 0 { + return nil, fmt.Errorf("get at top frame") + } + sizes := make(map[string]uint16) + var haveSink bool + for k, _ := range st.CacheMap { + l, ok := st.sizes[k] + if !ok { + panic(fmt.Sprintf("missing size for %v", k)) + } + if l == 0 { + if haveSink { + panic(fmt.Sprintf("duplicate sink for %v", k)) + } + haveSink = true + } + sizes[k] = l + } + return sizes, nil +} + +func(st *State) SetMenuSize(size uint16) error { + st.menuSize = size + log.Printf("menu size changed to %v", st.menuSize) + return nil +} + +func(st *State) GetMenuSize() uint16 { + return st.menuSize +} + +func(st *State) GetOutputSize() uint32 { + return st.outputSize +} + // Val returns value for key // // Fails if key is not mapped. @@ -372,7 +416,7 @@ func(st *State) Check(key string) bool { return st.frameOf(key) == -1 } -// Size returns size used by values, and remaining size available +// Size returns size used by values and menu, and remaining size available func(st *State) Size() (uint32, uint32) { var l int var c uint16 @@ -381,6 +425,7 @@ func(st *State) Size() (uint32, uint32) { c += st.sizes[k] } r := uint32(l) + r += uint32(st.menuSize) return r, uint32(c)-r } diff --git a/go/vm/runner_test.go b/go/vm/runner_test.go index 30bfa2d..bc6519c 100644 --- a/go/vm/runner_test.go +++ b/go/vm/runner_test.go @@ -34,7 +34,7 @@ type TestStatefulResolver struct { state *state.State } -func (r *TestResource) GetTemplate(sym string) (string, error) { +func (r *TestResource) GetTemplate(sym string, sizer *resource.Sizer) (string, error) { switch sym { case "foo": return "inky pinky blinky clyde", nil