From 6e38702f73bd8a7820999a3262df6fc08d0c2af6 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:19:59 +0100 Subject: [PATCH 1/5] feat(examples): add p/demo/avl/rotree Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rotree/gno.mod | 1 + .../gno.land/p/demo/avl/rotree/rotree.gno | 162 +++++++++++++ .../p/demo/avl/rotree/rotree_test.gno | 222 ++++++++++++++++++ examples/gno.land/p/demo/avl/tree.gno | 21 ++ 4 files changed, 406 insertions(+) create mode 100644 examples/gno.land/p/demo/avl/rotree/gno.mod create mode 100644 examples/gno.land/p/demo/avl/rotree/rotree.gno create mode 100644 examples/gno.land/p/demo/avl/rotree/rotree_test.gno diff --git a/examples/gno.land/p/demo/avl/rotree/gno.mod b/examples/gno.land/p/demo/avl/rotree/gno.mod new file mode 100644 index 00000000000..9110d8537d6 --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/avl/rotree \ No newline at end of file diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno new file mode 100644 index 00000000000..5d4818d112a --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -0,0 +1,162 @@ +// Package rotree provides a read-only wrapper for avl.Tree with safe value transformation. +// +// It is useful when you want to expose a read-only view of a tree while ensuring that +// the sensitive data cannot be modified. +// +// Example: +// +// // Define a user structure with sensitive data +// type User struct { +// Name string +// Balance int +// Internal string // sensitive field +// } +// +// // Create and populate the original tree +// privateTree := avl.NewTree() +// privateTree.Set("alice", &User{ +// Name: "Alice", +// Balance: 100, +// Internal: "sensitive", +// }) +// +// // Create a safe transformation function that copies the struct +// // while excluding sensitive data +// makeEntrySafeFn := func(v interface{}) interface{} { +// u := v.(*User) +// return &User{ +// Name: u.Name, +// Balance: u.Balance, +// Internal: "", // omit sensitive data +// } +// } +// +// // Create a read-only view of the tree +// PublicTree := rotree.Wrap(tree, makeEntrySafeFn) +// +// // Safely access the data +// value, _ := roTree.Get("alice") +// user := value.(*User) +// // user.Name == "Alice" +// // user.Balance == 100 +// // user.Internal == "" (sensitive data is filtered) +package rotree + +import ( + "gno.land/p/demo/avl" +) + +// Wrap creates a new ReadOnlyTree from an existing avl.Tree and a safety transformation function. +// If makeEntrySafeFn is nil, values will be returned as-is without transformation. +// +// makeEntrySafeFn is a function that transforms a tree entry into a safe version that can be exposed to external users. +// This function should be implemented based on the specific safety requirements of your use case: +// +// 1. No-op transformation: For primitive types (int, string, etc.) or already safe objects, +// simply pass nil as the makeEntrySafeFn to return values as-is. +// +// 2. Defensive copying: For mutable types like slices or maps, you should create a deep copy +// to prevent modification of the original data. +// Example: func(v interface{}) interface{} { return append([]int{}, v.([]int)...) } +// +// 3. Read-only wrapper: Return a read-only version of the object that implements +// a limited interface. +// Example: func(v interface{}) interface{} { return NewReadOnlyObject(v) } +// +// 4. DAO transformation: Transform the object into a data access object that +// controls how the underlying data can be accessed. +// Example: func(v interface{}) interface{} { return NewDAO(v) } +// +// The function ensures that the returned object is safe to expose to untrusted code, +// preventing unauthorized modifications to the original data structure. +func Wrap(tree *avl.Tree, makeEntrySafeFn func(interface{}) interface{}) *ReadOnlyTree { + return &ReadOnlyTree{ + tree: tree, + makeEntrySafeFn: makeEntrySafeFn, + } +} + +// ReadOnlyTree wraps an avl.Tree and provides read-only access. +type ReadOnlyTree struct { + tree *avl.Tree + makeEntrySafeFn func(interface{}) interface{} +} + +// Verify that ReadOnlyTree implements TreeInterface +var _ avl.TreeInterface = (*ReadOnlyTree)(nil) + +// getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value +func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { + if roTree.makeEntrySafeFn == nil { + return value + } + return roTree.makeEntrySafeFn(value) +} + +// Size returns the number of key-value pairs in the tree. +func (roTree *ReadOnlyTree) Size() int { + return roTree.tree.Size() +} + +// Has checks whether a key exists in the tree. +func (roTree *ReadOnlyTree) Has(key string) bool { + return roTree.tree.Has(key) +} + +// Get retrieves the value associated with the given key, converted to a safe format. +func (roTree *ReadOnlyTree) Get(key string) (interface{}, bool) { + value, exists := roTree.tree.Get(key) + if !exists { + return nil, false + } + return roTree.getSafeValue(value), true +} + +// GetByIndex retrieves the key-value pair at the specified index in the tree, with the value converted to a safe format. +func (roTree *ReadOnlyTree) GetByIndex(index int) (string, interface{}) { + key, value := roTree.tree.GetByIndex(index) + return key, roTree.getSafeValue(value) +} + +// Iterate performs an in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) Iterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.Iterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterate performs a reverse in-order traversal of the tree within the specified key range. +func (roTree *ReadOnlyTree) ReverseIterate(start, end string, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterate(start, end, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// IterateByOffset performs an in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) IterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.IterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// ReverseIterateByOffset performs a reverse in-order traversal of the tree starting from the specified offset. +func (roTree *ReadOnlyTree) ReverseIterateByOffset(offset int, count int, cb avl.IterCbFn) bool { + return roTree.tree.ReverseIterateByOffset(offset, count, func(key string, value interface{}) bool { + return cb(key, roTree.getSafeValue(value)) + }) +} + +// Set is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Set(key string, value interface{}) bool { + panic("Set operation not supported on ReadOnlyTree") +} + +// Remove is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) Remove(key string) (value interface{}, removed bool) { + panic("Remove operation not supported on ReadOnlyTree") +} + +// RemoveByIndex is not supported on ReadOnlyTree and will panic. +func (roTree *ReadOnlyTree) RemoveByIndex(index int) (key string, value interface{}) { + panic("RemoveByIndex operation not supported on ReadOnlyTree") +} diff --git a/examples/gno.land/p/demo/avl/rotree/rotree_test.gno b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno new file mode 100644 index 00000000000..fbc14bd688d --- /dev/null +++ b/examples/gno.land/p/demo/avl/rotree/rotree_test.gno @@ -0,0 +1,222 @@ +package rotree + +import ( + "testing" + + "gno.land/p/demo/avl" +) + +func TestExample(t *testing.T) { + // User represents our internal data structure + type User struct { + ID string + Name string + Balance int + Internal string // sensitive internal data + } + + // Create and populate the original tree with user pointers + tree := avl.NewTree() + tree.Set("alice", &User{ + ID: "1", + Name: "Alice", + Balance: 100, + Internal: "sensitive_data_1", + }) + tree.Set("bob", &User{ + ID: "2", + Name: "Bob", + Balance: 200, + Internal: "sensitive_data_2", + }) + + // Define a makeEntrySafeFn that: + // 1. Creates a defensive copy of the User struct + // 2. Omits sensitive internal data + makeEntrySafeFn := func(v interface{}) interface{} { + originalUser := v.(*User) + return &User{ + ID: originalUser.ID, + Name: originalUser.Name, + Balance: originalUser.Balance, + Internal: "", // Omit sensitive data + } + } + + // Create a read-only view of the tree + roTree := Wrap(tree, makeEntrySafeFn) + + // Test retrieving and verifying a user + t.Run("Get User", func(t *testing.T) { + // Get user from read-only tree + value, exists := roTree.Get("alice") + if !exists { + t.Fatal("User 'alice' not found") + } + + user := value.(*User) + + // Verify user data is correct + if user.Name != "Alice" || user.Balance != 100 { + t.Errorf("Unexpected user data: got name=%s balance=%d", user.Name, user.Balance) + } + + // Verify sensitive data is not exposed + if user.Internal != "" { + t.Error("Sensitive data should not be exposed") + } + + // Verify it's a different instance than the original + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if user == originalUser { + t.Error("Read-only tree should return a copy, not the original pointer") + } + }) + + // Test iterating over users + t.Run("Iterate Users", func(t *testing.T) { + count := 0 + roTree.Iterate("", "", func(key string, value interface{}) bool { + user := value.(*User) + // Verify each user has empty Internal field + if user.Internal != "" { + t.Error("Sensitive data exposed during iteration") + } + count++ + return false + }) + + if count != 2 { + t.Errorf("Expected 2 users, got %d", count) + } + }) + + // Verify that modifications to the returned user don't affect the original + t.Run("Modification Safety", func(t *testing.T) { + value, _ := roTree.Get("alice") + user := value.(*User) + + // Try to modify the returned user + user.Balance = 999 + user.Internal = "hacked" + + // Verify original is unchanged + originalValue, _ := tree.Get("alice") + originalUser := originalValue.(*User) + if originalUser.Balance != 100 || originalUser.Internal != "sensitive_data_1" { + t.Error("Original user data was modified") + } + }) +} + +func TestReadOnlyTree(t *testing.T) { + // Example of a makeEntrySafeFn that appends "_readonly" to demonstrate transformation + makeEntrySafeFn := func(value interface{}) interface{} { + return value.(string) + "_readonly" + } + + tree := avl.NewTree() + tree.Set("key1", "value1") + tree.Set("key2", "value2") + tree.Set("key3", "value3") + + roTree := Wrap(tree, makeEntrySafeFn) + + tests := []struct { + name string + key string + expected interface{} + exists bool + }{ + {"ExistingKey1", "key1", "value1_readonly", true}, + {"ExistingKey2", "key2", "value2_readonly", true}, + {"NonExistingKey", "key4", nil, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, exists := roTree.Get(tt.key) + if exists != tt.exists || value != tt.expected { + t.Errorf("For key %s, expected %v (exists: %v), got %v (exists: %v)", tt.key, tt.expected, tt.exists, value, exists) + } + }) + } +} + +// Add example tests showing different makeEntrySafeFn implementations +func TestMakeEntrySafeFnVariants(t *testing.T) { + tree := avl.NewTree() + tree.Set("slice", []int{1, 2, 3}) + tree.Set("map", map[string]int{"a": 1}) + + tests := []struct { + name string + makeEntrySafeFn func(interface{}) interface{} + key string + validate func(t *testing.T, value interface{}) + }{ + { + name: "Defensive Copy Slice", + makeEntrySafeFn: func(v interface{}) interface{} { + original := v.([]int) + return append([]int{}, original...) + }, + key: "slice", + validate: func(t *testing.T, value interface{}) { + slice := value.([]int) + // Modify the returned slice + slice[0] = 999 + // Verify original is unchanged + originalValue, _ := tree.Get("slice") + original := originalValue.([]int) + if original[0] != 1 { + t.Error("Original slice was modified") + } + }, + }, + // Add more test cases for different makeEntrySafeFn implementations + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + roTree := Wrap(tree, tt.makeEntrySafeFn) + value, exists := roTree.Get(tt.key) + if !exists { + t.Fatal("Key not found") + } + tt.validate(t, value) + }) + } +} + +func TestNilMakeEntrySafeFn(t *testing.T) { + // Create a tree with some test data + tree := avl.NewTree() + originalValue := []int{1, 2, 3} + tree.Set("test", originalValue) + + // Create a ReadOnlyTree with nil makeEntrySafeFn + roTree := Wrap(tree, nil) + + // Test that we get back the original value + value, exists := roTree.Get("test") + if !exists { + t.Fatal("Key not found") + } + + // Verify it's the exact same slice (not a copy) + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference") + } + + // Test through iteration as well + roTree.Iterate("", "", func(key string, value interface{}) bool { + retrievedSlice := value.([]int) + if &retrievedSlice[0] != &originalValue[0] { + t.Error("Expected to get back the original slice reference in iteration") + } + return false + }) +} diff --git a/examples/gno.land/p/demo/avl/tree.gno b/examples/gno.land/p/demo/avl/tree.gno index e7aa55eb7e4..3cc785b7caf 100644 --- a/examples/gno.land/p/demo/avl/tree.gno +++ b/examples/gno.land/p/demo/avl/tree.gno @@ -1,5 +1,23 @@ package avl +type TreeInterface interface { + // read operations + + Size() int + Has(key string) bool + Get(key string) (value interface{}, exists bool) + GetByIndex(index int) (key string, value interface{}) + Iterate(start, end string, cb IterCbFn) bool + ReverseIterate(start, end string, cb IterCbFn) bool + IterateByOffset(offset int, count int, cb IterCbFn) bool + ReverseIterateByOffset(offset int, count int, cb IterCbFn) bool + + // write operations + + Set(key string, value interface{}) (updated bool) + Remove(key string) (value interface{}, removed bool) +} + type IterCbFn func(key string, value interface{}) bool //---------------------------------------- @@ -101,3 +119,6 @@ func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) boo }, ) } + +// Verify that Tree implements TreeInterface +var _ TreeInterface = (*Tree)(nil) From ac1b7448ac6173bcfd491846909af50b0eb79f23 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:45:38 +0100 Subject: [PATCH 2/5] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/rotree/gno.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/avl/rotree/gno.mod b/examples/gno.land/p/demo/avl/rotree/gno.mod index 9110d8537d6..d2cb439b2eb 100644 --- a/examples/gno.land/p/demo/avl/rotree/gno.mod +++ b/examples/gno.land/p/demo/avl/rotree/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/avl/rotree \ No newline at end of file +module gno.land/p/demo/avl/rotree From 79a03b2357efbff9a9e57d4049409640be70aa87 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 10 Dec 2024 19:20:31 +0100 Subject: [PATCH 3/5] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/pager/pager.gno | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index cccdc0df645..91009c8e36c 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -11,7 +11,7 @@ import ( // Pager is a struct that holds the AVL tree and pagination parameters. type Pager struct { - Tree *avl.Tree + Tree avl.TreeInterface PageQueryParam string SizeQueryParam string DefaultPageSize int @@ -37,7 +37,7 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager { +func NewPager(tree avl.TreeInterface, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page", From 8e6ce0b1dfa0ed02f31bfc26ac3c647928533174 Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:10:31 +0100 Subject: [PATCH 4/5] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/pager/pager.gno | 2 +- examples/gno.land/p/demo/avl/rotree/rotree.gno | 4 ++-- examples/gno.land/p/demo/avl/tree.gno | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index 91009c8e36c..d229ab17d5e 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -11,7 +11,7 @@ import ( // Pager is a struct that holds the AVL tree and pagination parameters. type Pager struct { - Tree avl.TreeInterface + Tree avl.ITree PageQueryParam string SizeQueryParam string DefaultPageSize int diff --git a/examples/gno.land/p/demo/avl/rotree/rotree.gno b/examples/gno.land/p/demo/avl/rotree/rotree.gno index 5d4818d112a..3e093c4d0e0 100644 --- a/examples/gno.land/p/demo/avl/rotree/rotree.gno +++ b/examples/gno.land/p/demo/avl/rotree/rotree.gno @@ -82,8 +82,8 @@ type ReadOnlyTree struct { makeEntrySafeFn func(interface{}) interface{} } -// Verify that ReadOnlyTree implements TreeInterface -var _ avl.TreeInterface = (*ReadOnlyTree)(nil) +// Verify that ReadOnlyTree implements ITree +var _ avl.ITree = (*ReadOnlyTree)(nil) // getSafeValue applies the makeEntrySafeFn if it exists, otherwise returns the original value func (roTree *ReadOnlyTree) getSafeValue(value interface{}) interface{} { diff --git a/examples/gno.land/p/demo/avl/tree.gno b/examples/gno.land/p/demo/avl/tree.gno index 3cc785b7caf..3834246d2cd 100644 --- a/examples/gno.land/p/demo/avl/tree.gno +++ b/examples/gno.land/p/demo/avl/tree.gno @@ -1,6 +1,6 @@ package avl -type TreeInterface interface { +type ITree interface { // read operations Size() int @@ -121,4 +121,4 @@ func (tree *Tree) ReverseIterateByOffset(offset int, count int, cb IterCbFn) boo } // Verify that Tree implements TreeInterface -var _ TreeInterface = (*Tree)(nil) +var _ ITree = (*Tree)(nil) From cfec6946865a5f1f620894e12f61ca85c2c9530f Mon Sep 17 00:00:00 2001 From: moul <94029+moul@users.noreply.github.com> Date: Wed, 11 Dec 2024 06:14:19 +0100 Subject: [PATCH 5/5] chore: fixup Signed-off-by: moul <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/avl/pager/pager.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/avl/pager/pager.gno b/examples/gno.land/p/demo/avl/pager/pager.gno index d229ab17d5e..ca3eadde032 100644 --- a/examples/gno.land/p/demo/avl/pager/pager.gno +++ b/examples/gno.land/p/demo/avl/pager/pager.gno @@ -37,7 +37,7 @@ type Item struct { } // NewPager creates a new Pager with default values. -func NewPager(tree avl.TreeInterface, defaultPageSize int, reversed bool) *Pager { +func NewPager(tree avl.ITree, defaultPageSize int, reversed bool) *Pager { return &Pager{ Tree: tree, PageQueryParam: "page",