From 80548818c016440bb71db450b7a1924b9737280c Mon Sep 17 00:00:00 2001 From: D4ryl00 Date: Thu, 9 Oct 2025 17:38:36 +0200 Subject: [PATCH] fix: DB implementation Signed-off-by: D4ryl00 --- .../land/gno/gnonative/NativeDBManager.kt | 5 +- expo/ios/NativeDBManager.swift | 115 +++++++--- framework/service/db.go | 196 ++++++++++++++---- 3 files changed, 247 insertions(+), 69 deletions(-) diff --git a/expo/android/src/main/java/land/gno/gnonative/NativeDBManager.kt b/expo/android/src/main/java/land/gno/gnonative/NativeDBManager.kt index f1ded00d..29711df7 100644 --- a/expo/android/src/main/java/land/gno/gnonative/NativeDBManager.kt +++ b/expo/android/src/main/java/land/gno/gnonative/NativeDBManager.kt @@ -65,7 +65,7 @@ class NativeDBManager( val b64 = synchronized(lock) { prefs.getString("$entryPrefix$hex", null) } ?: return ByteArray(0) // gomobile generated non-null return -> use empty on miss val blob = Base64.decode(b64, Base64.NO_WRAP) - return decrypt(blob) ?: ByteArray(0) + return decrypt(blob) ?: throw Exception("Failed to decrypt value for key: $hex") } override fun has(p0: ByteArray?): Boolean { @@ -112,7 +112,8 @@ class NativeDBManager( val pairs = ArrayList>(page.size) for (h in page) { val b64 = prefs.getString("$entryPrefix$h", null) ?: continue - val v = decrypt(Base64.decode(b64, Base64.NO_WRAP)) ?: continue + val blob = Base64.decode(b64, Base64.NO_WRAP) + val v = decrypt(blob) ?: throw Exception("Failed to decrypt value for key: $h") pairs += (unhex(h) to v) } diff --git a/expo/ios/NativeDBManager.swift b/expo/ios/NativeDBManager.swift index d79fd116..3d92d247 100644 --- a/expo/ios/NativeDBManager.swift +++ b/expo/ios/NativeDBManager.swift @@ -24,9 +24,10 @@ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol { // MARK: - Public Interface Implementation - public func get(_ key: Data?) -> Data? { - guard let key = key else { return nil } - + public func get(_ key: Data?) throws -> Data { + guard let key = key, !key.isEmpty else { + throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.") + } let account = keyToAccount(key) var query: [String: Any] = [ @@ -44,21 +45,25 @@ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol { var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) - guard status == errSecSuccess else { - return nil + switch status { + case errSecSuccess: + return (result as? Data) ?? Data() + case errSecItemNotFound: + // This is not an error; it's the expected result for a missing key. + return Data() + default: + throw NativeDBError.keychainError(status: status, message: "Failed to get item.") } - - return result as? Data } - public func delete(_ key: Data?) { - DispatchQueue.global(qos: .utility).async { - self.deleteSync(key) - } + public func delete(_ key: Data?) throws { + try self.deleteSync(key) } - public func deleteSync(_ key: Data?) { - guard let key = key else { return } + public func deleteSync(_ key: Data?) throws { + guard let key = key, !key.isEmpty else { + throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.") + } let account = keyToAccount(key) @@ -72,38 +77,63 @@ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol { query[kSecAttrAccessGroup as String] = accessGroup } - SecItemDelete(query as CFDictionary) + let status = SecItemDelete(query as CFDictionary) + + // Deleting a non-existent item is not considered an error. + if status != errSecSuccess && status != errSecItemNotFound { + throw NativeDBError.keychainError(status: status, message: "Failed to delete item.") + } } - public func has(_ key: Data?) -> Bool { - guard let key = key else { return false } + public func has(_ key: Data?, ret0_ returnPointer: UnsafeMutablePointer?) throws { + // 1. Ensure the return pointer provided by the caller is valid. + guard let returnPointer = returnPointer else { + throw NativeDBError.invalidArgument(description: "Return pointer must not be nil.") + } + + // 2. Validate the input key. + guard let key = key, !key.isEmpty else { + throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.") + } let account = keyToAccount(key) var query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, - kSecAttrAccount as String: account, - kSecReturnData as String: false, - kSecMatchLimit as String: kSecMatchLimitOne + kSecAttrAccount as String: account ] - if let accessGroup = accessGroup { query[kSecAttrAccessGroup as String] = accessGroup } let status = SecItemCopyMatching(query as CFDictionary, nil) - return status == errSecSuccess + + // 3. Handle the result from the Keychain. + switch status { + case errSecSuccess: + // Key exists. Write `true` to the pointer's memory location. + returnPointer.pointee = ObjCBool(true) + case errSecItemNotFound: + // Key does not exist. Write `false` to the pointer's memory location. + returnPointer.pointee = ObjCBool(false) + default: + // A real keychain error occurred. Throw an error. + throw NativeDBError.keychainError(status: status, message: "Failed to check for item existence.") + } } - public func set(_ key: Data?, p1 value: Data?) { - DispatchQueue.global(qos: .utility).async { - self.setSync(key, p1: value) - } + public func set(_ key: Data?, value: Data?) throws { + try self.setSync(key, value: value) } - public func setSync(_ key: Data?, p1 value: Data?) { - guard let key = key, let value = value else { return } + public func setSync(_ key: Data?, value: Data?) throws { + guard let key = key, !key.isEmpty else { + throw NativeDBError.invalidArgument(description: "Key must not be nil or empty.") + } + guard let value = value else { + throw NativeDBError.invalidArgument(description: "Value must not be nil.") + } let account = keyToAccount(key) @@ -124,13 +154,23 @@ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol { let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary) - if updateStatus == errSecItemNotFound { - // Item doesn't exist, create new one + switch updateStatus { + case errSecSuccess: + // Update was successful. + return + case errSecItemNotFound: + // Item doesn't exist, so add it. var newItem = query newItem[kSecValueData as String] = value newItem[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly - SecItemAdd(newItem as CFDictionary, nil) + let addStatus = SecItemAdd(newItem as CFDictionary, nil) + if addStatus != errSecSuccess { + throw NativeDBError.keychainError(status: addStatus, message: "Failed to add new item.") + } + default: + // Another error occurred during update. + throw NativeDBError.keychainError(status: updateStatus, message: "Failed to update item.") } } @@ -260,3 +300,18 @@ public class NativeDBManager: NSObject, GnoGnonativeNativeDBProtocol { return true } } + +public enum NativeDBError: Error, LocalizedError { + case invalidArgument(description: String) + case keychainError(status: OSStatus, message: String) + + public var errorDescription: String? { + switch self { + case .invalidArgument(let description): + return "Invalid Argument: \(description)" + case .keychainError(let status, let message): + let statusMessage = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error" + return "Keychain Error: \(message) (status: \(status), reason: \(statusMessage))" + } + } +} diff --git a/framework/service/db.go b/framework/service/db.go index d2126189..c08ac243 100644 --- a/framework/service/db.go +++ b/framework/service/db.go @@ -2,6 +2,7 @@ package gnonative import ( "encoding/binary" + "errors" "fmt" "sync" "sync/atomic" @@ -11,14 +12,21 @@ import ( var errShort = fmt.Errorf("chunk blob: short buffer") +// Static assertions to ensure interface compliance. +var ( + _ mdb.DB = (*db)(nil) + _ mdb.Batch = (*batch)(nil) + _ mdb.Iterator = (*iterator)(nil) +) + // NativeDB is implemented in the native (Kotlin/Swift) layer. type NativeDB interface { - Get([]byte) []byte - Has(key []byte) bool - Set([]byte, []byte) - SetSync([]byte, []byte) - Delete([]byte) - DeleteSync([]byte) + Get(key []byte) ([]byte, error) + Has(key []byte) (bool, error) + Set(key, value []byte) error + SetSync(key, value []byte) error + Delete(key []byte) error + DeleteSync(key []byte) error ScanChunk(start, end, seekKey []byte, limit int, reverse bool) ([]byte, error) } @@ -31,17 +39,23 @@ type db struct { func (d *db) Close() error { d.closed.Store(true) + return nil } -func (d *db) ensureOpen() { +func (d *db) ensureOpen() error { if d.closed.Load() { - panic("db: use after Close") + return errors.New("db is closed") } + + return nil } -func (db *db) Iterator(start, end []byte) mdb.Iterator { - db.ensureOpen() +func (db *db) Iterator(start, end []byte) (mdb.Iterator, error) { + if err := db.ensureOpen(); err != nil { + return nil, err + } + it := &iterator{ db: db, start: append([]byte(nil), start...), @@ -49,12 +63,20 @@ func (db *db) Iterator(start, end []byte) mdb.Iterator { reverse: false, chunkLimit: 256, } + it.fill() - return it + if it.err != nil { + return nil, it.err + } + + return it, nil } -func (db *db) ReverseIterator(start, end []byte) mdb.Iterator { - db.ensureOpen() +func (db *db) ReverseIterator(start, end []byte) (mdb.Iterator, error) { + if err := db.ensureOpen(); err != nil { + return nil, err + } + it := &iterator{ db: db, start: append([]byte(nil), start...), @@ -62,37 +84,54 @@ func (db *db) ReverseIterator(start, end []byte) mdb.Iterator { reverse: true, chunkLimit: 256, } + it.fill() - return it + if it.err != nil { + return nil, it.err + } + + return it, nil } -func (d *db) Print() { +func (d *db) Print() error { d.mu.RLock() defer d.mu.RUnlock() - // With only point ops exposed, we can't enumerate keys here. - // Keep as a stub or log something useful for debugging: - fmt.Println("db.Print(): NativeDB has no range API; nothing to print") + fmt.Println("db.closed: ", d.closed.Load()) + + return nil } func (d *db) Stats() map[string]string { d.mu.RLock() defer d.mu.RUnlock() - // Return whatever you track on the Go side. Native side has no stats here. + return map[string]string{ "closed": fmt.Sprintf("%v", d.closed.Load()), } } func (d *db) NewBatch() mdb.Batch { - d.ensureOpen() + if err := d.ensureOpen(); err != nil { + return &batch{err: err} + } + return &batch{db: d} } +func (d *db) NewBatchWithSize(size int) mdb.Batch { + if err := d.ensureOpen(); err != nil { + return &batch{err: err} + } + + return &batch{db: d, ops: make([]op, 0, size)} +} + // --- Batch implementation (pure Go) --- type batch struct { db *db ops []op + err error } type op struct { @@ -101,51 +140,112 @@ type op struct { value []byte // nil for delete } -func (b *batch) Set(key, value []byte) { - b.db.ensureOpen() +func (b *batch) Set(key, value []byte) error { + if err := b.db.ensureOpen(); err != nil { + return err + } + + if b.err != nil { + return b.err + } + // Defensive copies: gomobile & callers must not mutate later. k := append([]byte(nil), key...) v := append([]byte(nil), value...) b.ops = append(b.ops, op{del: false, key: k, value: v}) + + return nil } -func (b *batch) Delete(key []byte) { - b.db.ensureOpen() +func (b *batch) Delete(key []byte) error { + if err := b.db.ensureOpen(); err != nil { + return err + } + + if b.err != nil { + return b.err + } + k := append([]byte(nil), key...) b.ops = append(b.ops, op{del: true, key: k}) + + return nil } // Write applies ops using async variants. -func (b *batch) Write() { - b.db.ensureOpen() +func (b *batch) Write() error { + if err := b.db.ensureOpen(); err != nil { + return err + } + + if b.err != nil { + return b.err + } + + var errs []error for _, o := range b.ops { if o.del { - b.db.Delete(o.key) + if err := b.db.Delete(o.key); err != nil { + errs = append(errs, err) + } } else { - b.db.Set(o.key, o.value) + if err := b.db.Set(o.key, o.value); err != nil { + errs = append(errs, err) + } } } // Clear buffer to allow reuse if desired. b.ops = b.ops[:0] + + err := errors.Join(errs...) + return err } // WriteSync applies ops using sync variants. -func (b *batch) WriteSync() { - b.db.ensureOpen() +func (b *batch) WriteSync() error { + if err := b.db.ensureOpen(); err != nil { + return err + } + + if b.err != nil { + return b.err + } + + var errs []error for _, o := range b.ops { if o.del { - b.db.DeleteSync(o.key) + if err := b.db.DeleteSync(o.key); err != nil { + errs = append(errs, err) + } } else { - b.db.SetSync(o.key, o.value) + if err := b.db.SetSync(o.key, o.value); err != nil { + errs = append(errs, err) + } } } b.ops = b.ops[:0] + + err := errors.Join(errs...) + return err +} + +func (b *batch) GetByteSize() (int, error) { + size := 0 + for _, op := range b.ops { + size += len(op.key) + if !op.del { + size += len(op.value) + } + } + return size, nil } -func (b *batch) Close() { +func (b *batch) Close() error { // Drop buffered ops; allow GC. b.ops = nil b.db = nil + + return nil } type kv struct { @@ -164,17 +264,22 @@ type iterator struct { hasMore bool closed bool chunkLimit int + err error } -func (it *iterator) Domain() (start, end []byte) { return it.start, it.end } +func (it *iterator) Domain() (start, end []byte) { + return it.start, it.end +} func (it *iterator) Valid() bool { if it.closed { return false } + for it.i >= len(it.chunk) && it.hasMore { it.fill() } + return it.i < len(it.chunk) } @@ -182,6 +287,7 @@ func (it *iterator) Next() { if !it.Valid() { return } + cur := it.chunk[it.i] it.i++ // keep seekKey strictly at the last returned key @@ -192,6 +298,7 @@ func (it *iterator) Key() []byte { if !it.Valid() { return nil } + return it.chunk[it.i].k } @@ -199,27 +306,42 @@ func (it *iterator) Value() []byte { if !it.Valid() { return nil } + return it.chunk[it.i].v } -func (it *iterator) Close() { it.closed = true; it.chunk = nil } + +func (it *iterator) Error() error { + return it.err +} + +func (it *iterator) Close() error { + it.closed = true + it.chunk = nil + + return nil +} func (it *iterator) fill() { if it.closed { return } + blob, err := it.db.ScanChunk(it.start, it.end, it.seekKey, it.chunkLimit, it.reverse) if err != nil { - it.chunk, it.i, it.hasMore = nil, 0, false + it.chunk, it.i, it.hasMore, it.err = nil, 0, false, err return } + pairs, nextSeek, hasMore, err := decodeChunkBlob(blob) if err != nil { - it.chunk, it.i, it.hasMore = nil, 0, false + it.chunk, it.i, it.hasMore, it.err = nil, 0, false, err return } + it.chunk = pairs it.i = 0 it.hasMore = hasMore + if len(nextSeek) > 0 { it.seekKey = append(it.seekKey[:0], nextSeek...) }