fix: decode long attributedBody typedstream payloads

Decode typedstream attributedBody segments that use 0x81/0x82 length prefixes so long fallback message text is preserved in history/watch output.

Also records the fix in the 0.7.0 changelog.

Co-authored-by: Sagar Dagdu <shags032@gmail.com>
This commit is contained in:
Sagar Dagdu 2026-05-06 11:24:45 +05:30 committed by GitHub
parent 672c0b7eb7
commit e0a2e972b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 7 deletions

View File

@ -12,6 +12,9 @@
- fix: route default bridge calls over v2 IPC when available and reject
unsupported `chat-create --service SMS` requests instead of reporting a
service that was not applied.
- fix: decode typedstream attributed bodies with `0x81`/`0x82` length prefixes
so long fallback message text is preserved in history and watch output (#99,
thanks @SagarSDagdu).
### Docs And CI
- docs: publish the per-feature docs site at `imsg.sh` and add

View File

@ -19,13 +19,8 @@ enum TypedStreamParser {
if bytes[index] == start[0], bytes[index + 1] == start[1] {
let sliceStart = index + 2
if let sliceEnd = findSequence(end, in: bytes, from: sliceStart) {
var segment = Array(bytes[sliceStart..<sliceEnd])
// Check if first byte equals length prefix (convert byte to Int for comparison)
if segment.count > 1, Int(segment[0]) == segment.count - 1 {
segment.removeFirst()
}
let candidate = String(decoding: segment, as: UTF8.self)
.trimmingLeadingControlCharacters()
let segment = Array(bytes[sliceStart..<sliceEnd])
let candidate = decodeSegment(segment)
if candidate.count > best.count {
best = candidate
}
@ -42,6 +37,39 @@ enum TypedStreamParser {
return text.trimmingLeadingControlCharacters()
}
/// Strips a typedstream length prefix from `segment` and returns the longest valid UTF-8 decoding.
/// Length prefix forms (BER-style): single byte (< 0x80), `0x81 NN`, or `0x82 NN NN`. The older
/// implementation only handled the single-byte form, which silently dropped any message longer
/// than 127 bytes because the unstripped 0x81/0x82 byte is invalid as a UTF-8 leading byte.
private static func decodeSegment(_ segment: [UInt8]) -> String {
guard let first = segment.first else { return "" }
var prefixLengths: Set<Int> = [0]
if first < 0x80, Int(first) == segment.count - 1 {
prefixLengths.insert(1)
}
if first == 0x81, segment.count >= 2 {
prefixLengths.insert(2)
}
if first == 0x82, segment.count >= 3 {
prefixLengths.insert(3)
}
var best = ""
for prefixLen in prefixLengths {
guard prefixLen <= segment.count else { continue }
let body = Array(segment[prefixLen...])
guard
let candidate = String(bytes: body, encoding: .utf8)?
.trimmingLeadingControlCharacters()
else { continue }
if candidate.count > best.count {
best = candidate
}
}
return best
}
private static func findSequence(_ needle: [UInt8], in haystack: [UInt8], from start: Int)
-> Int?
{

View File

@ -156,6 +156,44 @@ func typedStreamParserTrimsControlCharacters() {
#expect(TypedStreamParser.parseAttributedBody(data) == "hello")
}
@Test
func typedStreamParserDecodesShortSingleBytePrefix() {
let text = "hello"
let bytes: [UInt8] =
[0x01, 0x2b, UInt8(text.utf8.count)] + Array(text.utf8) + [0x86, 0x84]
#expect(TypedStreamParser.parseAttributedBody(Data(bytes)) == text)
}
@Test
func typedStreamParserDecodesMediumMessageWith0x81Prefix() {
let text = String(repeating: "A", count: 140)
let length = UInt8(text.utf8.count)
let bytes: [UInt8] =
[0x01, 0x2b, 0x81, length] + Array(text.utf8) + [0x86, 0x84]
#expect(TypedStreamParser.parseAttributedBody(Data(bytes)) == text)
}
@Test
func typedStreamParserDecodesLongMessageWith0x82Prefix() {
let text = String(repeating: "B", count: 300)
let length = UInt16(text.utf8.count)
let lengthHi = UInt8((length >> 8) & 0xff)
let lengthLo = UInt8(length & 0xff)
let bytes: [UInt8] =
[0x01, 0x2b, 0x82, lengthHi, lengthLo] + Array(text.utf8) + [0x86, 0x84]
#expect(TypedStreamParser.parseAttributedBody(Data(bytes)) == text)
}
@Test
func typedStreamParserHandlesMixedBinaryNoise() {
// First byte 0x42 is neither 0x81 nor 0x82, and does not equal segment.count - 1 (= 6).
// The decoder should fall back to no-prefix decoding without crashing.
let bytes: [UInt8] =
[0x01, 0x2b, 0x42, 0x68, 0x69, 0x21, 0x86, 0x84]
let result = TypedStreamParser.parseAttributedBody(Data(bytes))
#expect(result == "Bhi!")
}
@Test
func typedStreamParserDecodesUTF16LittleEndianBOM() throws {
var data = Data([0xff, 0xfe])