From ecf7aabdb1bb59d8b4ccf2fbe36b307e2a47bf66 Mon Sep 17 00:00:00 2001 From: Helium314 Date: Sun, 9 Feb 2025 15:17:06 +0100 Subject: [PATCH] stricter checks for json layouts (and fix an old bug in hebrew layout) --- app/src/main/assets/layouts/hebrew.json | 4 +- .../keyboard/keyboard/KeyboardSwitcher.java | 2 +- .../keyboard_parser/RawKeyboardParser.kt | 49 +++++++++++++++++-- .../helium314/keyboard/KeyboardParserTest.kt | 9 ++++ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/app/src/main/assets/layouts/hebrew.json b/app/src/main/assets/layouts/hebrew.json index 955f60ea4..99cdaa34b 100644 --- a/app/src/main/assets/layouts/hebrew.json +++ b/app/src/main/assets/layouts/hebrew.json @@ -2,12 +2,12 @@ [ { "$": "variation_selector", "email": { "label": "-" }, - "url": { "label": "-" }, + "uri": { "label": "-" }, "default": { "label": "'", "popup": { "relevant": [{ "label": "׳" }, { "label": "״" }, { "label": "\"" }] } } }, { "$": "variation_selector", "email": { "label": "_" }, - "url": { "label": "_" }, + "uri": { "label": "_" }, "default": { "label": "-", "popup": { "relevant": [{ "label": "־" }, { "label": "_" }] } } }, { "label": "ק", "popup": { diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java index 92d4d656d..d07628e9f 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardSwitcher.java @@ -541,7 +541,7 @@ public final class KeyboardSwitcher implements KeyboardState.SwitchActions { } // Displays a toast-like message with the provided text for a specified duration. - public void showFakeToast(final String text, final int timeMillis) { + private void showFakeToast(final String text, final int timeMillis) { if (mFakeToastView.getVisibility() == View.VISIBLE) return; final Drawable appIcon = mFakeToastView.getCompoundDrawables()[0]; diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/RawKeyboardParser.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/RawKeyboardParser.kt index 80cafd7f0..e26006c0e 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/RawKeyboardParser.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/RawKeyboardParser.kt @@ -27,6 +27,7 @@ import helium314.keyboard.latin.utils.ScriptUtils.script import helium314.keyboard.latin.utils.getCustomFunctionalLayoutName import helium314.keyboard.latin.utils.getCustomLayoutFile import helium314.keyboard.latin.utils.getCustomLayoutFiles +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.polymorphic @@ -69,7 +70,9 @@ object RawKeyboardParser { * codes of multi_text_key not used, only the label * (currently) popups is always read to [number, main, relevant] layoutPopupKeys, no choice of which to use or which hint is provided */ - fun parseJsonString(layoutText: String): List> = florisJsonConfig.decodeFromString(layoutText.stripCommentLines()) + fun parseJsonString(layoutText: String, strict: Boolean = true): List> = + if (strict) checkJsonConfig.decodeFromString(layoutText.stripCommentLines()) + else florisJsonConfig.decodeFromString(layoutText.stripCommentLines()) /** Parse simple layouts, defined only as rows of (normal) keys with popup keys. */ fun parseSimpleString(layoutText: String): List> { @@ -90,7 +93,7 @@ object RawKeyboardParser { val layoutFileName = getLayoutFileName(layoutName, context) val layoutText = if (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) { try { - getCustomLayoutFile(layoutFileName, context).readText() + getCustomLayoutFile(layoutFileName, context).readText().trimStart() } catch (e: Exception) { // fall back to defaults if for some reason file is broken val name = when { layoutName.contains("functional") -> "functional_keys.json" @@ -102,15 +105,17 @@ object RawKeyboardParser { context.assets.open("layouts${File.separator}$name").reader().use { it.readText() } } } else context.assets.open("layouts${File.separator}$layoutFileName").reader().use { it.readText() } - if (layoutFileName.endsWith(".json") || layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX)) { + if (layoutFileName.endsWith(".json") || (layoutFileName.startsWith(CUSTOM_LAYOUT_PREFIX) && layoutText.startsWith("["))) { try { - val florisKeyData = parseJsonString(layoutText) + val florisKeyData = parseJsonString(layoutText, false) return { params -> florisKeyData.mapTo(mutableListOf()) { row -> row.mapNotNullTo(mutableListOf()) { it.compute(params) } } } - } catch (_: Exception) { } + } catch (e: Exception) { + Log.w(TAG, "could not parse json layout for $layoutName, falling back to simple layout parsing", e) + } } // not a json, or invalid json val simpleKeyData = parseSimpleString(layoutText) @@ -178,7 +183,9 @@ object RawKeyboardParser { * modified * SPDX-License-Identifier: Apache-2.0 */ + @OptIn(ExperimentalSerializationApi::class) private val florisJsonConfig = Json { + allowTrailingComma = true classDiscriminator = "$" encodeDefaults = true ignoreUnknownKeys = true @@ -205,4 +212,36 @@ object RawKeyboardParser { } } } + + // copy of florisJsonConfig, but with ignoreUnknownKeys = false so users get warned + // this is not default because users may have old layouts that should not stop working on app upgrade + @OptIn(ExperimentalSerializationApi::class) + private val checkJsonConfig = Json { + allowTrailingComma = true + classDiscriminator = "$" + encodeDefaults = true + ignoreUnknownKeys = false + isLenient = true + serializersModule = SerializersModule { + polymorphic(AbstractKeyData::class) { + subclass(TextKeyData::class, TextKeyData.serializer()) + subclass(AutoTextKeyData::class, AutoTextKeyData.serializer()) + subclass(MultiTextKeyData::class, MultiTextKeyData.serializer()) + subclass(CaseSelector::class, CaseSelector.serializer()) + subclass(ShiftStateSelector::class, ShiftStateSelector.serializer()) + subclass(VariationSelector::class, VariationSelector.serializer()) + subclass(KeyboardStateSelector::class, KeyboardStateSelector.serializer()) + subclass(LayoutDirectionSelector::class, LayoutDirectionSelector.serializer()) + subclass(CharWidthSelector::class, CharWidthSelector.serializer()) + subclass(KanaSelector::class, KanaSelector.serializer()) + defaultDeserializer { TextKeyData.serializer() } + } + polymorphic(KeyData::class) { + subclass(TextKeyData::class, TextKeyData.serializer()) + subclass(AutoTextKeyData::class, AutoTextKeyData.serializer()) + subclass(MultiTextKeyData::class, MultiTextKeyData.serializer()) + defaultDeserializer { TextKeyData.serializer() } + } + } + } } diff --git a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt index 42b261f9e..0509729df 100644 --- a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt +++ b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt @@ -458,6 +458,15 @@ f""", // no newline at the end } } + @Test fun parseExistingLayouts() { + latinIME.assets.list("layouts")?.forEach { + val content = latinIME.assets.open("layouts/$it").reader().readText() + if (it.endsWith(".json")) + RawKeyboardParser.parseJsonString(content) + else RawKeyboardParser.parseSimpleString(content) + } + } + private data class Expected(val code: Int, val label: String? = null, val icon: String? = null, val text: String? = null, val popups: List>? = null) private fun assertIsExpected(json: String, expected: Expected) {