Understand a file somebody sent, rather than showing a line of text

iOS can now send a document from Files. This build treated `file` as a kind
from a newer peer: legible, because a file envelope always carries a
caption naming the document, but only as text.

The engine now knows the kind, parses the additive `fn`/`cls`/`cmp` keys,
and can assemble them — so sending works in both directions, not one.

🔴 The interop test parses an envelope produced by the iOS build rather
than by this engine. Two implementations of one format that each only test
against themselves agree right up to the day somebody sends a real file,
and the symptom is "[unsupported attachment]" with no clue why. It also
pins the two rules that keep older peers working: a file envelope is never
captionless, and a photo envelope still carries none of the new keys.

⚠️ The bubble shows the classification as a MARKING. It says how the
sender had it filed; it does not mean this device was cleared for it, and
whoever can read the message already has the key.

⚠️ No "Save to Files" on this side yet — this build has no Files of its
own, so the button would be one that cannot work. Showing what arrived is
the honest half of parity while the other half is built.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-03 12:21:41 -04:00
co-authored by Claude Opus 4.8
parent df00bea388
commit 688a519e0c
3 changed files with 315 additions and 6 deletions
@@ -0,0 +1,164 @@
package io.flamenet.e2e
import org.json.JSONObject
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.assertFalse
/**
* Documents sent between the two apps.
*
* 🔴 **The envelope in [IOS_FILE] was produced by the iOS build**, not by this
* engine. Two implementations of one format that each only test against
* themselves agree right up to the day somebody sends a real file — and the
* symptom is a message that arrives as "[unsupported attachment]" with no clue
* why. This is the test that catches a drift in the field names before then.
*/
class FileEnvelopeInteropTest {
// Standard base64 with padding, which is what Bytes.unb64 decodes.
private val key = "ERERERERERERERERERERERERERERERERERERERERERE="
private val digest = "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI="
/** As iOS assembles it: `fn`, `cls`, `cmp` as additive top-level keys. */
private val iosFile = """
{"fnc":1,"kind":"file","caption":"📎 Lease agreement.pdf",
"att":{"id":"${"a".repeat(64)}","key":"$key","digest":"$digest",
"bytes":4096,"mime":"application/pdf","w":0,"h":0},
"fn":"Lease agreement.pdf","cls":2,"cmp":[0,1]}
""".trimIndent().replace("\n", "")
@Test
fun `a file envelope from iOS parses with its name and label`() {
val content = Content.parse(iosFile)
val ref = assertNotNull(content.attachment, "no attachment parsed")
assertEquals(Attachment.FILE, ref.kind)
assertEquals("Lease agreement.pdf", ref.filename)
assertEquals(2, ref.classification)
assertEquals(listOf(0, 1), ref.compartments)
assertEquals(4096, ref.bytes)
assertEquals("application/pdf", ref.mime)
}
/** 🔴 It must be recognised, not merely tolerated. */
@Test
fun `a file is a known kind rather than needing a newer client`() {
val content = Content.parse(iosFile)
assertFalse(content.needsNewerClient, "file is still treated as an unknown kind")
assertFalse(content.isControl, "a document is not control traffic")
}
@Test
fun `this engine assembles the same shape it parses`() {
val ref = Attachment.Ref(
kind = Attachment.FILE,
id = "b".repeat(64),
key = ByteArray(32) { 0x11 },
digest = ByteArray(32) { 0x22 },
bytes = 2048,
mime = "application/pdf",
filename = "Settlement draft.docx",
classification = 3,
compartments = listOf(2),
)
val json = Content.attachment(ref)
val parsed = JSONObject(json)
assertEquals("file", parsed.getString("kind"))
assertEquals("Settlement draft.docx", parsed.getString("fn"))
assertEquals(3, parsed.getInt("cls"))
assertEquals(1, parsed.getJSONArray("cmp").length())
val back = assertNotNull(Content.parse(json).attachment)
assertEquals("Settlement draft.docx", back.filename)
assertEquals(3, back.classification)
assertEquals(listOf(2), back.compartments)
}
/**
* 🔴 The compatibility rule, on this side too: never captionless, because
* a build that predates this kind shows only the caption.
*/
@Test
fun `a file envelope always carries a caption naming the document`() {
val ref = Attachment.Ref(
kind = Attachment.FILE, id = "c".repeat(64),
key = ByteArray(32) { 0x11 }, digest = ByteArray(32) { 0x22 },
bytes = 10, mime = "application/pdf", filename = "notes.txt",
)
val json = Content.attachment(ref)
assertTrue(json.contains("\"caption\""), "a file envelope went out captionless")
assertEquals("📎 notes.txt", Content.parse(json).text)
}
@Test
fun `a caption the sender wrote is not replaced`() {
val ref = Attachment.Ref(
kind = Attachment.FILE, id = "d".repeat(64),
key = ByteArray(32) { 0x11 }, digest = ByteArray(32) { 0x22 },
bytes = 10, mime = "application/pdf", filename = "notes.txt",
)
assertEquals("signed copy", Content.parse(Content.attachment(ref, "signed copy")).text)
}
/** ⚠️ An older peer sends no `fn`/`cls`/`cmp`. It must parse, not throw. */
@Test
fun `an envelope without the new fields still parses`() {
val older = """
{"fnc":1,"kind":"file","caption":"📎 old.pdf",
"att":{"id":"${"e".repeat(64)}","key":"$key","digest":"$digest",
"bytes":10,"mime":"application/pdf","w":0,"h":0}}
""".trimIndent().replace("\n", "")
val ref = assertNotNull(Content.parse(older).attachment)
assertEquals("", ref.filename)
assertEquals(0, ref.classification)
assertEquals(emptyList(), ref.compartments)
}
/**
* 🔴 The additive property that makes any of this safe: a photo envelope
* from this engine still carries none of the file keys, so a peer that has
* never heard of them reads it exactly as before.
*/
@Test
fun `a photo envelope carries none of the file keys`() {
val photo = Attachment.Ref(
kind = Attachment.IMAGE, id = "f".repeat(64),
key = ByteArray(32) { 0x11 }, digest = ByteArray(32) { 0x22 },
bytes = 1000, mime = "image/jpeg", width = 100, height = 200,
)
val json = Content.attachment(photo, "hi")
assertFalse(json.contains("\"fn\""), "a photo envelope carried file fields")
assertFalse(json.contains("\"cls\""))
assertFalse(json.contains("\"cmp\""))
}
/**
* ⚠️ A filename from a peer is display text and a name, never a path.
* Carried verbatim, and nothing here should ever join it onto a directory.
*/
@Test
fun `a hostile filename survives as plain text`() {
val hostile = """
{"fnc":1,"kind":"file","caption":"x",
"att":{"id":"${"a".repeat(64)}","key":"$key","digest":"$digest",
"bytes":1,"mime":"application/pdf","w":0,"h":0},
"fn":"../../../etc/passwd","cls":0,"cmp":[]}
""".trimIndent().replace("\n", "")
assertEquals("../../../etc/passwd",
assertNotNull(Content.parse(hostile).attachment).filename)
}
@Test
fun `a file in a group keeps its routing`() {
val ref = Attachment.Ref(
kind = Attachment.FILE, id = "a".repeat(64),
key = ByteArray(32) { 0x11 }, digest = ByteArray(32) { 0x22 },
bytes = 1, mime = "application/pdf", filename = "g.pdf",
)
val content = Content.parse(Content.attachment(ref, groupId = "g-123"))
assertEquals("g-123", content.groupId)
assertEquals(Attachment.FILE, content.attachment?.kind)
}
}