mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(android): keep link preview metadata UTF-16 safe (#102988)
* fix(android): keep link preview metadata UTF-16 safe * refactor(android): share UTF-16-safe truncation --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
internal fun String.takeUtf16Safe(maxChars: Int): String {
|
||||
if (length <= maxChars) return this
|
||||
// Keep the code-unit cap without leaving a high surrogate at its boundary.
|
||||
val endsOnHighSurrogate = maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])
|
||||
return take(if (endsOnHighSurrogate) maxChars - 1 else maxChars)
|
||||
}
|
||||
+1
-11
@@ -4,6 +4,7 @@ import ai.openclaw.app.NotificationBurstLimiter
|
||||
import ai.openclaw.app.SecurePrefs
|
||||
import ai.openclaw.app.allowsPackage
|
||||
import ai.openclaw.app.isWithinQuietHours
|
||||
import ai.openclaw.app.takeUtf16Safe
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.RemoteInput
|
||||
@@ -30,17 +31,6 @@ internal fun sanitizeNotificationText(value: CharSequence?): String? {
|
||||
return normalized.takeUtf16Safe(MAX_NOTIFICATION_TEXT_CHARS).ifEmpty { null }
|
||||
}
|
||||
|
||||
private fun String.takeUtf16Safe(maxChars: Int): String {
|
||||
if (length <= maxChars) return this
|
||||
val end =
|
||||
if (maxChars > 0 && Character.isHighSurrogate(this[maxChars - 1])) {
|
||||
maxChars - 1
|
||||
} else {
|
||||
maxChars
|
||||
}
|
||||
return take(end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable notification snapshot entry exposed through the Android notifications command.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package ai.openclaw.app.ui.chat
|
||||
|
||||
import ai.openclaw.app.takeUtf16Safe
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.util.LruCache
|
||||
@@ -524,7 +525,7 @@ private fun sanitizeMetadataText(
|
||||
.filterNot(Character::isISOControl)
|
||||
.replace(Regex("\\s+"), " ")
|
||||
.trim()
|
||||
return sanitized.take(maxChars).takeIf(String::isNotEmpty)
|
||||
return sanitized.takeUtf16Safe(maxChars).takeIf(String::isNotEmpty)
|
||||
}
|
||||
|
||||
private fun findTitle(html: String): String? =
|
||||
|
||||
@@ -164,6 +164,21 @@ class ChatLinkPreviewTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun metadataTruncationPreservesUtf16Boundaries() {
|
||||
val titlePrefix = "t".repeat(LINK_PREVIEW_TITLE_MAX_CHARS - 1)
|
||||
val descriptionPrefix = "d".repeat(LINK_PREVIEW_DESCRIPTION_MAX_CHARS - 2)
|
||||
val result =
|
||||
parseOpenGraph(
|
||||
"<meta property='og:title' content='$titlePrefix\uD83D\uDE80 trailing'>" +
|
||||
"<meta property='og:description' content='$descriptionPrefix\uD83D\uDE80 trailing'>",
|
||||
"https://example.com",
|
||||
) as LinkPreviewResult.Loaded
|
||||
|
||||
assertEquals(titlePrefix, result.metadata.title)
|
||||
assertEquals("$descriptionPrefix\uD83D\uDE80", result.metadata.description)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchesHtmlWithoutAmbientHeaders() =
|
||||
withServer { server ->
|
||||
|
||||
Reference in New Issue
Block a user