/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { KeyValueService } = ChromeUtils.importESModule( "resource://gre/modules/kvstore.sys.mjs" ); function run_test() { do_get_profile(); run_next_test(); } async function makeDatabaseDir(name, { mockCorrupted = false } = {}) { const databaseDir = PathUtils.join(PathUtils.profileDir, name); await IOUtils.makeDirectory(databaseDir); if (mockCorrupted) { // Mock a corrupted db. await IOUtils.write( PathUtils.join(databaseDir, "data.safe.bin"), new Uint8Array([0x00, 0x00, 0x00, 0x00]) ); } return databaseDir; } const gKeyValueService = Cc["@mozilla.org/key-value-service;1"].getService( Ci.nsIKeyValueService ); add_task(async function getService() { Assert.ok(gKeyValueService); }); add_task(async function getOrCreate_defaultRecoveryStrategyError() { const databaseDir = await makeDatabaseDir("getOrCreate_Error", { mockCorrupted: true, }); await Assert.rejects( KeyValueService.getOrCreate(databaseDir, "db"), /FileInvalid/ ); }); add_task(async function getOrCreateWithOptions_RecoveryStrategyError() { const databaseDir = await makeDatabaseDir("getOrCreateWithOptions_Error", { mockCorrupted: true, }); await Assert.rejects( KeyValueService.getOrCreateWithOptions(databaseDir, "db", { strategy: KeyValueService.RecoveryStrategy.ERROR, }), /FileInvalid/ ); }); add_task(async function getOrCreateWithOptions_RecoveryStrategyRename() { const databaseDir = await makeDatabaseDir("getOrCreateWithOptions_Rename", { mockCorrupted: true, }); const database = await KeyValueService.getOrCreateWithOptions( databaseDir, "db", { strategy: KeyValueService.RecoveryStrategy.RENAME, } ); Assert.ok(database); Assert.ok( await IOUtils.exists(PathUtils.join(databaseDir, "data.safe.bin.corrupt")), "Expect corrupt file to be found" ); }); add_task(async function getOrCreateWithOptions_RecoveryStrategyDiscard() { const databaseDir = await makeDatabaseDir("getOrCreateWithOptions_Discard", { mockCorrupted: true, }); const database = await KeyValueService.getOrCreateWithOptions( databaseDir, "db", { strategy: KeyValueService.RecoveryStrategy.DISCARD, } ); Assert.ok(database); Assert.equal( await IOUtils.exists(PathUtils.join(databaseDir, "data.safe.bin.corrupt")), false, "Expect corrupt file to not exist" ); }); add_task(async function getOrCreate() { const databaseDir = await makeDatabaseDir("getOrCreate"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); Assert.ok(database); // Test creating a database with a nonexistent path. const nonexistentDir = PathUtils.join(PathUtils.profileDir, "nonexistent"); await Assert.rejects( KeyValueService.getOrCreate(nonexistentDir, "db"), /UnsuitableEnvironmentPath/ ); // Test creating a database with a non-normalized but fully-qualified path. let nonNormalizedDir = await makeDatabaseDir("non-normalized"); nonNormalizedDir = [nonNormalizedDir, "..", ".", "non-normalized"].join( Services.appinfo.OS === "WINNT" ? "\\" : "/" ); Assert.ok(await KeyValueService.getOrCreate(nonNormalizedDir, "db")); }); add_task(async function putGetHasDelete() { const databaseDir = await makeDatabaseDir("putGetHasDelete"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // Getting key/value pairs that don't exist (yet) returns default values // or null, depending on whether you specify a default value. Assert.strictEqual(await database.get("int-key", 1), 1); Assert.strictEqual(await database.get("double-key", 1.1), 1.1); Assert.strictEqual(await database.get("string-key", ""), ""); Assert.strictEqual(await database.get("bool-key", false), false); Assert.strictEqual(await database.get("int-key"), null); Assert.strictEqual(await database.get("double-key"), null); Assert.strictEqual(await database.get("string-key"), null); Assert.strictEqual(await database.get("bool-key"), null); // The put method succeeds without returning a value. Assert.strictEqual(await database.put("int-key", 1234), undefined); Assert.strictEqual(await database.put("double-key", 56.78), undefined); Assert.strictEqual( await database.put("string-key", "Héllo, wőrld!"), undefined ); Assert.strictEqual(await database.put("bool-key", true), undefined); // Getting key/value pairs that exist returns the expected values. Assert.strictEqual(await database.get("int-key", 1), 1234); Assert.strictEqual(await database.get("double-key", 1.1), 56.78); Assert.strictEqual(await database.get("string-key", ""), "Héllo, wőrld!"); Assert.strictEqual(await database.get("bool-key", false), true); Assert.strictEqual(await database.get("int-key"), 1234); Assert.strictEqual(await database.get("double-key"), 56.78); Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!"); Assert.strictEqual(await database.get("bool-key"), true); // The has() method works as expected for both existing and non-existent keys. Assert.strictEqual(await database.has("int-key"), true); Assert.strictEqual(await database.has("double-key"), true); Assert.strictEqual(await database.has("string-key"), true); Assert.strictEqual(await database.has("bool-key"), true); Assert.strictEqual(await database.has("nonexistent-key"), false); // The delete() method succeeds without returning a value. Assert.strictEqual(await database.delete("int-key"), undefined); Assert.strictEqual(await database.delete("double-key"), undefined); Assert.strictEqual(await database.delete("string-key"), undefined); Assert.strictEqual(await database.delete("bool-key"), undefined); // The has() method works as expected for a deleted key. Assert.strictEqual(await database.has("int-key"), false); Assert.strictEqual(await database.has("double-key"), false); Assert.strictEqual(await database.has("string-key"), false); Assert.strictEqual(await database.has("bool-key"), false); // Getting key/value pairs that were deleted returns default values. Assert.strictEqual(await database.get("int-key", 1), 1); Assert.strictEqual(await database.get("double-key", 1.1), 1.1); Assert.strictEqual(await database.get("string-key", ""), ""); Assert.strictEqual(await database.get("bool-key", false), false); Assert.strictEqual(await database.get("int-key"), null); Assert.strictEqual(await database.get("double-key"), null); Assert.strictEqual(await database.get("string-key"), null); Assert.strictEqual(await database.get("bool-key"), null); }); add_task(async function putWithResizing() { const databaseDir = await makeDatabaseDir("putWithResizing"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // The default store size is 1MB, putting key/value pairs bigger than that // would trigger auto resizing. const base = "A humongous string in 32 bytes!!"; const val1M = base.repeat(32768); const val2M = val1M.repeat(2); Assert.strictEqual(await database.put("A-1M-value", val1M), undefined); Assert.strictEqual(await database.put("A-2M-value", val2M), undefined); Assert.strictEqual(await database.put("A-32B-value", base), undefined); Assert.strictEqual(await database.get("A-1M-value"), val1M); Assert.strictEqual(await database.get("A-2M-value"), val2M); Assert.strictEqual(await database.get("A-32B-value"), base); }); add_task(async function largeNumbers() { const databaseDir = await makeDatabaseDir("largeNumbers"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); const MAX_INT_VARIANT = Math.pow(2, 31) - 1; const MIN_DOUBLE_VARIANT = Math.pow(2, 31); await database.put("max-int-variant", MAX_INT_VARIANT); await database.put("min-double-variant", MIN_DOUBLE_VARIANT); await database.put("max-safe-integer", Number.MAX_SAFE_INTEGER); await database.put("min-safe-integer", Number.MIN_SAFE_INTEGER); await database.put("max-value", Number.MAX_VALUE); await database.put("min-value", Number.MIN_VALUE); Assert.strictEqual(await database.get("max-int-variant"), MAX_INT_VARIANT); Assert.strictEqual( await database.get("min-double-variant"), MIN_DOUBLE_VARIANT ); Assert.strictEqual( await database.get("max-safe-integer"), Number.MAX_SAFE_INTEGER ); Assert.strictEqual( await database.get("min-safe-integer"), Number.MIN_SAFE_INTEGER ); Assert.strictEqual(await database.get("max-value"), Number.MAX_VALUE); Assert.strictEqual(await database.get("min-value"), Number.MIN_VALUE); }); add_task(async function extendedCharacterKey() { const databaseDir = await makeDatabaseDir("extendedCharacterKey"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // Ensure that we can use extended character (i.e. non-ASCII) strings as keys. await database.put("Héllo, wőrld!", 1); Assert.strictEqual(await database.has("Héllo, wőrld!"), true); Assert.strictEqual(await database.get("Héllo, wőrld!"), 1); const enumerator = await database.enumerate(); const { key } = enumerator.getNext(); Assert.strictEqual(key, "Héllo, wőrld!"); await database.delete("Héllo, wőrld!"); }); add_task(async function clear() { const databaseDir = await makeDatabaseDir("clear"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); await database.put("int-key", 1234); await database.put("double-key", 56.78); await database.put("string-key", "Héllo, wőrld!"); await database.put("bool-key", true); Assert.strictEqual(await database.clear(), undefined); Assert.strictEqual(await database.has("int-key"), false); Assert.strictEqual(await database.has("double-key"), false); Assert.strictEqual(await database.has("string-key"), false); Assert.strictEqual(await database.has("bool-key"), false); }); add_task(async function writeManyFailureCases() { const databaseDir = await makeDatabaseDir("writeManyFailureCases"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); Assert.throws(() => database.writeMany(), /unexpected argument/); Assert.throws(() => database.writeMany("foo"), /unexpected argument/); Assert.throws(() => database.writeMany(["foo"]), /unexpected argument/); }); add_task(async function writeManyPutOnly() { const databaseDir = await makeDatabaseDir("writeMany"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); async function test_helper(pairs) { Assert.strictEqual(await database.writeMany(pairs), undefined); Assert.strictEqual(await database.get("int-key"), 1234); Assert.strictEqual(await database.get("double-key"), 56.78); Assert.strictEqual(await database.get("string-key"), "Héllo, wőrld!"); Assert.strictEqual(await database.get("bool-key"), true); await database.clear(); } // writeMany with an empty object is OK Assert.strictEqual(await database.writeMany({}), undefined); // writeMany with an object const pairs = { "int-key": 1234, "double-key": 56.78, "string-key": "Héllo, wőrld!", "bool-key": true, }; await test_helper(pairs); // writeMany with an array of pairs const arrayPairs = [ ["int-key", 1234], ["double-key", 56.78], ["string-key", "Héllo, wőrld!"], ["bool-key", true], ]; await test_helper(arrayPairs); // writeMany with a key/value generator function* pairMaker() { yield ["int-key", 1234]; yield ["double-key", 56.78]; yield ["string-key", "Héllo, wőrld!"]; yield ["bool-key", true]; } await test_helper(pairMaker()); // writeMany with a map const mapPairs = new Map(arrayPairs); await test_helper(mapPairs); }); add_task(async function writeManyLargePairsWithResizing() { const databaseDir = await makeDatabaseDir("writeManyWithResizing"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // The default store size is 1MB, putting key/value pairs bigger than that // would trigger auto resizing. const base = "A humongous string in 32 bytes!!"; const val1M = base.repeat(32768); const val2M = val1M.repeat(2); // writeMany with an object const pairs = { "A-1M-value": val1M, "A-32B-value": base, "A-2M-value": val2M, }; Assert.strictEqual(await database.writeMany(pairs), undefined); Assert.strictEqual(await database.get("A-1M-value"), val1M); Assert.strictEqual(await database.get("A-2M-value"), val2M); Assert.strictEqual(await database.get("A-32B-value"), base); }); add_task(async function writeManySmallPairsWithResizing() { const databaseDir = await makeDatabaseDir("writeManyWithResizing"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // The default store size is 1MB, putting key/value pairs bigger than that // would trigger auto resizing. const base = "A humongous string in 32 bytes!!"; const val1K = base.repeat(32); // writeMany with a key/value generator function* pairMaker() { for (let i = 0; i < 1024; i++) { yield [`key-${i}`, val1K]; } } Assert.strictEqual(await database.writeMany(pairMaker()), undefined); for (let i = 0; i < 1024; i++) { Assert.ok(await database.has(`key-${i}`)); } }); add_task(async function writeManyDeleteOnly() { const databaseDir = await makeDatabaseDir("writeManyDeletesOnly"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); // writeMany with an object const pairs = { "int-key": 1234, "double-key": 56.78, "string-key": "Héllo, wőrld!", "bool-key": true, }; async function test_helper(deletes) { Assert.strictEqual(await database.writeMany(pairs), undefined); Assert.strictEqual(await database.writeMany(deletes), undefined); Assert.strictEqual(await database.get("int-key"), null); Assert.strictEqual(await database.get("double-key"), null); Assert.strictEqual(await database.get("string-key"), null); Assert.strictEqual(await database.get("bool-key"), null); } // writeMany with an empty object is OK Assert.strictEqual(await database.writeMany({}), undefined); // writeMany with an object await test_helper({ "int-key": null, "double-key": null, "string-key": null, "bool-key": null, }); // writeMany with an array of pairs const arrayPairs = [ ["int-key", null], ["double-key", null], ["string-key", null], ["bool-key", null], ]; await test_helper(arrayPairs); // writeMany with a key/value generator function* pairMaker() { yield ["int-key", null]; yield ["double-key", null]; yield ["string-key", null]; yield ["bool-key", null]; } await test_helper(pairMaker()); // writeMany with a map const mapPairs = new Map(arrayPairs); await test_helper(mapPairs); }); add_task(async function writeManyPutDelete() { const databaseDir = await makeDatabaseDir("writeManyPutDelete"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); await database.writeMany([ ["key1", "val1"], ["key3", "val3"], ["key4", "val4"], ["key5", "val5"], ]); await database.writeMany([ ["key2", "val2"], ["key4", null], ["key5", null], ]); Assert.strictEqual(await database.get("key1"), "val1"); Assert.strictEqual(await database.get("key2"), "val2"); Assert.strictEqual(await database.get("key3"), "val3"); Assert.strictEqual(await database.get("key4"), null); Assert.strictEqual(await database.get("key5"), null); await database.clear(); await database.writeMany([ ["key1", "val1"], ["key1", null], ["key1", "val11"], ["key1", null], ["key2", null], ["key2", "val2"], ]); Assert.strictEqual(await database.get("key1"), null); Assert.strictEqual(await database.get("key2"), "val2"); }); add_task(async function getOrCreateNamedDatabases() { const databaseDir = await makeDatabaseDir("getOrCreateNamedDatabases"); let fooDB = await KeyValueService.getOrCreate(databaseDir, "foo"); Assert.ok(fooDB, "retrieval of first named database works"); let barDB = await KeyValueService.getOrCreate(databaseDir, "bar"); Assert.ok(barDB, "retrieval of second named database works"); let bazDB = await KeyValueService.getOrCreate(databaseDir, "baz"); Assert.ok(bazDB, "retrieval of third named database works"); // Key/value pairs that are put into a database don't exist in others. await bazDB.put("key", 1); Assert.ok(!(await fooDB.has("key")), "the foo DB still doesn't have the key"); await fooDB.put("key", 2); Assert.ok(!(await barDB.has("key")), "the bar DB still doesn't have the key"); await barDB.put("key", 3); Assert.strictEqual( await bazDB.get("key", 0), 1, "the baz DB has its KV pair" ); Assert.strictEqual( await fooDB.get("key", 0), 2, "the foo DB has its KV pair" ); Assert.strictEqual( await barDB.get("key", 0), 3, "the bar DB has its KV pair" ); // Key/value pairs that are deleted from a database still exist in other DBs. await bazDB.delete("key"); Assert.strictEqual( await fooDB.get("key", 0), 2, "the foo DB still has its KV pair" ); await fooDB.delete("key"); Assert.strictEqual( await barDB.get("key", 0), 3, "the bar DB still has its KV pair" ); await barDB.delete("key"); }); add_task(async function enumeration() { const databaseDir = await makeDatabaseDir("enumeration"); const database = await KeyValueService.getOrCreate(databaseDir, "db"); await database.put("int-key", 1234); await database.put("double-key", 56.78); await database.put("string-key", "Héllo, wőrld!"); await database.put("bool-key", true); async function test(fromKey, toKey, pairs) { const enumerator = await database.enumerate(fromKey, toKey); for (const pair of pairs) { Assert.strictEqual(enumerator.hasMoreElements(), true); const element = enumerator.getNext(); Assert.ok(element); Assert.strictEqual(element.key, pair[0]); Assert.strictEqual(element.value, pair[1]); } Assert.strictEqual(enumerator.hasMoreElements(), false); Assert.throws(() => enumerator.getNext(), /NS_ERROR_FAILURE/); } // Test enumeration without specifying "from" and "to" keys, which should // enumerate all of the pairs in the database. This test does so explicitly // by passing "null", "undefined" or "" (empty string) arguments // for those parameters. The iterator test below also tests this implicitly // by not specifying arguments for those parameters. await test(null, null, [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); await test(undefined, undefined, [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // The implementation doesn't distinguish between a null/undefined value // and an empty string, so enumerating pairs from "" to "" has the same effect // as enumerating pairs without specifying from/to keys: it enumerates // all of the pairs in the database. await test("", "", [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // Test enumeration from a key that doesn't exist and is lexicographically // less than the least key in the database, which should enumerate // all of the pairs in the database. await test("aaaaa", null, [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // Test enumeration from a key that doesn't exist and is lexicographically // greater than the first key in the database, which should enumerate pairs // whose key is greater than or equal to the specified key. await test("ccccc", null, [ ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // Test enumeration from a key that does exist, which should enumerate pairs // whose key is greater than or equal to that key. await test("int-key", null, [ ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // Test enumeration from a key that doesn't exist and is lexicographically // greater than the greatest test key in the database, which should enumerate // none of the pairs in the database. await test("zzzzz", null, []); // Test enumeration to a key that doesn't exist and is lexicographically // greater than the greatest test key in the database, which should enumerate // all of the pairs in the database. await test(null, "zzzzz", [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ["string-key", "Héllo, wőrld!"], ]); // Test enumeration to a key that doesn't exist and is lexicographically // less than the greatest test key in the database, which should enumerate // pairs whose key is less than the specified key. await test(null, "ppppp", [ ["bool-key", true], ["double-key", 56.78], ["int-key", 1234], ]); // Test enumeration to a key that does exist, which should enumerate pairs // whose key is less than that key. await test(null, "int-key", [ ["bool-key", true], ["double-key", 56.78], ]); // Test enumeration to a key that doesn't exist and is lexicographically // less than the least key in the database, which should enumerate // none of the pairs in the database. await test(null, "aaaaa", []); // Test enumeration between intermediate keys that don't exist, which should // enumerate the pairs whose keys lie in between them. await test("ggggg", "ppppp", [["int-key", 1234]]); // Test enumeration from a key that exists to the same key, which shouldn't // enumerate any pairs, because the "to" key is exclusive. await test("int-key", "int-key", []); // Test enumeration from a greater key to a lesser one, which should // enumerate none of the pairs in the database, even if the reverse ordering // would enumerate some pairs. Consumers are responsible for ordering // the "from" and "to" keys such that "from" is less than or equal to "to". await test("ppppp", "ccccc", []); await test("int-key", "ccccc", []); await test("ppppp", "int-key", []); const actual = {}; for (const { key, value } of await database.enumerate()) { actual[key] = value; } Assert.deepEqual(actual, { "bool-key": true, "double-key": 56.78, "int-key": 1234, "string-key": "Héllo, wőrld!", }); await database.delete("int-key"); await database.delete("double-key"); await database.delete("string-key"); await database.delete("bool-key"); });