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:
parent
672c0b7eb7
commit
e0a2e972b8
@ -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
|
||||
|
||||
@ -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?
|
||||
{
|
||||
|
||||
@ -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])
|
||||
|
||||
Loading…
Reference in New Issue
Block a user