Transaction
1A736DFE292B5B…903953F1AD34
Block 747,742 · index 0 · indexed
Summary
- Hash
- 1A736DFE292B5B7F8454AB8C22BF18CE8700D420E2AD68A687FE903953F1AD34
- Block
- 747,742
- Size
- 33532 bytes
- Gas used
- 48,265,689 / 96,531,318
- Fee
- 96532 ugnot
- Status
- success
Messages
Arguments · 17
- #1definition
- #2definition.gno
- #3package definition import ( "chain/runtime/unsafe" "net/url" "sort" "strings" "gno.land/p/akkadia/beta/accesscontrol" "gno.land/p/nt/bptree/v0" "gno.land/r/akkadia/beta/admin" ) type Field struct { Key string Value string } var definitions *bptree.BPTree func init() { definitions = bptree.NewBPTree32() } // DeleteAll removes all definitions so an administrator can reload the catalog. func DeleteAll(cur realm) { assertCanWrite(0, cur) definitions = bptree.NewBPTree32() } // SetDefinitions accepts URL-escaped keys and query-encoded field objects as paired // comma-separated strings, so MsgCall can pass a batch without realm DTOs. func SetDefinitions(cur realm, keys, values string) { assertCanWrite(0, cur) if len(keys)+len(values) > 131071 { panic("call bytes exceeds 131071") } if keys == "" && values == "" { assertBatchCount(0) } if keys == "" || values == "" { panic("keys and values count mismatch") } type entry struct { Key string Fields []Field } entries := []entry{} keyStart, keyEnd := 0, 0 valueStart, valueEnd := 0, 0 for { assertBatchCount(len(entries) + 1) for keyEnd < len(keys) && keys[keyEnd] != ',' { keyEnd++ } for valueEnd < len(values) && values[valueEnd] != ',' { valueEnd++ } if keyStart == keyEnd || valueStart == valueEnd { panic("empty entry token") } key, err := url.QueryUnescape(keys[keyStart:keyEnd]) if err != nil { panic("invalid encoded key") } attributes, err := url.ParseQuery(values[valueStart:valueEnd]) if err != nil { panic("invalid encoded fields") } if len(attributes) < 1 || len(attributes) > 32 { panic("field count must be between 1 and 32") } fields := make([]Field, 0, len(attributes)) for name, fieldValues := range attributes { if len(fieldValues) != 1 { panic("duplicate field key") } fields = append(fields, Field{name, fieldValues[0]}) } entries = append(entries, entry{key, fields}) keyDone := keyEnd >= len(keys) valueDone := valueEnd >= len(values) if keyDone != valueDone { panic("keys and values count mismatch") } if keyDone { break } keyEnd++ valueEnd++ keyStart, valueStart = keyEnd, valueEnd } // Prepare every final record before writing any state. prepared := make([][]Field, len(entries)) batchBytes := 0 for i, entry := range entries { assertIdentifier(entry.Key, "key", 128) batchBytes += len(entry.Key) + assertFields(entry.Fields) if batchBytes > 65536 { panic("batch bytes exceeds 65536") } for j := 0; j < i; j++ { if entries[j].Key == entry.Key { panic("duplicate entry") } } fields, _ := getFields(entry.Key) for _, update := range entry.Fields { found := false for j := range fields { if fields[j].Key == update.Key { fields[j].Value = update.Value found = true break } } if !found { fields = append(fields, update) } } assertFields(fields) // At most 32 fields; insertion sort avoids another persistent index. for j := 1; j < len(fields); j++ { for k := j; k > 0 && fields[k].Key < fields[k-1].Key; k-- { fields[k], fields[k-1] = fields[k-1], fields[k] } } prepared[i] = fields } for i, entry := range entries { definitions.Set(entry.Key, prepared[i]) } } // Get returns ordered field records. An empty list means the key is absent; // stored definitions always contain at least one field. func Get(key string) []map[string]string { fields, _ := getFields(key) result := make([]map[string]string, 0, len(fields)) for _, field := range fields { result = append(result, map[string]string{"key": field.Key, "value": field.Value}) } return result } func getFields(key string) (fields []Field, found bool) { if !definitions.Has(key) { return nil, false } stored := definitions.Get(key).([]Field) fields = make([]Field, len(stored)) copy(fields, stored) return fields, true } // GetField includes "value" even when it is empty. An empty map means absent. func GetField(key, fieldKey string) map[string]string { fields, _ := getFields(key) for _, field := range fields { if field.Key == fieldKey { return map[string]string{"value": field.Value} } } return map[string]string{} } func GetKeyCount() int { return definitions.Size() } func ListKeys(page, count int) []string { assertPage(page, count) result := []string{} // Compare before multiplication so an extreme page cannot overflow. if definitions.Size() == 0 || page-1 > (definitions.Size()-1)/count { return result } // Sort a query-local view so existing records keep their original storage keys. ordered := make([]string, 0, definitions.Size()) definitions.Iterate("", "", func(key string, _ any) bool { numeric := true for _, r := range key { if r < '0' || r > '9' { numeric = false break } } if numeric { // Keys are at most 128 bytes. Padding avoids integer overflow; the // original key breaks ties such as "01" and "1" without merging them. ordered = append(ordered, "0"+strings.Repeat("0", 128-len(key))+key+":"+key) } else { ordered = append(ordered, "1:"+key) } return false }) sort.Strings(ordered) start := (page - 1) * count end := start + count if end > len(ordered) { end = len(ordered) } for _, key := range ordered[start:end] { result = append(result, key[strings.IndexByte(key, ':')+1:]) } return result } func assertCanWrite(_ int, rlm realm) { accesscontrol.AssertIsAdmin(0, rlm, admin.IsAdmin) // MsgRun has the signer address but an ephemeral /e/<address>/run path. if !rlm.Previous().IsUser() { panic("direct user call or run required") } if len(unsafe.OriginSend()) != 0 { panic("attached coins not allowed") } }
- #4definition_test.gno
- #5package definition import ( "chain" "net/url" "strconv" "strings" "testing" "gno.land/p/nt/bptree/v0" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" ) var definitionAdmin = testutils.TestAddress("definitionAdmin") // Call with cross(cur); each test gets fresh state and a direct admin caller. func setupDefinition(cur realm, t *testing.T) { t.Helper() definitions = bptree.NewBPTree32() testing.SetOriginSend(chain.Coins{}) if admin.GetAdmin() != definitionAdmin { testing.SetOriginCaller(admin.GetAdmin()) testing.SetRealm(testing.NewUserRealm(admin.GetAdmin())) admin.TransferAdmin(cross(cur), definitionAdmin) testing.SetOriginCaller(definitionAdmin) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) admin.AcceptAdmin(cross(cur)) } testing.SetOriginCaller(definitionAdmin) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) } func TestStorageMergeAndCopies(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) uassert.Equal(t, 0, GetKeyCount()) fields := []Field{{"z", ""}, {"name", "돌"}, {"description", " 줄\n바꿈 ` <tag> "}} setTestDefinition(cross(cur), "1101000", fields) fields[1].Value = "changed" setTestDefinition(cross(cur), "1101000", []Field{{"name", "Stone"}, {"a", "added"}}) got := Get("1101000") assertFieldList(t, []Field{{"a", "added"}, {"description", " 줄\n바꿈 ` <tag> "}, {"name", "Stone"}, {"z", ""}}, got) got[0]["value"] = "mutated return" field := GetField("1101000", "a") field["value"] = "mutated field return" value, found := GetField("1101000", "a")["value"] uassert.True(t, found) uassert.Equal(t, "added", value) value, found = GetField("1101000", "z")["value"] uassert.True(t, found) uassert.Equal(t, "", value) _, found = GetField("1101000", "missing")["value"] uassert.False(t, found) uassert.Equal(t, len(Get("missing")), 0) uassert.Equal(t, 1, GetKeyCount()) setTestDefinition(cross(cur), " A : 한글 ", []Field{{" 이름 ", ""}}) assertStrings(t, []string{"1101000", " A : 한글 "}, ListKeys(1, 50)) } func TestDeleteAllAndReload(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) batch := []testEntry{} for i := 100; i < 200; i++ { batch = append(batch, testEntry{strconv.Itoa(i), []Field{{"old", "discard"}}}) } setTestDefinitions(cross(cur), batch) uassert.Equal(t, 100, GetKeyCount()) DeleteAll(cross(cur)) uassert.Equal(t, 0, GetKeyCount()) assertStrings(t, []string{}, ListKeys(1, 50)) for _, entry := range batch { uassert.Equal(t, len(Get(entry.Key)), 0) _, found := GetField(entry.Key, "old")["value"] uassert.False(t, found) } uassert.True(t, strings.Contains(Render(""), "No entries")) DeleteAll(cross(cur)) setTestDefinition(cross(cur), "100", []Field{{"new", "replacement"}}) fields := Get("100") assertFieldList(t, []Field{{"new", "replacement"}}, fields) uassert.Equal(t, 1, GetKeyCount()) } func TestBatchIdempotenceAndAtomicValidation(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) entries := []testEntry{{"a", []Field{{"name", "old"}}}, {"b", []Field{{"name", "other"}}}} setTestDefinitions(cross(cur), entries) setTestDefinitions(cross(cur), entries) uassert.Equal(t, 2, GetKeyCount()) for _, tc := range []struct { name string entry testEntry errMsg string }{ {"duplicate entry", testEntry{"a", []Field{{"n", "v"}}}, "duplicate entry"}, {"duplicate field", testEntry{"c", []Field{{"n", "v"}, {"n", "v"}}}, "duplicate field key"}, {"invalid key", testEntry{"", []Field{{"n", "v"}}}, "empty entry token"}, {"invalid field", testEntry{"c", []Field{{"\n", "v"}}}, "field key is required"}, {"empty fields", testEntry{"c", nil}, "empty entry token"}, } { t.Run(tc.name, func(t *testing.T) { testing.SetRealm(testing.NewUserRealm(definitionAdmin)) batch := []testEntry{{"a", []Field{{"name", "new"}}}, tc.entry} uassert.AbortsWithMessage(t, cur, tc.errMsg, func() { setTestDefinitions(cross(cur), batch) }) value, _ := GetField("a", "name")["value"] uassert.Equal(t, "old", value) uassert.Equal(t, 2, GetKeyCount()) }) } } func TestLimitsAndMergedRecord(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) full := []Field{} for i := 0; i < 32; i++ { full = append(full, Field{strconv.Itoa(i), "v"}) } setTestDefinition(cross(cur), "full", full) uassert.AbortsWithMessage(t, cur, "field count must be between 1 and 32", func() { setTestDefinition(cross(cur), "full", []Field{{"extra", "v"}}) }) setTestDefinition(cross(cur), "bytes", []Field{{"a", strings.Repeat("x", 16383)}}) uassert.AbortsWithMessage(t, cur, "entry bytes exceeds 16384", func() { setTestDefinition(cross(cur), "bytes", []Field{{"b", ""}}) }) setTestDefinition(cross(cur), "bytes", []Field{{"a", "small"}, {"b", ""}}) for _, tc := range []struct{ key, errMsg string }{ {"", "keys and values count mismatch"}, {" \t", "key is required"}, {"x\n", "invalid key"}, {string([]byte{255}), "invalid key"}, {strings.Repeat("가", 43), "key bytes exceeds 128"}, } { uassert.AbortsWithMessage(t, cur, tc.errMsg, func() { setTestDefinition(cross(cur), tc.key, []Field{{"n", "v"}}) }) } setTestDefinition(cross(cur), strings.Repeat("k", 128), []Field{{strings.Repeat("f", 128), ""}}) uassert.AbortsWithMessage(t, cur, "field key bytes exceeds 128", func() { setTestDefinition(cross(cur), "x", []Field{{strings.Repeat("f", 129), ""}}) }) uassert.AbortsWithMessage(t, cur, "invalid field value", func() { setTestDefinition(cross(cur), "x", []Field{{"n", string([]byte{255})}}) }) uassert.AbortsWithMessage(t, cur, "batch count must be between 1 and 100", func() { setTestDefinitions(cross(cur), nil) }) uassert.AbortsWithMessage(t, cur, "batch count must be between 1 and 100", func() { SetDefinitions(cross(cur), strings.Repeat("x,", 100)+"x", strings.Repeat("n=v,", 100)+"n=v") }) total := GetKeyCount() batch := []testEntry{} for i := 0; i < 5; i++ { batch = append(batch, testEntry{strconv.Itoa(i), []Field{{"a", strings.Repeat("x", 16383)}}}) } uassert.AbortsWithMessage(t, cur, "batch bytes exceeds 65536", func() { setTestDefinitions(cross(cur), batch) }) uassert.Equal(t, total, GetKeyCount()) } func TestPagination(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) assertStrings(t, []string{}, ListKeys(1, 50)) batch := []testEntry{} for i := 99; i >= 0; i-- { batch = append(batch, testEntry{strconv.Itoa(i + 100), []Field{{"n", "v"}}}) } setTestDefinitions(cross(cur), batch) uassert.Equal(t, 100, GetKeyCount()) assertStrings(t, []string{"100", "101"}, ListKeys(1, 2)) assertStrings(t, []string{"198", "199"}, ListKeys(50, 2)) assertStrings(t, []string{}, ListKeys(51, 2)) assertStrings(t, []string{}, ListKeys(int(9223372036854775807), 50)) uassert.PanicsWithMessage(t, cur, "page must be positive", func() { ListKeys(0, 1) }) uassert.PanicsWithMessage(t, cur, "count must be between 1 and 50", func() { ListKeys(1, 51) }) keys := ListKeys(1, 1) keys[0] = "changed" assertStrings(t, []string{"100"}, ListKeys(1, 1)) } func TestWriteAuthorityAndPayment(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) setTestDefinition(cross(cur), "kept", []Field{{"name", "preserved"}}) operator := testutils.TestAddress("definitionOperator") admin.SetOperator(cross(cur), operator) for _, caller := range []address{testutils.TestAddress("definitionUser"), operator} { testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetDefinitions(cross(cur), "x", "n=v") }) } testing.SetOriginCaller(definitionAdmin) testing.SetRealm(testing.NewCodeRealm("gno.land/r/example/definition_relay")) uassert.AbortsWithMessage(t, cur, "admin access required", func() { DeleteAll(cross(cur)) }) uassert.AbortsWithMessage(t, cur, "admin access required", func() { SetDefinitions(cross(cur), "x", "n=v") }) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) testing.SetOriginSend(chain.Coins{{"ugnot", 1}}) uassert.AbortsWithMessage(t, cur, "attached coins not allowed", func() { DeleteAll(cross(cur)) }) uassert.AbortsWithMessage(t, cur, "attached coins not allowed", func() { SetDefinitions(cross(cur), "x", "n=v") }) testing.SetOriginSend(chain.Coins{}) uassert.Equal(t, 1, GetKeyCount()) value, found := GetField("kept", "name")["value"] uassert.True(t, found) uassert.Equal(t, "preserved", value) stale := captureDefinitionRealm(cross(cur)) uassert.PanicsWithMessage(t, cur, "spoofed realm", func() { assertCanWrite(0, stale) }) } func assertStrings(t *testing.T, expected, actual []string) { t.Helper() if !uassert.Equal(t, len(expected), len(actual)) { return } for i := range expected { uassert.Equal(t, expected[i], actual[i]) } } func assertFieldList(t *testing.T, expected []Field, actual []map[string]string) { t.Helper() if !uassert.Equal(t, len(expected), len(actual)) { return } for i := range expected { uassert.Equal(t, expected[i].Key, actual[i]["key"]) uassert.Equal(t, expected[i].Value, actual[i]["value"]) } } func captureDefinitionRealm(cur realm) realm { return cur } func TestSetDefinitionsAndEscapedFields(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) key := "한글 😀,+&=%;" fieldKey := "field,+&=%;" value := "한글 😀\n\"\\`\x00,+&=%; } //" SetDefinitions(cross(cur), url.QueryEscape(key)+",1101000", url.QueryEscape(fieldKey)+"="+url.QueryEscape(value)+"&empty=,key=wooden_sword") got, found := GetField(key, fieldKey)["value"] uassert.True(t, found) uassert.Equal(t, value, got) got, found = GetField(key, "empty")["value"] uassert.True(t, found) uassert.Equal(t, "", got) SetDefinitions(cross(cur), url.QueryEscape(key), "new=added") got, _ = GetField(key, fieldKey)["value"] uassert.Equal(t, value, got) uassert.Equal(t, 2, GetKeyCount()) got, _ = GetField("1101000", "key")["value"] uassert.Equal(t, "wooden_sword", got) DeleteAll(cross(cur)) SetDefinitions(cross(cur), "1101000", "fresh=value") _, found = GetField("1101000", "key")["value"] uassert.False(t, found) } func TestSetDefinitionsRejectsInvalidInputAtomically(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) setTestDefinition(cross(cur), "kept", []Field{{"name", "old"}}) for _, tc := range []struct{ name, keys, values, errMsg string }{ {"empty", "", "", "batch count must be between 1 and 100"}, {"missing keys", "", "n=v", "keys and values count mismatch"}, {"missing values", "x", "", "keys and values count mismatch"}, {"short values", "kept,x", "name=new", "keys and values count mismatch"}, {"short keys", "kept", "name=new,n=v", "keys and values count mismatch"}, {"empty key", "kept,,x", "name=new,n=v,n=v", "empty entry token"}, {"empty fields", "kept,x", "name=new,", "empty entry token"}, {"trailing comma", "kept,", "name=new,", "empty entry token"}, {"duplicate key", "kept,k%65pt", "name=new,n=v", "duplicate entry"}, {"duplicate field", "kept", "name=new&n=1&%6E=2", "duplicate field key"}, {"invalid escape key", "kept,%zz", "name=new,n=v", "invalid encoded key"}, {"invalid escape value", "kept,x", "name=new,n=%zz", "invalid encoded fields"}, {"invalid utf8 key", "kept,%FF", "name=new,n=v", "invalid key"}, {"invalid utf8 value", "kept,x", "name=new,n=%FF", "invalid field value"}, {"invalid field", "kept,x", "name=new,%0A=v", "field key is required"}, {"encoded empty key", "kept,+", "name=new,n=v", "key is required"}, {"no fields", "kept,x", "name=new,&", "field count must be between 1 and 32"}, {"encoded limit", strings.Repeat("x", 131072), "n=v", "call bytes exceeds 131071"}, {"count limit", strings.Repeat("x,", 100) + "last", strings.Repeat("n=v,", 100) + "n=v", "batch count must be between 1 and 100"}, {"raw limit", "a,b,c,d,e", strings.Repeat("a="+strings.Repeat("x", 16383)+",", 4) + "a=" + strings.Repeat("x", 16383), "batch bytes exceeds 65536"}, } { t.Run(tc.name, func(t *testing.T) { testing.SetRealm(testing.NewUserRealm(definitionAdmin)) uassert.AbortsWithMessage(t, cur, tc.errMsg, func() { SetDefinitions(cross(cur), tc.keys, tc.values) }) value, found := GetField("kept", "name")["value"] uassert.True(t, found) uassert.Equal(t, "old", value) uassert.Equal(t, 1, GetKeyCount()) }) } } func TestSetDefinitionsBatchAndMergedLimits(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) keys, values, fields := "", "", "" for i := 0; i < 32; i++ { if i > 0 { fields += "&" } fields += strconv.Itoa(i) + "=value" } for i := 0; i < 100; i++ { if i > 0 { keys += "," values += "," } keys += strconv.Itoa(i) values += fields } SetDefinitions(cross(cur), keys, values) uassert.Equal(t, 100, GetKeyCount()) uassert.AbortsWithMessage(t, cur, "field count must be between 1 and 32", func() { SetDefinitions(cross(cur), "0", "extra=value") }) got := Get("0") uassert.Equal(t, 32, len(got)) uassert.AbortsWithMessage(t, cur, "field count must be between 1 and 32", func() { SetDefinitions(cross(cur), "new", fields+"&extra=value") }) } // Test fixtures use the same encoded input boundary as the importer. type testEntry struct { Key string Fields []Field } func setTestDefinition(cur realm, key string, fields []Field) { setTestDefinitions(cur, []testEntry{{key, fields}}) } func setTestDefinitions(cur realm, entries []testEntry) { keys, values := "", "" for i, entry := range entries { if i > 0 { keys += "," values += "," } keys += url.QueryEscape(entry.Key) for j, field := range entry.Fields { if j > 0 { values += "&" } values += url.QueryEscape(field.Key) + "=" + url.QueryEscape(field.Value) } } SetDefinitions(cur, keys, values) } func TestNumericKeyPagination(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) huge := strings.Repeat("9", 128) keys := []string{"1000000", "100", "10", "2", "1", "0", "01", "00", "item:2", "item:10", "18446744073709551616", huge} entries := []testEntry{} for _, key := range keys { entries = append(entries, testEntry{key, []Field{{"name", key}}}) } setTestDefinitions(cross(cur), entries) assertStrings(t, []string{"0", "00", "01", "1", "2"}, ListKeys(1, 5)) assertStrings(t, []string{"10", "100", "1000000", "18446744073709551616", huge}, ListKeys(2, 5)) assertStrings(t, []string{"item:10", "item:2"}, ListKeys(3, 5)) assertStrings(t, []string{}, ListKeys(4, 5)) for _, key := range keys { value, found := GetField(key, "name")["value"] uassert.True(t, found) uassert.Equal(t, key, value) } SetDefinitions(cross(cur), "2", "name=updated") uassert.Equal(t, len(keys), GetKeyCount()) assertStrings(t, []string{"0", "00", "01", "1", "2"}, ListKeys(1, 5)) DeleteAll(cross(cur)) assertStrings(t, []string{}, ListKeys(1, 5)) }
- #6external_test.gno
- #7package definition_test import ( "chain" "chain/runtime" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" "gno.land/r/akkadia/beta/admin" "gno.land/r/akkadia/beta/definition" ) func TestExternalDefinitionsAndAdminRun(cur realm, t *testing.T) { caller := testutils.TestAddress("definitionExternal") testing.SetOriginCaller(admin.GetAdmin()) testing.SetRealm(testing.NewUserRealm(admin.GetAdmin())) admin.TransferAdmin(cross(cur), caller) testing.SetOriginCaller(caller) testing.SetRealm(testing.NewUserRealm(caller)) admin.AcceptAdmin(cross(cur)) testing.SetOriginSend(chain.Coins{}) testing.SetRealm(runtime.NewRealm(caller, "gno.land/e/"+caller.String()+"/run")) definition.SetDefinitions(cross(cur), "x", "name=before") value, found := definition.GetField("x", "name")["value"] uassert.True(t, found) uassert.Equal(t, "before", value) // A forged code path with the admin address still is not a user call/run. testing.SetRealm(runtime.NewRealm(caller, "gno.land/r/example/forged_definition_admin")) uassert.AbortsWithMessage(t, cur, "direct user call or run required", func() { definition.DeleteAll(cross(cur)) }) uassert.AbortsWithMessage(t, cur, "direct user call or run required", func() { definition.SetDefinitions(cross(cur), "x", "name=forged") }) value, _ = definition.GetField("x", "name")["value"] uassert.Equal(t, "before", value) testing.SetRealm(runtime.NewRealm(caller, "gno.land/e/"+caller.String()+"/run")) definition.DeleteAll(cross(cur)) uassert.Equal(t, len(definition.Get("x")), 0) definition.SetDefinitions(cross(cur), "x", "fresh=value") _, found = definition.GetField("x", "name")["value"] uassert.False(t, found) value, found = definition.GetField("x", "fresh")["value"] uassert.True(t, found) uassert.Equal(t, "value", value) }
- #8gnomod.toml
- #9module = "gno.land/r/akkadia/beta/definition" gno = "0.9"
- #10render.gno
- #11package definition import ( "html" "net/url" "strconv" "strings" "unicode" ) func Render(path string) string { u, err := url.Parse(path) if err != nil { return renderError("Invalid URL") } query, err := url.ParseQuery(u.RawQuery) if err != nil { return renderError("Invalid query") } page := 1 if raw := query.Get("page"); raw != "" { page, err = strconv.Atoi(raw) if err != nil || page < 1 { return renderError("Invalid page") } } key := query.Get("key") output := "# AKKADIA Definition\n\n[All items](./definition:)\n\n" + renderSearchForm(key) switch u.Path { case "", "list": total := GetKeyCount() output += "## Items (" + strconv.Itoa(total) + ")\n\n" keys := ListKeys(page, 20) if len(keys) == 0 { output += "No entries on this page.\n" } for _, key := range keys { label := key if itemKey, found := GetField(key, "key")["value"]; found && itemKey != "" { label += " — " + itemKey } output += "* [" + renderLabel(label) + "](./definition:search?key=" + url.QueryEscape(key) + "&page=" + strconv.Itoa(page) + ")\n" } pages := 1 if total > 0 { pages = (total-1)/20 + 1 } output += "\nPage " + strconv.Itoa(page) + " / " + strconv.Itoa(pages) + "\n" return output + renderPages("./definition:list?", page, total) case "search": if strings.TrimSpace(key) == "" { return output + "Key is required\n" } fields, found := getFields(key) if !found { return output + "Entry not found\n" } output += "[Back to list](./definition:list?page=" + strconv.Itoa(page) + ")\n\n" output += "## " + renderLabel(key) + "\n\n" for _, field := range fields { output += "### " + renderLabel(field.Key) + "\n\n" link, err := url.Parse(field.Value) isLink := err == nil && (link.Scheme == "http" || link.Scheme == "https") && link.Hostname() != "" && link.Opaque == "" for _, r := range field.Value { if unicode.IsSpace(r) || unicode.IsControl(r) { isLink = false } } if isLink { // Angle-delimited Markdown destinations retain parentheses and ampersands. target := strings.ReplaceAll(strings.ReplaceAll(link.String(), "<", "%3C"), ">", "%3E") output += "[" + renderLabel(field.Value) + "](<" + target + ">)\n\n" } else { // A fence longer than any user fence keeps HTML and Markdown literal. longest, run := 2, 0 for _, r := range field.Value { if r == '`' { run++ if run > longest { longest = run } } else { run = 0 } } fence := strings.Repeat("`", longest+1) output += fence + "\n" + field.Value + "\n" + fence + "\n\n" } } return output default: return renderError("Page not found") } } func renderLabel(value string) string { // Gnoweb's TOC unescapes Markdown punctuation but leaves HTML entities literal. var output strings.Builder for _, r := range value { if unicode.IsControl(r) { output.WriteString(" ") continue } if (r >= '!' && r <= '/') || (r >= ':' && r <= '@') || (r >= '[' && r <= '`') || (r >= '{' && r <= '~') { output.WriteString("\\") } output.WriteRune(r) } return output.String() } func renderPages(base string, page, total int) string { output := "\n" if page > 1 { output += "[Previous](" + base + "page=" + strconv.Itoa(page-1) + ") " } if total > 0 && page <= (total-1)/20 { output += "[Next](" + base + "page=" + strconv.Itoa(page+1) + ")" } return output + "\n" } func renderError(message string) string { return "# Definition\n\n" + message + "\n\n[All items](./definition:)\n\n" + renderSearchForm("") } func renderSearchForm(key string) string { return "<gno-form path=\"search\">\n <gno-input name=\"key\" value=\"" + html.EscapeString(key) + "\" placeholder=\"Item ID (e.g. 1101000)\" />\n</gno-form>\n\n" }
- #12render_test.gno
- #13package definition import ( "net/url" "strconv" "strings" "testing" "gno.land/p/nt/uassert/v0" ) func TestRenderSearchAndEscaping(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) key := "한글\"<&[key] & ? :" setTestDefinition(cross(cur), key, []Field{ {"text", "한글\n```\n<script>alert(1)</script>\n**bold**"}, {"link", "https://example.com/a(b)?x=1&y=2"}, {"unsafe", "javascript:alert(1)"}, {"mixed", "https://example.com/ text"}, {"empty", ""}, }) page := Render("") uassert.True(t, strings.Contains(page, "<gno-form path=\"search\">")) uassert.True(t, strings.Contains(page, "name=\"key\"")) uassert.False(t, strings.Contains(page, "name=\"type\"")) uassert.True(t, strings.Contains(page, "search?key="+url.QueryEscape(key))) result := Render("search?key=" + url.QueryEscape(key) + "&page=2") uassert.True(t, strings.Contains(result, "value=\"한글"<&[key] & ? :\"")) uassert.True(t, strings.Contains(result, "[Back to list](./definition:list?page=2)")) uassert.True(t, strings.Contains(result, "````\n한글\n```\n<script>alert(1)</script>\n**bold**\n````")) uassert.True(t, strings.Contains(result, "](<https://example.com/a(b)?x=1&y=2>)")) uassert.True(t, strings.Contains(result, "```\njavascript:alert(1)\n```")) uassert.True(t, strings.Contains(result, "```\nhttps://example.com/ text\n```")) for _, tc := range []struct{ path, message string }{ {"search", "Key is required"}, {"search?key=missing", "Entry not found"}, {"list?page=0", "Invalid page"}, {"?page=999999999999999999999999", "Invalid page"}, {"?key=%zz", "Invalid query"}, {"other", "Page not found"}, } { result := Render(tc.path) uassert.True(t, strings.Contains(result, tc.message)) uassert.True(t, strings.Contains(result, "<gno-form path=\"search\">")) } uassert.True(t, strings.Contains(Render("search?key=missing"), "value=\"missing\"")) } func TestRenderPagination(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) uassert.True(t, strings.Contains(Render(""), "No entries")) batch := []testEntry{} for i := 100; i < 141; i++ { batch = append(batch, testEntry{strconv.Itoa(i), []Field{{"key", "item_" + strconv.Itoa(i)}}}) } setTestDefinitions(cross(cur), batch) for _, tc := range []struct { page, count int previous, next bool }{ {1, 20, false, true}, {2, 20, true, true}, {3, 1, true, false}, {4, 0, true, false}, } { page := Render("list?page=" + strconv.Itoa(tc.page)) uassert.Equal(t, tc.count, strings.Count(page, "./definition:search?key=")) uassert.Equal(t, tc.previous, strings.Contains(page, "[Previous]")) uassert.Equal(t, tc.next, strings.Contains(page, "[Next]")) uassert.True(t, strings.Contains(page, "Page "+strconv.Itoa(tc.page)+" / 3")) } first := Render("") uassert.True(t, strings.Contains(first, "* [100 — item\\_100](./definition:search?key=100&page=1)")) uassert.True(t, strings.Contains(first, "search?key=119&page=1")) uassert.False(t, strings.Contains(first, "search?key=120")) uassert.False(t, strings.Contains(Render("list?page=9223372036854775807"), "[Next]")) } func TestRenderReadableHeadings(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) SetDefinitions(cross(cur), "1103015", "abilities=value&displayMetadata=value&key=wooden_sword") page := Render("search?key=1103015&page=10") uassert.True(t, strings.Contains(page, "## 1103015\n")) uassert.True(t, strings.Contains(page, "### abilities\n")) uassert.True(t, strings.Contains(page, "### displayMetadata\n")) uassert.False(t, strings.Contains(page, "&#")) uassert.True(t, strings.Contains(Render(""), "[1103015 — wooden\\_sword]")) // Literal entities and markup must remain text, including in the TOC. uassert.Equal(t, `\&\#49\; \<script\> \[x\]\(url\) \*bold\* \~strike\~`, renderLabel("1 <script> [x](url) *bold* ~strike~")) uassert.Equal(t, "한글 😀 next line", renderLabel("한글 😀\nnext\rline")) } func TestRenderNumericPageOrder(cur realm, t *testing.T) { setupDefinition(cross(cur), t) testing.SetRealm(testing.NewUserRealm(definitionAdmin)) entries := []testEntry{} for i := 21; i >= 0; i-- { entries = append(entries, testEntry{strconv.Itoa(i), []Field{{"name", "item"}}}) } setTestDefinitions(cross(cur), entries) first := Render("") previous := -1 for i := 0; i < 20; i++ { position := strings.Index(first, "](./definition:search?key="+strconv.Itoa(i)+"&page=1)") uassert.True(t, position > previous) previous = position } uassert.False(t, strings.Contains(first, "search?key=20&")) second := Render("list?page=2") uassert.Equal(t, 2, strings.Count(second, "](./definition:search?key=")) uassert.True(t, strings.Contains(second, "search?key=20&page=2")) uassert.True(t, strings.Contains(second, "search?key=21&page=2")) }
- #14validation.gno
- #15package definition import ( "strconv" "strings" "unicode" "unicode/utf8" ) func assertIdentifier(value, name string, limit int) { if strings.TrimSpace(value) == "" { panic(name + " is required") } if len(value) > limit { panic(name + " bytes exceeds " + strconv.Itoa(limit)) } if !utf8.ValidString(value) { panic("invalid " + name) } for _, r := range value { if unicode.IsControl(r) { panic("invalid " + name) } } } func assertFields(fields []Field) int { if len(fields) < 1 || len(fields) > 32 { panic("field count must be between 1 and 32") } size := 0 for i, field := range fields { assertIdentifier(field.Key, "field key", 128) if !utf8.ValidString(field.Value) { panic("invalid field value") } for j := 0; j < i; j++ { if fields[j].Key == field.Key { panic("duplicate field key") } } size += len(field.Key) + len(field.Value) if size > 16384 { panic("entry bytes exceeds 16384") } } return size } func assertBatchCount(count int) { if count < 1 || count > 100 { panic("batch count must be between 1 and 100") } } func assertPage(page, count int) { if page < 1 { panic("page must be positive") } if count < 1 || count > 50 { panic("count must be between 1 and 50") } }
- #16/gno.MemPackageType
- #17 MPUserAll
Result log
msg:0,success:true,log:,events:[]