test: pin that unknown file TLVs are skipped, not fatal (#1550)

`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>
This commit is contained in:
Vincenzo Palazzo 2026-07-31 12:50:21 +02:00 committed by GitHub
parent 7b39d72bec
commit 59a9f628df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -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)