Skip to content

Commit

Permalink
[shared_preferences] Fix Android type mismatch regression (#8512)
Browse files Browse the repository at this point in the history
`getStringList` should throw a `TypeError` if the stored value is of a
different type, but the recent change to use JSON-encoded string lists
regression that behavior if the stored type was a string, causing it to
instead return null.

This restores the previous behavior by passing extra information from
Kotlin to Dart when attempting to get an enecoded string list, so that
if a non-encoded-list string is found, a TypeError can be created on the
Dart side.

Since extra information is now being passed, the case of a
legacy-encoded value is now communicated as well, so that we only have
to request the legacy value if it's there, rather than always trying
(which was not worth the complexity of adding extra data just for that
initially, but now that we need extra data anyway, it's easy to
distinguish that case).

Fixes OOB regression in `shared_preferences` tests that has closed the
tree.
  • Loading branch information
stuartmorgan authored Jan 27, 2025
1 parent 258f6dc commit 99f79d9
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 2.4.4

* Restores the behavior of throwing a `TypeError` when calling `getStringList`
on a value stored with `setString`.

## 2.4.3

* Migrates `List<String>` value encoding to JSON.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down Expand Up @@ -43,6 +43,22 @@ class SharedPreferencesError(
val details: Any? = null
) : Throwable()

/** Possible types found during a getStringList call. */
enum class StringListLookupResultType(val raw: Int) {
/** A deprecated platform-side encoding string list. */
PLATFORM_ENCODED(0),
/** A JSON-encoded string list. */
JSON_ENCODED(1),
/** A string that doesn't have the expected encoding prefix. */
UNEXPECTED_STRING(2);

companion object {
fun ofRaw(raw: Int): StringListLookupResultType? {
return values().firstOrNull { it.raw == raw }
}
}
}

/** Generated class from Pigeon that represents data sent in messages. */
data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useDataStore: Boolean) {
companion object {
Expand All @@ -61,22 +77,59 @@ data class SharedPreferencesPigeonOptions(val fileName: String? = null, val useD
}
}

/** Generated class from Pigeon that represents data sent in messages. */
data class StringListResult(
/** The JSON-encoded stored value, or null if something else was found. */
val jsonEncodedValue: String? = null,
/** The type of value found. */
val type: StringListLookupResultType
) {
companion object {
fun fromList(pigeonVar_list: List<Any?>): StringListResult {
val jsonEncodedValue = pigeonVar_list[0] as String?
val type = pigeonVar_list[1] as StringListLookupResultType
return StringListResult(jsonEncodedValue, type)
}
}

fun toList(): List<Any?> {
return listOf(
jsonEncodedValue,
type,
)
}
}

private open class MessagesAsyncPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let { StringListLookupResultType.ofRaw(it.toInt()) }
}
130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let {
SharedPreferencesPigeonOptions.fromList(it)
}
}
131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { StringListResult.fromList(it) }
}
else -> super.readValueOfType(type, buffer)
}
}

override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is SharedPreferencesPigeonOptions -> {
is StringListLookupResultType -> {
stream.write(129)
writeValue(stream, value.raw)
}
is SharedPreferencesPigeonOptions -> {
stream.write(130)
writeValue(stream, value.toList())
}
is StringListResult -> {
stream.write(131)
writeValue(stream, value.toList())
}
else -> super.writeValue(stream, value)
Expand Down Expand Up @@ -119,8 +172,8 @@ interface SharedPreferencesAsyncApi {
key: String,
options: SharedPreferencesPigeonOptions
): List<String>?
/** Gets individual List<String> value stored with [key], if any. */
fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String?
/** Gets the JSON-encoded List<String> value stored with [key], if any. */
fun getStringList(key: String, options: SharedPreferencesPigeonOptions): StringListResult?
/** Removes all properties from shared preferences data set with matching prefix. */
fun clear(allowList: List<String>?, options: SharedPreferencesPigeonOptions)
/** Gets all properties from shared preferences data set with matching prefix. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,20 @@ class SharedPreferencesPlugin() : FlutterPlugin, SharedPreferencesAsyncApi {
}

/** Gets StringList at [key] from data store. */
override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? {
override fun getStringList(
key: String,
options: SharedPreferencesPigeonOptions
): StringListResult? {
val stringValue = getString(key, options)
stringValue?.let {
// The JSON-encoded lists use an extended prefix to distinguish them from
// lists that using listEncoder.
if (stringValue.startsWith(JSON_LIST_PREFIX)) {
return stringValue
return if (stringValue.startsWith(JSON_LIST_PREFIX)) {
StringListResult(stringValue, StringListLookupResultType.JSON_ENCODED)
} else if (stringValue.startsWith(LIST_PREFIX)) {
StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED)
} else {
StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING)
}
}
return null
Expand Down Expand Up @@ -408,12 +415,21 @@ class SharedPreferencesBackend(
}

/** Gets StringList at [key] from data store. */
override fun getStringList(key: String, options: SharedPreferencesPigeonOptions): String? {
override fun getStringList(
key: String,
options: SharedPreferencesPigeonOptions
): StringListResult? {
val preferences = createSharedPreferences(options)
if (preferences.contains(key)) {
val value = preferences.getString(key, "")
if (value!!.startsWith(JSON_LIST_PREFIX)) {
return value
// The JSON-encoded lists use an extended prefix to distinguish them from
// lists that using listEncoder.
return if (value!!.startsWith(JSON_LIST_PREFIX)) {
StringListResult(value, StringListLookupResultType.JSON_ENCODED)
} else if (value.startsWith(LIST_PREFIX)) {
StringListResult(null, StringListLookupResultType.PLATFORM_ENCODED)
} else {
StringListResult(null, StringListLookupResultType.UNEXPECTED_STRING)
}
}
return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,27 @@ internal class SharedPreferencesTest {
fun testSetAndGetStringListWithDataStore() {
val plugin = pluginSetup(dataStoreOptions)
plugin.setEncodedStringList(listKey, testList, dataStoreOptions)
Assert.assertEquals(plugin.getStringList(listKey, dataStoreOptions), testList)
val result = plugin.getStringList(listKey, dataStoreOptions)
Assert.assertEquals(result?.jsonEncodedValue, testList)
Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED)
}

@Test
fun testSetAndGetStringListWithDataStoreRedirectsForPlatformEncoded() {
val plugin = pluginSetup(dataStoreOptions)
plugin.setDeprecatedStringList(listKey, listOf(""), dataStoreOptions)
val result = plugin.getStringList(listKey, dataStoreOptions)
Assert.assertEquals(result?.jsonEncodedValue, null)
Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED)
}

@Test
fun testSetAndGetStringListWithDataStoreReportsRawString() {
val plugin = pluginSetup(dataStoreOptions)
plugin.setString(listKey, testString, dataStoreOptions)
val result = plugin.getStringList(listKey, dataStoreOptions)
Assert.assertEquals(result?.jsonEncodedValue, null)
Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING)
}

@Test
Expand Down Expand Up @@ -217,7 +237,27 @@ internal class SharedPreferencesTest {
fun testSetAndGetStringListWithSharedPreferences() {
val plugin = pluginSetup(sharedPreferencesOptions)
plugin.setEncodedStringList(listKey, testList, sharedPreferencesOptions)
Assert.assertEquals(plugin.getStringList(listKey, sharedPreferencesOptions), testList)
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
Assert.assertEquals(result?.jsonEncodedValue, testList)
Assert.assertEquals(result?.type, StringListLookupResultType.JSON_ENCODED)
}

@Test
fun testSetAndGetStringListWithSharedPreferencesRedirectsForPlatformEncoded() {
val plugin = pluginSetup(sharedPreferencesOptions)
plugin.setDeprecatedStringList(listKey, listOf(""), sharedPreferencesOptions)
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
Assert.assertEquals(result?.jsonEncodedValue, null)
Assert.assertEquals(result?.type, StringListLookupResultType.PLATFORM_ENCODED)
}

@Test
fun testSetAndGetStringListWithSharedPreferencesReportsRawString() {
val plugin = pluginSetup(sharedPreferencesOptions)
plugin.setString(listKey, testString, sharedPreferencesOptions)
val result = plugin.getStringList(listKey, sharedPreferencesOptions)
Assert.assertEquals(result?.jsonEncodedValue, null)
Assert.assertEquals(result?.type, StringListLookupResultType.UNEXPECTED_STRING)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,22 @@ void main() {
expect(list?.length, testList.length + 1);
});

testWidgets('getStringList throws type error for String with $backend',
(WidgetTester _) async {
final SharedPreferencesAsyncAndroidOptions options =
getOptions(useDataStore: useDataStore, fileName: 'notDefault');
final SharedPreferencesAsyncPlatform preferences = getPreferences();
await clearPreferences(preferences, options);

await preferences.setString(listKey, testString, options);

// Internally, List<String> is stored as a String on Android, but that
// implementation detail shouldn't leak to clients; getting the wrong
// type should still throw.
expect(preferences.getStringList(listKey, options),
throwsA(isA<TypeError>()));
});

testWidgets('getPreferences with $backend', (WidgetTester _) async {
final SharedPreferencesAsyncAndroidOptions options =
getOptions(useDataStore: useDataStore, fileName: 'notDefault');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v22.7.2), do not edit directly.
// Autogenerated from Pigeon (v22.7.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

Expand All @@ -18,6 +18,18 @@ PlatformException _createConnectionError(String channelName) {
);
}

/// Possible types found during a getStringList call.
enum StringListLookupResultType {
/// A deprecated platform-side encoding string list.
platformEncoded,

/// A JSON-encoded string list.
jsonEncoded,

/// A string that doesn't have the expected encoding prefix.
unexpectedString,
}

class SharedPreferencesPigeonOptions {
SharedPreferencesPigeonOptions({
this.fileName,
Expand All @@ -44,15 +56,49 @@ class SharedPreferencesPigeonOptions {
}
}

class StringListResult {
StringListResult({
this.jsonEncodedValue,
required this.type,
});

/// The JSON-encoded stored value, or null if something else was found.
String? jsonEncodedValue;

/// The type of value found.
StringListLookupResultType type;

Object encode() {
return <Object?>[
jsonEncodedValue,
type,
];
}

static StringListResult decode(Object result) {
result as List<Object?>;
return StringListResult(
jsonEncodedValue: result[0] as String?,
type: result[1]! as StringListLookupResultType,
);
}
}

class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is SharedPreferencesPigeonOptions) {
} else if (value is StringListLookupResultType) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is SharedPreferencesPigeonOptions) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is StringListResult) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
Expand All @@ -63,7 +109,12 @@ class _PigeonCodec extends StandardMessageCodec {
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final int? value = readValue(buffer) as int?;
return value == null ? null : StringListLookupResultType.values[value];
case 130:
return SharedPreferencesPigeonOptions.decode(readValue(buffer)!);
case 131:
return StringListResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
Expand Down Expand Up @@ -373,8 +424,8 @@ class SharedPreferencesAsyncApi {
}
}

/// Gets individual List<String> value stored with [key], if any.
Future<String?> getStringList(
/// Gets the JSON-encoded List<String> value stored with [key], if any.
Future<StringListResult?> getStringList(
String key, SharedPreferencesPigeonOptions options) async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.shared_preferences_android.SharedPreferencesAsyncApi.getStringList$pigeonVar_messageChannelSuffix';
Expand All @@ -395,7 +446,7 @@ class SharedPreferencesAsyncApi {
details: pigeonVar_replyList[2],
);
} else {
return (pigeonVar_replyList[0] as String?);
return (pigeonVar_replyList[0] as StringListResult?);
}
}

Expand Down
Loading

0 comments on commit 99f79d9

Please sign in to comment.