mirror of
https://github.com/Tencent/MMKV.git
synced 2026-10-16 20:21:12 +08:00
add harness for ohos ArrayBuffer
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
|
||||
This is a bugfix release based on v2.4.2, focused on encryption, expiration, imports, and file recovery. It also adds Android KMP initialization without an explicit Context and non-expired-key enumeration on Apple platforms.
|
||||
|
||||
**Upcoming compatibility change:** The release after v2.4.3 will raise the minimum supported versions from iOS 13.0 to **iOS 15.0** and from watchOS 6.0 to **watchOS 9.0**, to align with Xcode 27's supported deployment targets. The minimum deployment targets in v2.4.3 remain unchanged.
|
||||
|
||||
### Changes for All Platforms
|
||||
* **Fix:** Prevented IV reuse when repeatedly updating an encrypted store containing a single key.
|
||||
* **Fix:** Loaded expiration state before the first read or write after reopening a store, and honored expiration in read-only reads, key lists, and counts.
|
||||
|
||||
@@ -68,5 +68,5 @@ recover
|
||||
itemSizeLimit
|
||||
|
||||
-keep-dts
|
||||
/Users/lingol/Developer/mmkv/OpenHarmony/MMKV/src/main/cpp/types/libmmkv/
|
||||
/Users/guoling/Developer/mmkv/OpenHarmony/MMKV/src/main/cpp/types/libmmkv/
|
||||
|
||||
|
||||
@@ -16,3 +16,29 @@ Coverage includes malformed numeric ranges and wrong value types, N-API failures
|
||||
typed element size and actual pointer alignment, missing arguments, optional expiration dispatch, empty slices,
|
||||
zero-copy pointers for valid nonempty slices, no write/preservation on rejection for all five encoders,
|
||||
and empty/nonempty output ArrayBuffers (including a null backing pointer for zero bytes).
|
||||
|
||||
The complementary device harness is `entry/src/ohosTest/ets/test/ArrayBufferBounds.test.ets` (relative to
|
||||
`OpenHarmony`). Its 40 Hypium cases exercise the real N-API callbacks and MMKV storage for all five encoders:
|
||||
valid nonzero offsets, invalid numeric ranges and bounds, typed alignment, preservation after rejection,
|
||||
omitted/undefined expiration, expiration-enabled writes, empty buffers, negative zero, and public typed subviews.
|
||||
It creates unique stores under `filesDir/mmkv_arraybuffer_bounds` and removes only those stores after testing.
|
||||
Wrong argument types, simulated N-API failures, and null backing pointers remain covered by the portable harness.
|
||||
|
||||
With DevEco's Node/Hvigor/HDC on `PATH`, `DEVECO_SDK_HOME` configured, and an existing debug signing configuration
|
||||
valid for the attached physical device, build from `OpenHarmony`:
|
||||
|
||||
```sh
|
||||
ohpm install
|
||||
hvigorw --mode module -p module=entry@ohosTest -p product=default -p buildMode=debug assembleHap --no-daemon
|
||||
```
|
||||
|
||||
The demo's main module must already be installed with the same signing identity. Select the physical device's
|
||||
serial explicitly, then install the test module without uninstalling or clearing the demo:
|
||||
|
||||
```sh
|
||||
hdc -t "$DEVICE_SERIAL" install -r entry/build/default/outputs/ohosTest/entry-ohosTest-signed.hap
|
||||
hdc -t "$DEVICE_SERIAL" shell aa test -b com.tencent.mmkvdemo -m entry_test \
|
||||
-s unittest OpenHarmonyTestRunner -s class ArrayBufferBounds -s timeout 15000
|
||||
```
|
||||
|
||||
Check the reported test count and failures; a successful HDC exit alone does not establish that the suite passed.
|
||||
|
||||
@@ -9,5 +9,8 @@
|
||||
"@tencent/mmkv": "file:../MMKV",
|
||||
// "@tencent/mmkv": "~2.4.3",
|
||||
"libentry.so": "file:./src/main/cpp/types/libentry",
|
||||
},
|
||||
"devDependencies": {
|
||||
"libmmkv.so": "file:../MMKV/src/main/cpp/types/libmmkv"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright (C) 2026 THL A29 Limited, a Tencent company.
|
||||
* Licensed under the BSD 3-Clause License.
|
||||
*/
|
||||
|
||||
import { abilityDelegatorRegistry } from '@kit.TestKit';
|
||||
import { beforeAll, beforeEach, afterAll, describe, expect, it } from '@ohos/hypium';
|
||||
import { MMKV, MMKVLogLevel } from '@tencent/mmkv';
|
||||
import native from 'libmmkv.so';
|
||||
|
||||
interface SliceCodec {
|
||||
name: string;
|
||||
elementSize: number;
|
||||
encode: (handle: bigint, key: string, value: ArrayBuffer, offset: number, length: number,
|
||||
expiration?: number) => boolean;
|
||||
decode: (handle: bigint, key: string, defaultValue?: ArrayBuffer) => ArrayBuffer;
|
||||
}
|
||||
|
||||
function assertBytes(actual: ArrayBuffer, expected: Uint8Array): void {
|
||||
expect(actual instanceof ArrayBuffer).assertTrue();
|
||||
const bytes = new Uint8Array(actual);
|
||||
expect(bytes.length).assertEqual(expected.length);
|
||||
for (let index = 0; index < expected.length; index++) {
|
||||
expect(bytes[index]).assertEqual(expected[index]);
|
||||
}
|
||||
}
|
||||
|
||||
export default function arrayBufferBoundsTest(): void {
|
||||
describe('ArrayBufferBounds', () => {
|
||||
const codecs: SliceCodec[] = [
|
||||
{ name: 'bytes', elementSize: 1, encode: native.encodeBytes, decode: native.decodeBytes },
|
||||
{ name: 'int32', elementSize: 4, encode: native.encodeInt32Array, decode: native.decodeInt32Array },
|
||||
{ name: 'uint32', elementSize: 4, encode: native.encodeUInt32Array, decode: native.decodeUInt32Array },
|
||||
{ name: 'int64', elementSize: 8, encode: native.encodeInt64Array, decode: native.decodeInt64Array },
|
||||
{ name: 'uint64', elementSize: 8, encode: native.encodeUInt64Array, decode: native.decodeUInt64Array }
|
||||
];
|
||||
const source = new Uint8Array(24);
|
||||
for (let index = 0; index < source.length; index++) {
|
||||
source[index] = index + 1;
|
||||
}
|
||||
const buffer = source.buffer as ArrayBuffer;
|
||||
const fallback = new Uint8Array([255, 254, 253, 252, 251, 250, 249, 248]).buffer as ArrayBuffer;
|
||||
const key = 'slice';
|
||||
let handle: bigint = 0n;
|
||||
let mmapID: string = '';
|
||||
let rootDir: string = '';
|
||||
|
||||
beforeAll(() => {
|
||||
const context = abilityDelegatorRegistry.getAbilityDelegator().getAppContext();
|
||||
rootDir = context.filesDir + '/mmkv_arraybuffer_bounds';
|
||||
MMKV.initializeWithPath(rootDir, context.cacheDir, MMKVLogLevel.LevelWarning);
|
||||
mmapID = 'arraybuffer_bounds_' + Date.now().toString() + '_' + Math.random().toString().slice(2);
|
||||
handle = native.mmkvWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, undefined, rootDir);
|
||||
expect(handle !== 0n).assertTrue();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
expect(handle !== 0n).assertTrue();
|
||||
native.clearAll(handle, false);
|
||||
expect(native.disableAutoKeyExpire(handle)).assertTrue();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (handle !== 0n) {
|
||||
native.close(handle);
|
||||
handle = 0n;
|
||||
expect(native.removeStorage(mmapID, rootDir)).assertTrue();
|
||||
}
|
||||
});
|
||||
|
||||
const seed = (codec: SliceCodec): void => {
|
||||
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2)).assertTrue();
|
||||
expect(native.containsKey(handle, key)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
|
||||
};
|
||||
|
||||
const reject = (codec: SliceCodec, offset: number, length: number, expiration?: number): void => {
|
||||
expect(codec.encode(handle, key, buffer, offset, length, expiration)).assertFalse();
|
||||
expect(native.containsKey(handle, key)).assertTrue();
|
||||
expect(native.count(handle, false) === 1n).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
|
||||
};
|
||||
|
||||
codecs.forEach((codec: SliceCodec) => {
|
||||
it(codec.name + '_valid_nonzero_offset', 0, () => {
|
||||
seed(codec); // Five arguments: the optional expiration is genuinely omitted.
|
||||
native.clearMemoryCache(handle);
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
|
||||
});
|
||||
|
||||
it(codec.name + '_explicit_undefined_expiration', 0, () => {
|
||||
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2, undefined)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
|
||||
});
|
||||
|
||||
it(codec.name + '_invalid_numbers_preserve_value', 0, () => {
|
||||
seed(codec);
|
||||
const invalid: number[] = [-1, 0.5, NaN, Infinity, -Infinity, 4294967295, 4294967296, 4294967297];
|
||||
invalid.forEach((value: number) => {
|
||||
reject(codec, value, codec.elementSize * 2);
|
||||
reject(codec, codec.elementSize, value);
|
||||
});
|
||||
});
|
||||
|
||||
it(codec.name + '_out_of_bounds_preserve_value', 0, () => {
|
||||
seed(codec);
|
||||
reject(codec, buffer.byteLength + 1, 0);
|
||||
reject(codec, buffer.byteLength, codec.elementSize);
|
||||
reject(codec, buffer.byteLength - codec.elementSize, codec.elementSize * 2);
|
||||
reject(codec, 0, buffer.byteLength + codec.elementSize);
|
||||
});
|
||||
|
||||
if (codec.elementSize > 1) {
|
||||
it(codec.name + '_misaligned_slices_preserve_value', 0, () => {
|
||||
seed(codec);
|
||||
reject(codec, 1, codec.elementSize);
|
||||
reject(codec, 0, codec.elementSize + 1);
|
||||
});
|
||||
}
|
||||
|
||||
it(codec.name + '_empty_slices_roundtrip', 0, () => {
|
||||
[0, 1, buffer.byteLength].forEach((offset: number) => {
|
||||
seed(codec);
|
||||
expect(codec.encode(handle, key, buffer, offset, 0)).assertTrue();
|
||||
expect(native.containsKey(handle, key)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
|
||||
});
|
||||
expect(codec.encode(handle, key, new ArrayBuffer(0), 0, 0)).assertTrue();
|
||||
expect(native.containsKey(handle, key)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
|
||||
});
|
||||
|
||||
it(codec.name + '_negative_zero_is_valid', 0, () => {
|
||||
expect(codec.encode(handle, key, buffer, -0, codec.elementSize)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(0, codec.elementSize));
|
||||
});
|
||||
|
||||
it(codec.name + '_expiration_path_preserves_value_on_rejection', 0, () => {
|
||||
expect(native.enableAutoKeyExpire(handle, 0)).assertTrue();
|
||||
expect(codec.encode(handle, key, buffer, codec.elementSize, codec.elementSize * 2, 3600)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), source.subarray(codec.elementSize, codec.elementSize * 3));
|
||||
reject(codec, -1, codec.elementSize, 3600);
|
||||
reject(codec, buffer.byteLength, codec.elementSize, 3600);
|
||||
expect(codec.encode(handle, key, new ArrayBuffer(0), 0, 0, 3600)).assertTrue();
|
||||
expect(native.containsKey(handle, key)).assertTrue();
|
||||
assertBytes(codec.decode(handle, key, fallback), new Uint8Array(0));
|
||||
});
|
||||
});
|
||||
|
||||
it('public_typed_subviews_roundtrip', 0, () => {
|
||||
const wrapperID = mmapID + '_public';
|
||||
const kv = MMKV.mmkvWithID(wrapperID, MMKV.SINGLE_PROCESS_MODE, undefined, rootDir);
|
||||
try {
|
||||
const int32 = new Int32Array([-99, -2147483648, 2147483647, 99]);
|
||||
const uint32 = new Uint32Array([99, 0, 4294967295, 99]);
|
||||
const int64 = new BigInt64Array([-99n, -9223372036854775808n, 9223372036854775807n, 99n]);
|
||||
const uint64 = new BigUint64Array([99n, 0n, 18446744073709551615n, 99n]);
|
||||
expect(kv.encodeTypedArray('int32', int32.subarray(1, 3))).assertTrue();
|
||||
expect(kv.encodeTypedArray('uint32', uint32.subarray(1, 3))).assertTrue();
|
||||
expect(kv.encodeTypedArray('int64', int64.subarray(1, 3))).assertTrue();
|
||||
expect(kv.encodeTypedArray('uint64', uint64.subarray(1, 3))).assertTrue();
|
||||
const int32Result = kv.decodeInt32Array('int32', new Int32Array(1))!;
|
||||
const uint32Result = kv.decodeUint32Array('uint32', new Uint32Array(1))!;
|
||||
const int64Result = kv.decodeInt64Array('int64', new BigInt64Array(1))!;
|
||||
const uint64Result = kv.decodeUint64Array('uint64', new BigUint64Array(1))!;
|
||||
expect(int32Result.length).assertEqual(2);
|
||||
expect(uint32Result.length).assertEqual(2);
|
||||
expect(int64Result.length).assertEqual(2);
|
||||
expect(uint64Result.length).assertEqual(2);
|
||||
for (let index = 0; index < 2; index++) {
|
||||
expect(int32Result[index]).assertEqual(int32[index + 1]);
|
||||
expect(uint32Result[index]).assertEqual(uint32[index + 1]);
|
||||
expect(int64Result[index] === int64[index + 1]).assertTrue();
|
||||
expect(uint64Result[index] === uint64[index + 1]).assertTrue();
|
||||
}
|
||||
} finally {
|
||||
kv.close();
|
||||
expect(MMKV.removeStorage(wrapperID, rootDir)).assertTrue();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,9 @@
|
||||
*/
|
||||
|
||||
import abilityTest from './Ability.test';
|
||||
import arrayBufferBoundsTest from './ArrayBufferBounds.test';
|
||||
|
||||
export default function testsuite() {
|
||||
abilityTest();
|
||||
}
|
||||
arrayBufferBoundsTest();
|
||||
}
|
||||
|
||||
+2
@@ -72,6 +72,7 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
|
||||
BuildableName = "WatchApp.app"
|
||||
BlueprintName = "WatchApp"
|
||||
ReferencedContainer = "container:MMKVDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -90,6 +91,7 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
|
||||
BuildableName = "WatchApp.app"
|
||||
BlueprintName = "WatchApp"
|
||||
ReferencedContainer = "container:MMKVDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
|
||||
BuildableName = "WatchApp.app"
|
||||
BlueprintName = "WatchApp"
|
||||
ReferencedContainer = "container:MMKVDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
@@ -86,6 +87,7 @@
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CBF19011243D6F59001C82ED"
|
||||
BuildableName = "WatchApp.app"
|
||||
BlueprintName = "WatchApp"
|
||||
ReferencedContainer = "container:MMKVDemo.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
|
||||
Reference in New Issue
Block a user