diff --git a/Sources/LvDBWrapper/LvDB.h b/Sources/LvDBWrapper/LvDB.h index 7e182bb..5848f88 100644 --- a/Sources/LvDBWrapper/LvDB.h +++ b/Sources/LvDBWrapper/LvDB.h @@ -2,20 +2,14 @@ #define LvDB_h #import +@class LvDBIterator; @interface LvDB : NSObject - (id)initWithDBPath:(NSString *)path; - (void)close; -- (void)seekToFirst; -- (void)seekToLast; -- (void)seek:(NSData *)key; -- (void)next; -- (void)prev; -- (BOOL)valid; -- (NSData *)key; -- (NSData *)value; +- (LvDBIterator *)makeIterator; - (NSArray *)iterate:(NSData *)start :(NSData *)end; - (NSData *)get:(NSData *)key; diff --git a/Sources/LvDBWrapper/LvDB.mm b/Sources/LvDBWrapper/LvDB.mm index 996cd92..bab2947 100644 --- a/Sources/LvDBWrapper/LvDB.mm +++ b/Sources/LvDBWrapper/LvDB.mm @@ -1,4 +1,5 @@ #import "LvDB.h" +#import "LvDBIterator.h" #import #import @@ -13,7 +14,6 @@ @implementation LvDB std::unique_ptr db; -std::unique_ptr iter; leveldb::Options options; leveldb::ReadOptions readOptions; leveldb::WriteOptions writeOptions; @@ -27,7 +27,7 @@ - (id)initWithDBPath:(NSString *)path { options.compressors[0] = new leveldb::ZlibCompressorRaw(-1); //use the new raw-zip compressor to write (and read) options.compressors[1] = new leveldb::ZlibCompressor(); //also setup the old, slower compressor for backwards compatibility. This will only be used to read old compressed blocks. // options.block_size = 163840; - + readOptions.decompress_allocator = new leveldb::DecompressAllocator(); // writeOptions = leveldb::WriteOptions(); @@ -39,8 +39,7 @@ - (id)initWithDBPath:(NSString *)path { return nil; } db.reset(tmp); - iter.reset(db->NewIterator(readOptions)); - std::cout << "[LvDBWrapper] LevelDB connected: " << dbPath << std::endl; + std::cout << "[LvDBWrapper] LevelDB opened: " << dbPath << std::endl; } return self; } @@ -50,51 +49,20 @@ - (void)dealloc { } - (void)close { - iter.reset(); db.reset(); delete options.filter_policy; delete options.block_cache; delete options.compressors[0]; delete options.compressors[1]; delete readOptions.decompress_allocator; - std::cout << "[LvDBWrapper] LevelDB disconnected." << std::endl; + std::cout << "[LvDBWrapper] LevelDB closed." << std::endl; } /* ---------- ---------- ---------- ---------- ---------- ---------- */ -- (void)seekToFirst { - iter->SeekToFirst(); -} - -- (void)seekToLast { - iter->SeekToLast(); -} - -- (void)seek:(NSData *)key { - leveldb::Slice dbKey = leveldb::Slice((const char *)[key bytes], [key length]); - iter->Seek(dbKey); -} - -- (void)next { - iter->Next(); -} - -- (void)prev { - iter->Prev(); -} - -- (BOOL)valid { - return iter->Valid(); -} - -- (NSData *)key { - leveldb::Slice key = iter->key(); - return [[NSData alloc] initWithBytes:key.data() length:key.size()]; -} - -- (NSData *)value { - leveldb::Slice value = iter->value(); - return [[NSData alloc] initWithBytes:value.data() length:value.size()]; +- (LvDBIterator *)makeIterator { + auto dbIterator = db->NewIterator(readOptions); + return [[LvDBIterator alloc] initFromIterator:dbIterator]; } /// Iterate through all keys and data that exist between the given keys. diff --git a/Sources/LvDBWrapper/LvDBIterator.h b/Sources/LvDBWrapper/LvDBIterator.h new file mode 100644 index 0000000..414a316 --- /dev/null +++ b/Sources/LvDBWrapper/LvDBIterator.h @@ -0,0 +1,20 @@ +#ifndef Header_h +#define Header_h + +@interface LvDBIterator : NSObject + +- (id)initFromIterator:(void *)dbIterator; +- (void)destroy; + +- (void)seekToFirst; +- (void)seekToLast; +- (void)seek:(NSData *)key; +- (void)next; +- (void)prev; +- (BOOL)valid; +- (NSData *)key; +- (NSData *)value; + +@end + +#endif /* Header_h */ diff --git a/Sources/LvDBWrapper/LvDBIterator.mm b/Sources/LvDBWrapper/LvDBIterator.mm new file mode 100644 index 0000000..b893aa4 --- /dev/null +++ b/Sources/LvDBWrapper/LvDBIterator.mm @@ -0,0 +1,65 @@ +#import "LvDB.h" +#import "LvDBIterator.h" + +#import +#import +#import "leveldb/db.h" + +@implementation LvDBIterator + +std::unique_ptr iterator; + +- (id)initFromIterator:(void *)dbIterator { + if (self = [super init]) { + std::cout << "[LvDBWrapper] Iterator generated." << std::endl; + leveldb::Iterator* it = static_cast(dbIterator); + iterator.reset(it); + } + return self; +} + +- (void)dealloc { + std::cout << "[LvDBWrapper] Iterator deallocated." << std::endl; +} + +- (void)destroy { + std::cout << "[LvDBWrapper] Iterator destroyed." << std::endl; + iterator.reset(); +} + +- (void)seekToFirst { + iterator->SeekToFirst(); +} + +- (void)seekToLast { + iterator->SeekToLast(); +} + +- (void)seek:(NSData *)key { + leveldb::Slice dbKey = leveldb::Slice((const char *)[key bytes], [key length]); + iterator->Seek(dbKey); +} + +- (void)next { + iterator->Next(); +} + +- (void)prev { + iterator->Prev(); +} + +- (BOOL)valid { + return iterator->Valid(); +} + +- (NSData *)key { + leveldb::Slice key = iterator->key(); + return [[NSData alloc] initWithBytes:key.data() length:key.size()]; +} + +- (NSData *)value { + leveldb::Slice value = iterator->value(); + return [[NSData alloc] initWithBytes:value.data() length:value.size()]; +} + +@end diff --git a/Sources/LvDBWrapper/include/ExposedHeader.h b/Sources/LvDBWrapper/include/ExposedHeader.h index a006efe..fa89edd 100644 --- a/Sources/LvDBWrapper/include/ExposedHeader.h +++ b/Sources/LvDBWrapper/include/ExposedHeader.h @@ -2,5 +2,6 @@ #define ExposedHeader_h #include "../LvDB.h" +#include "../LvDBIterator.h" #endif /* ExposedHeader_h */ diff --git a/Tests/LvDBWrapperTests/LvDBWrapperTests.swift b/Tests/LvDBWrapperTests/LvDBWrapperTests.swift index 112293c..9343035 100644 --- a/Tests/LvDBWrapperTests/LvDBWrapperTests.swift +++ b/Tests/LvDBWrapperTests/LvDBWrapperTests.swift @@ -3,78 +3,116 @@ import XCTest final class LvDBWrapperTests: XCTestCase { private let dbPath = Bundle.module.path(forResource: "world/db", ofType: nil)! + private func prepareTemporaryDB() -> String { + let originalURL: URL + let tempURL: URL + if #available(iOS 16.0, macOS 13.0, *) { + originalURL = URL(filePath: dbPath, directoryHint: .isDirectory) + tempURL = FileManager.default.temporaryDirectory.appending(component: "db", directoryHint: .isDirectory) + } else { + originalURL = URL(fileURLWithPath: dbPath, isDirectory: true) + tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("db", isDirectory: true) + } + + do { + try? FileManager.default.removeItem(at: tempURL) + try FileManager.default.copyItem(at: originalURL, to: tempURL) + print("Created test db at \(tempURL)") + } catch { + XCTFail("Failed to copy the database from bundle.") + } + + if #available(iOS 16.0, macOS 13.0, *) { + return tempURL.path() + } else { + return tempURL.path + } + } func testOpenDB() { + let dbPath = prepareTemporaryDB() defer { print("\n") } print("\n") - guard let a = LvDB(dbPath: dbPath) else { + guard let db01 = LvDB(dbPath: dbPath), + let iterator01 = db01.makeIterator() + else { XCTFail() return } - a.seek("mobevents".data(using: .utf8)!) - if a.valid(), let v = a.value() { + iterator01.seek("mobevents".data(using: .utf8)!) + if iterator01.valid(), let v = iterator01.value() { XCTAssertEqual(126, v.count) } else { XCTFail() } - a.close() + iterator01.destroy() + db01.close() print("") - guard let b = LvDB(dbPath: dbPath) else { + guard let db02 = LvDB(dbPath: dbPath), + let iterator02 = db02.makeIterator() + else { XCTFail() return } - b.seek("~local_player".data(using: .utf8)!) - if b.valid(), let v = b.value() { + iterator02.seek("~local_player".data(using: .utf8)!) + if iterator02.valid(), let v = iterator02.value() { XCTAssertEqual(7323, v.count) } else { XCTFail() } - b.close() + iterator02.destroy() + db02.close() print("") - guard let c = LvDB(dbPath: dbPath) else { + guard let db03 = LvDB(dbPath: dbPath), + let iterator03 = db03.makeIterator() + else { XCTFail() return } - c.seek("BiomeData".data(using: .utf8)!) - if c.valid(), let v = c.value() { + iterator03.seek("BiomeData".data(using: .utf8)!) + if iterator03.valid(), let v = iterator03.value() { XCTAssertEqual(436, v.count) } else { XCTFail() } - c.close() + iterator03.destroy() + db03.close() } func testSeek() { - guard let db = LvDB(dbPath: dbPath) else { + let dbPath = prepareTemporaryDB() + guard let db = LvDB(dbPath: dbPath), + let iterator = db.makeIterator() + else { XCTFail() return } - + var firstToLastCount = 0 - db.seekToFirst() - while db.valid() { + iterator.seekToFirst() + while iterator.valid() { firstToLastCount += 1 - db.next() + iterator.next() } var lastToFirstCount = 0 - db.seekToLast() - while db.valid() { + iterator.seekToLast() + while iterator.valid() { lastToFirstCount += 1 - db.prev() + iterator.prev() } XCTAssertEqual(firstToLastCount, lastToFirstCount) - + let wellKnownKey = "~local_player".data(using: .utf8)! - db.seek(wellKnownKey) - - guard let key = db.key(), let value = db.value() else { + iterator.seek(wellKnownKey) + + guard let key = iterator.key(), let value = iterator.value() else { XCTFail() return } @@ -82,4 +120,51 @@ final class LvDBWrapperTests: XCTestCase { XCTAssertEqual("~local_player", keyStr ?? "") XCTAssertEqual(7323, value.count) } + + func testDeleteKeysAndCountAgain() { + let dbPath = prepareTemporaryDB() + guard let db = LvDB(dbPath: dbPath), + let iterator01 = db.makeIterator() + else { + XCTFail() + return + } + + var count01 = 0 + iterator01.seekToFirst() + while iterator01.valid() { + count01 += 1 + iterator01.next() + } + + db.remove("mobevents".data(using: .utf8)!) + db.remove("~local_player".data(using: .utf8)!) + db.remove("BiomeData".data(using: .utf8)!) + + var count02 = 0 + iterator01.seekToFirst() + while iterator01.valid() { + count02 += 1 + iterator01.next() + } + + XCTAssertEqual(count01, count02) + + guard let iterator02 = db.makeIterator() else { + XCTFail() + return + } + var count03 = 0 + iterator02.seekToFirst() + while iterator02.valid() { + count03 += 1 + iterator02.next() + } + + XCTAssertEqual(count03, count01 - 3) + + iterator01.destroy() + iterator02.destroy() + db.close() + } }