Skip to content

Commit 8a4a394

Browse files
authored
filter bidi isolates in notifications, fixes #60 (#68)
* stripBidiIsolates in notifications, fixes #60 * add io.rebble.libpebblecommon.util.stripBidiIsolates * test for BidiSanitizer --------- Co-authored-by: Iakov Davydov <[email protected]>
1 parent c307817 commit 8a4a394

File tree

4 files changed

+66
-6
lines changed

4 files changed

+66
-6
lines changed

libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/notification/LibPebbleNotificationAction.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import io.rebble.libpebblecommon.NotificationConfig
99
import io.rebble.libpebblecommon.database.entity.ChannelItem
1010
import io.rebble.libpebblecommon.database.entity.NotificationAppItem
1111
import io.rebble.libpebblecommon.packets.blobdb.TimelineItem
12+
import io.rebble.libpebblecommon.util.stripBidiIsolates
1213

1314
data class ActionRemoteInput(
1415
val remoteInput: RemoteInput,
@@ -59,12 +60,12 @@ data class LibPebbleNotificationAction(
5960
} else {
6061
SemanticAction.None
6162
}
62-
val title = action.title?.toString() ?: return null
63+
val title = stripBidiIsolates(action.title) ?: return null
6364
val pendingIntent = action.actionIntent ?: return null
6465
val input = action.remoteInputs?.firstOrNull {
6566
it.allowFreeFormInput
6667
}
67-
val suggestedResponses = input?.choices?.map { it.toString() }
68+
val suggestedResponses = input?.choices?.map { stripBidiIsolates(it.toString()) ?: "" }
6869
return LibPebbleNotificationAction(
6970
packageName = packageName,
7071
title = title,

libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/notification/processor/BasicNotificationProcessor.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import io.rebble.libpebblecommon.notification.NotificationDecision
2525
import io.rebble.libpebblecommon.packets.blobdb.TimelineIcon
2626
import io.rebble.libpebblecommon.timeline.TimelineColor
2727
import io.rebble.libpebblecommon.timeline.argbColor
28+
import io.rebble.libpebblecommon.util.stripBidiIsolates
2829
import kotlin.time.Instant
2930
import kotlin.uuid.Uuid
3031

@@ -50,11 +51,13 @@ class BasicNotificationProcessor(
5051
channel,
5152
notificationConfigFlow.value
5253
)
53-
val title = sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE) ?: ""
54+
val title = stripBidiIsolates(
55+
sbn.notification.extras.getCharSequence(Notification.EXTRA_TITLE)
56+
) ?: ""
5457
val text = sbn.notification.extras.getCharSequence(Notification.EXTRA_TEXT)
5558
val bigText = sbn.notification.extras.getCharSequence(Notification.EXTRA_BIG_TEXT)
5659
val showWhen = sbn.notification.extras.getBoolean(Notification.EXTRA_SHOW_WHEN)
57-
val body = bigText ?: text ?: ""
60+
val body = stripBidiIsolates(bigText ?: text) ?: ""
5861
val people = sbn.notification.people()
5962
val contactKeys = people.asContacts(context.context)
6063
val contactEntries = contactKeys.mapNotNull {
@@ -70,8 +73,8 @@ class BasicNotificationProcessor(
7073
uuid = Uuid.random(),
7174
groupKey = sbn.groupKey,
7275
key = sbn.key,
73-
title = title.toString(),
74-
body = body.toString(),
76+
title = title,
77+
body = body,
7578
icon = icon,
7679
timestamp = if (showWhen) {
7780
Instant.fromEpochMilliseconds(sbn.notification.`when`)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.rebble.libpebblecommon.util
2+
3+
/**
4+
* Strips Unicode bidi isolate markers that some upstream apps include in notification text.
5+
*
6+
* These characters are invisible control codes in Unicode, but some downstream renderers
7+
* (e.g. Pebble firmware fonts) may display them as tofu/squares.
8+
*
9+
* Removed range: U+2066..U+2069 (LRI, RLI, FSI, PDI).
10+
*/
11+
fun stripBidiIsolates(text: CharSequence?): String? {
12+
if (text == null) return null
13+
14+
// Allocate a StringBuilder only if we actually encounter isolate markers, so the common case
15+
// (no markers) stays allocation-free.
16+
var out: StringBuilder? = null
17+
for (i in 0 until text.length) {
18+
val ch = text[i]
19+
if (ch >= '\u2066' && ch <= '\u2069') {
20+
if (out == null) {
21+
out = StringBuilder(text.length)
22+
out.append(text, 0, i)
23+
}
24+
continue
25+
}
26+
out?.append(ch)
27+
}
28+
29+
return out?.toString() ?: text.toString()
30+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.rebble.libpebblecommon.util
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertNull
6+
7+
class BidiSanitizerTest {
8+
@Test
9+
fun stripBidiIsolates_nullInput() {
10+
assertNull(stripBidiIsolates(null))
11+
}
12+
13+
@Test
14+
fun stripBidiIsolates_removesIsolateMarkers() {
15+
val input = "\u2068Юлия\u2069 and \u2066abc\u2069 and \u2067xyz\u2069"
16+
val expected = "Юлия and abc and xyz"
17+
assertEquals(expected, stripBidiIsolates(input))
18+
}
19+
20+
@Test
21+
fun stripBidiIsolates_noopWhenNonePresent() {
22+
val input = "Sender Name"
23+
assertEquals(input, stripBidiIsolates(input))
24+
}
25+
}
26+

0 commit comments

Comments
 (0)