commit 2df1e931c8d5c6653ecf13a458028a44ce7cee62
parent bf75432888da21d7fee24438684463b483bb442b
Author: triesap <tyson@radroots.org>
Date: Mon, 31 Aug 2026 21:53:52 +0000
test(ios): bind exported persona evidence
- emit canonical attempt JSON without escaped slashes
- parse the exact Xcode repetition and UUID name envelope
- bind exported names back to canonical attempt identities
- cover malformed names and bounded attachment reads
Diffstat:
3 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/RadrootsUITests/RadrootsRemoteQualificationUITests.swift b/RadrootsUITests/RadrootsRemoteQualificationUITests.swift
@@ -1555,7 +1555,7 @@ final class RadrootsRemoteQualificationUITests: XCTestCase {
artifactDigests: []
)
let encoder = JSONEncoder()
- encoder.outputFormatting = [.sortedKeys]
+ encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
let data = try encoder.encode(evidence)
XCTAssertLessThanOrEqual(data.count, 64 * 1024)
let attachment = XCTAttachment(data: data, uniformTypeIdentifier: "public.json")
diff --git a/scripts/local-social-fixture.py b/scripts/local-social-fixture.py
@@ -108,6 +108,10 @@ PERSONA_ATTACHMENT_NAMES = tuple(
for persona in range(1, 6)
for attempt in range(1, 4)
)
+PERSONA_XCRESULT_ATTACHMENT_NAME = re.compile(
+ r"^radroots-local-social-(P0[1-5]-A0[1-3])_0_"
+ r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\.json$"
+)
FORBIDDEN_EVIDENCE_KEYS = frozenset(
("private_key", "secret", "seed", "signed_event", "event_content", "raw_event")
)
@@ -1951,6 +1955,13 @@ def exported_attachment_inventory(path: Path) -> set[str]:
return {relative.decode("utf-8") for _, relative, _ in inventory}
+def xcresult_attachment_attempt_id(name: object) -> str | None:
+ if not isinstance(name, str):
+ return None
+ matched = PERSONA_XCRESULT_ATTACHMENT_NAME.fullmatch(name)
+ return matched.group(1) if matched is not None else None
+
+
def load_exported_persona_attachments(
export_directory: Path,
suite: dict[str, Any],
@@ -2002,9 +2013,13 @@ def load_exported_persona_attachments(
raise ValueError("xcresult attachment row is invalid")
exported = row_value["exportedFileName"]
name = row_value["suggestedHumanReadableName"]
+ attempt_id = xcresult_attachment_attempt_id(name)
+ if attempt_id is None:
+ raise ValueError("xcresult attempt attachment identity is invalid")
+ canonical_name = f"radroots-local-social-{attempt_id}.json"
if (
- name not in PERSONA_ATTACHMENT_NAMES
- or name in attachments_by_name
+ canonical_name not in PERSONA_ATTACHMENT_NAMES
+ or canonical_name in attachments_by_name
or not isinstance(exported, str)
or exported in exported_names
or not 1 <= len(exported.encode("utf-8")) <= 255
@@ -2026,12 +2041,12 @@ def load_exported_persona_attachments(
attempt = validate_persona_attempt_evidence(
value, suite, require_measured_network=require_measured_network
)
- if name != f"radroots-local-social-{attempt['attempt_id']}.json":
+ if attempt_id != attempt["attempt_id"]:
raise ValueError("xcresult attachment name does not bind its attempt")
total_bytes += len(raw)
if total_bytes > MAX_PERSONA_ATTACHMENTS_BYTES:
raise ValueError("xcresult attempt attachments exceed their aggregate bound")
- attachments_by_name[name] = (raw, attempt)
+ attachments_by_name[canonical_name] = (raw, attempt)
if tuple(sorted(attachments_by_name)) != tuple(sorted(PERSONA_ATTACHMENT_NAMES)):
raise ValueError("xcresult persona attachment inventory is incomplete")
if inventory != {"manifest.json", *exported_names}:
diff --git a/scripts/test_local_social_fixture.py b/scripts/test_local_social_fixture.py
@@ -852,15 +852,18 @@ class LocalSocialFixtureTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
rows = []
- for name, (raw, _) in zip(
- fixture.PERSONA_ATTACHMENT_NAMES, attachments, strict=True
+ for index, (name, (raw, _)) in enumerate(
+ zip(fixture.PERSONA_ATTACHMENT_NAMES, attachments, strict=True)
):
exported = "exported-" + name
+ suggested = (
+ f"{name[:-5]}_0_00000000-0000-0000-0000-{index:012X}.json"
+ )
(root / exported).write_bytes(raw)
rows.append(
{
"exportedFileName": exported,
- "suggestedHumanReadableName": name,
+ "suggestedHumanReadableName": suggested,
"isAssociatedWithFailure": False,
"configurationName": "Test Scheme Action",
"deviceName": "iPhone 17 Pro",
@@ -902,11 +905,25 @@ class LocalSocialFixtureTests(unittest.TestCase):
root, suite, require_measured_network=True
)
+ rows[0]["suggestedHumanReadableName"] = (
+ "radroots-local-social-P01-A01.json"
+ )
+ (root / "manifest.json").write_text(
+ json.dumps(manifest), encoding="utf-8"
+ )
+ with self.assertRaises(ValueError):
+ fixture.load_exported_persona_attachments(
+ root, suite, require_measured_network=True
+ )
+
def test_xcresult_attachment_read_rejects_maximum_plus_one(self) -> None:
suite, attachments = self.persona_attempt_attachments()
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
- name = fixture.PERSONA_ATTACHMENT_NAMES[0]
+ name = (
+ "radroots-local-social-P01-A01_0_"
+ "00000000-0000-0000-0000-000000000000.json"
+ )
exported = "oversized.json"
(root / exported).write_bytes(
b"{" + b" " * fixture.MAX_PERSONA_ATTACHMENT_BYTES + b"}"
@@ -921,9 +938,13 @@ class LocalSocialFixtureTests(unittest.TestCase):
"deviceId": "11111111-2222-3333-4444-555555555555",
}
]
- for index, other_name in enumerate(
+ for index, canonical_name in enumerate(
fixture.PERSONA_ATTACHMENT_NAMES[1:], 1
):
+ other_name = (
+ f"{canonical_name[:-5]}_0_"
+ f"00000000-0000-0000-0000-{index:012X}.json"
+ )
other_exported = f"exported-{index}.json"
(root / other_exported).write_bytes(attachments[index][0])
rows.append(