mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 16:14:40 +00:00
test(nip29): cover weird apostrophe placements + guard relay-URL possessives
Reject a single-character group id (except the default `_`) so a possessive glued to a bare relay URL — `wss://relay.damus.io's uptime` — no longer linkifies group "s". Real ids (relay29/Wisp/0xchat) are all longer. Adds coverage proving only genuine ws/wss relay URLs are peeked: apostrophes after http, nostr:, blossom:, email and bech32 tokens never become group links; plus ws:// (insecure), second-apostrophe boundary, and multi-link cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B5MLY4hq5LXJ2D5WeLRyXj
This commit is contained in:
+66
@@ -99,4 +99,70 @@ class RichTextParserGroupLinkTest {
|
||||
assertTrue(links.groupLinks.isEmpty())
|
||||
assertNull(groupSegmentsOf("odd wss://relay.damus.io' end").firstOrNull())
|
||||
}
|
||||
|
||||
// ---- weird apostrophe placements after non-relay URLs: none may become a group link ----
|
||||
|
||||
@Test
|
||||
fun possessiveAfterRelayUrlIsNotAGroupLink() {
|
||||
// "wss://relay.damus.io's uptime" — the possessive `'s` must not linkify group "s".
|
||||
val text = "how is wss://relay.damus.io's uptime today"
|
||||
val links = UrlParser().parseValidUrls(text)
|
||||
assertTrue(links.groupLinks.isEmpty())
|
||||
assertTrue(links.relayUrls.contains("wss://relay.damus.io"))
|
||||
assertTrue(groupSegmentsOf(text).isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apostropheAfterHttpUrlIsNotAGroupLink() {
|
||||
val links = UrlParser().parseValidUrls("see https://example.com'abc123 here")
|
||||
assertTrue(links.groupLinks.isEmpty())
|
||||
assertTrue(links.withScheme.contains("https://example.com"))
|
||||
assertTrue(groupSegmentsOf("see https://example.com'abc123 here").isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apostropheAfterNostrUriIsNotAGroupLink() {
|
||||
val text = "ping nostr:npub1sn0wdenkukak0d9dfczzeacvhkrgz92ak56egt7vdgzn8pv2wfqqhrjdv9'abc there"
|
||||
val links = UrlParser().parseValidUrls(text)
|
||||
assertTrue(links.groupLinks.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apostropheAfterBlossomUriIsNotAGroupLink() {
|
||||
val text = "file blossom://b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553'abc"
|
||||
assertTrue(UrlParser().parseValidUrls(text).groupLinks.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun apostropheAfterEmailIsNotAGroupLink() {
|
||||
val links = UrlParser().parseValidUrls("mail vitor@example.com'abc please")
|
||||
assertTrue(links.groupLinks.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insecureWsSchemeAlsoLinkifies() {
|
||||
// `ws://` (not just `wss://`) is a relay scheme and must peek the same way.
|
||||
val links = UrlParser().parseValidUrls("dev ws://relay.example.com'abc123 test")
|
||||
assertEquals(setOf("ws://relay.example.com'abc123"), links.groupLinks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun secondApostropheEndsTheGroupId() {
|
||||
// `wss://relay.example.com'abc'def` — the group id is `abc`; `'def` is left as text.
|
||||
val segs = groupSegmentsOf("go wss://relay.example.com'abc'def now")
|
||||
assertEquals(1, segs.size)
|
||||
assertEquals("wss://relay.example.com'abc", segs.first().segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleGroupLinksInOnePostAreAllDetected() {
|
||||
val links =
|
||||
UrlParser().parseValidUrls(
|
||||
"two groups wss://groups.0xchat.com'aaaa and wss://chat.wisp.talk'bbbb here",
|
||||
)
|
||||
assertEquals(
|
||||
setOf("wss://groups.0xchat.com'aaaa", "wss://chat.wisp.talk'bbbb"),
|
||||
links.groupLinks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-1
@@ -47,6 +47,16 @@ data class GroupInviteLink(
|
||||
// swallowing trailing prose punctuation into the link.
|
||||
private fun isIdChar(c: Char): Boolean = c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' || c == '-' || c == '_'
|
||||
|
||||
/**
|
||||
* A single-character group id is only accepted when it is the relay-wide default
|
||||
* group `_`. Every real group id from the reference clients (relay29 random ids,
|
||||
* Wisp/0xchat 12+ chars) is longer, whereas a lone char after an apostrophe is
|
||||
* almost always an English possessive/contraction glued to a bare relay URL
|
||||
* (`wss://relay.damus.io's uptime`). Rejecting it kills that false positive without
|
||||
* dropping any real link.
|
||||
*/
|
||||
private fun isAcceptableGroupId(id: String): Boolean = id.length >= 2 || id == "_"
|
||||
|
||||
private const val CODE_PREFIX = "?code="
|
||||
|
||||
/**
|
||||
@@ -64,6 +74,7 @@ data class GroupInviteLink(
|
||||
var i = from
|
||||
while (i < content.length && isIdChar(content[i])) i++
|
||||
if (i == from) return 0 // empty group id — not a link
|
||||
if (!isAcceptableGroupId(content.substring(from, i))) return 0 // possessive/contraction guard
|
||||
|
||||
// Optional `?code=<code>`; only extend the span when a non-empty code follows.
|
||||
if (content.startsWith(CODE_PREFIX, i)) {
|
||||
@@ -89,7 +100,7 @@ data class GroupInviteLink(
|
||||
val rest = link.substring(apos + 1)
|
||||
val queryIdx = rest.indexOf('?')
|
||||
val groupId = if (queryIdx < 0) rest else rest.substring(0, queryIdx)
|
||||
if (groupId.isEmpty() || !groupId.all { isIdChar(it) }) return null
|
||||
if (!groupId.all { isIdChar(it) } || !isAcceptableGroupId(groupId)) return null
|
||||
|
||||
val code =
|
||||
if (queryIdx >= 0) {
|
||||
|
||||
+25
@@ -77,6 +77,31 @@ class GroupInviteLinkTest {
|
||||
assertNull(GroupInviteLink.parse("'abc123"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsSingleCharPossessive() {
|
||||
// `wss://relay.damus.io's uptime` — the `'s` is a possessive, not a group id.
|
||||
assertNull(GroupInviteLink.parse("wss://relay.damus.io's"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun acceptsUnderscoreDefaultGroupButNotOtherSingleChars() {
|
||||
// `_` is the relay-wide default group and is a legitimate 1-char id; other lone
|
||||
// chars are rejected as contractions.
|
||||
assertEquals("_", GroupInviteLink.parse("wss://relay.example.com'_")?.groupId)
|
||||
assertNull(GroupInviteLink.parse("wss://relay.example.com'x"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suffixLengthRejectsPossessiveS() {
|
||||
// "wss://r.com's uptime" — apostrophe at 11, `s` at 12 → not a link.
|
||||
assertEquals(0, GroupInviteLink.suffixLength("wss://r.com's uptime", 12))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suffixLengthAcceptsUnderscore() {
|
||||
assertEquals(1, GroupInviteLink.suffixLength("wss://r.com'_ hi", 12))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun suffixLengthMeasuresGroupIdOnly() {
|
||||
// "wss://r.com'abc def" — apostrophe at index 11, group id starts at 12.
|
||||
|
||||
Reference in New Issue
Block a user