From 59a9f628dfdb0d1c34dd947516577eaab97c348e Mon Sep 17 00:00:00 2001 From: Vincenzo Palazzo Date: Fri, 31 Jul 2026 12:50:21 +0200 Subject: [PATCH] test: pin that unknown file TLVs are skipped, not fatal (#1550) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `BitchatFilePacket.decode` skips tags it does not recognise (`case nil: continue`), which is what keeps the TLV list a floor rather than a ceiling: a field the sender considered optional costs the receiver that field, not the whole file. Nothing pinned it. The behaviour is load-bearing for any peer, version or third-party client that adds a field this build has not seen, and it is also where the two implementations diverge — the Android decoder returns null on an unknown tag, which is why `PrivateMediaMessageIdentity` has to derive its receipt key from fields already on the wire instead of adding one. Worth a test on the side that gets it right so it cannot quietly drift into the strict behaviour. Two cases, both hand-built so they do not depend on our own encoder: an unknown TLV between MIME_TYPE and CONTENT (where an encoder appending content last would put it), and one trailing CONTENT. Changing `case nil: continue` to `return nil` fails both. Co-authored-by: jack <212554440+jackjackbits@users.noreply.github.com> --- .../Protocols/BitchatFilePacketTests.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/bitchatTests/Protocols/BitchatFilePacketTests.swift b/bitchatTests/Protocols/BitchatFilePacketTests.swift index 2476647f..22ea83f9 100644 --- a/bitchatTests/Protocols/BitchatFilePacketTests.swift +++ b/bitchatTests/Protocols/BitchatFilePacketTests.swift @@ -75,6 +75,70 @@ final class BitchatFilePacketTests: XCTestCase { XCTAssertEqual(decoded.content, content) } + /// The TLV tag list is a floor, not a ceiling: a decoder that bails on the + /// first tag it does not know makes the format unextendable, because a field + /// the sender considered optional costs the receiver the whole file. This + /// decoder skips them (`case nil: continue`) and that has to stay true — it + /// is load-bearing for any peer, version or third-party client that adds a + /// field we have not seen. `PrivateMediaMessageIdentity` exists precisely + /// because the Android decoder does *not* do this, so the asymmetry is real + /// and worth pinning on the side that gets it right. + func testDecodeSkipsUnknownTLVTypesInsteadOfDroppingTheFile() throws { + let content = Data((0..<64).map { UInt8($0) }) + let unknownValue = Data("some-message-id".utf8) + var data = Data() + + // fileName + data.append(0x01) + data.append(contentsOf: [0x00, 0x09]) + data.append(Data("photo.jpg".utf8)) + // fileSize + data.append(0x02) + data.append(contentsOf: [0x00, 0x04]) + data.append(contentsOf: [0x00, 0x00, 0x00, UInt8(content.count)]) + // mimeType + data.append(0x03) + data.append(contentsOf: [0x00, 0x0A]) + data.append(Data("image/jpeg".utf8)) + // An unknown tag, where an encoder appending content last would put it + data.append(0x05) + data.append(contentsOf: [0x00, UInt8(unknownValue.count)]) + data.append(unknownValue) + // content + data.append(0x04) + data.append(contentsOf: [0x00, 0x00, 0x00, UInt8(content.count)]) + data.append(content) + + let decoded = try XCTUnwrap(BitchatFilePacket.decode(data)) + XCTAssertEqual(decoded.fileName, "photo.jpg") + XCTAssertEqual(decoded.mimeType, "image/jpeg") + XCTAssertEqual(decoded.fileSize, UInt64(content.count)) + XCTAssertEqual(decoded.content, content) + } + + /// Same contract for an extension that trails the content, which a decoder + /// stopping at the first unknown tag would also lose. + func testDecodeSkipsAnUnknownTLVTrailingTheContent() throws { + let content = Data(repeating: 0x7F, count: 16) + var data = Data() + + data.append(0x01) + data.append(contentsOf: [0x00, 0x08]) + data.append(Data("note.m4a".utf8)) + data.append(0x04) + data.append(contentsOf: [0x00, 0x00, 0x00, UInt8(content.count)]) + data.append(content) + data.append(0x7F) + data.append(contentsOf: [0x00, 0x04]) + data.append(Data([0x11, 0x11, 0x11, 0x11])) + + let decoded = try XCTUnwrap(BitchatFilePacket.decode(data)) + XCTAssertEqual(decoded.fileName, "note.m4a") + XCTAssertNil(decoded.mimeType) + XCTAssertEqual(decoded.fileSize, UInt64(content.count)) + XCTAssertEqual(decoded.content, content) + } + func testPrivateMediaMessageIdentityConvergesAcrossPeerIDAliases() throws { let senderKey = Data(repeating: 0x11, count: 32) let recipientKey = Data(repeating: 0x22, count: 32)