// Acceptance for P3.3 — the platform-agnostic ("common") artifact // validation pass (subplan 05 "Common Validation" subset). // // Runs validate_artifact_file against the on-disk fixtures under // examples/fixtures/ and asserts: // 1. a GOOD artifact (correct size + sha256 + content type + extension // matching the platform) -> status .valid, reason .ok — for BOTH the // .apk/android_apk and .ipa/ios fixtures (cross-checks the ext map). // 2. each tampered case -> status .invalid with the SPECIFIC reason: // - wrong declared size -> .size_mismatch // - wrong declared sha256 -> .digest_mismatch // - a .apk file declared as `ios` -> .extension_mismatch // - a disallowed content type -> .content_type_denied // - a path with no file -> .missing_file // // The expected sha256 values are the real digests of the fixtures, // cross-checked against `shasum -a 256` and recomputed here by std.hash // inside validate_artifact_file. Prints PASS/FAIL per case; exits non-zero // if any case behaves unexpectedly so the runner counts it as a failure. #import "modules/std.sx"; #import "../src/domain/platform.sx"; #import "../src/domain/artifact.sx"; #import "../src/validation/artifact_file.sx"; APK_PATH :: "examples/fixtures/acme-1.2.3-android.apk"; IPA_PATH :: "examples/fixtures/acme-1.2.3-ios.ipa"; APK_SHA :: "55a008aa634d45313ef0a758624e0d2a356c156e507f28a2c60d19d38893af09"; IPA_SHA :: "1581f47098fd83277fa07c3f234ec92d9ce456804738278ede4e76f7f7bced84"; APK_SIZE :: 5; IPA_SIZE :: 5; APK_CT :: "application/vnd.android.package-archive"; IPA_CT :: "application/octet-stream"; ZERO_SHA :: "0000000000000000000000000000000000000000000000000000000000000000"; passed :: (o: ValidationOutcome) -> bool { return o.status == .valid and o.reason == .ok; } failed_with :: (o: ValidationOutcome, r: ValidationReason) -> bool { return o.status == .invalid and o.reason == r; } // ── good fixtures -> .valid / .ok ─────────────────────────────────────── check_good_apk :: () -> bool { o := validate_artifact_file(APK_PATH, APK_SIZE, APK_SHA, .android_apk, APK_CT); return passed(o); } check_good_ipa :: () -> bool { o := validate_artifact_file(IPA_PATH, IPA_SIZE, IPA_SHA, .ios, IPA_CT); return passed(o); } // ── tampered: wrong declared size -> .size_mismatch ───────────────────── check_size_mismatch :: () -> bool { o := validate_artifact_file(APK_PATH, 999, APK_SHA, .android_apk, APK_CT); return failed_with(o, .size_mismatch); } // ── tampered: wrong declared sha256 -> .digest_mismatch ───────────────── check_digest_mismatch :: () -> bool { o := validate_artifact_file(APK_PATH, APK_SIZE, ZERO_SHA, .android_apk, APK_CT); return failed_with(o, .digest_mismatch); } // ── tampered: .apk declared as `ios` -> .extension_mismatch ───────────── // Size, digest, and content type all pass; only the extension/platform // policy rejects it. check_extension_mismatch :: () -> bool { o := validate_artifact_file(APK_PATH, APK_SIZE, APK_SHA, .ios, APK_CT); return failed_with(o, .extension_mismatch); } // ── tampered: disallowed content type -> .content_type_denied ─────────── check_content_type_denied :: () -> bool { o := validate_artifact_file(APK_PATH, APK_SIZE, APK_SHA, .android_apk, "text/plain"); return failed_with(o, .content_type_denied); } // ── tampered: no file at path -> .missing_file ────────────────────────── check_missing_file :: () -> bool { o := validate_artifact_file("examples/fixtures/does-not-exist.apk", APK_SIZE, APK_SHA, .android_apk, APK_CT); return failed_with(o, .missing_file); } run_case :: (label: string, ok: bool) -> s32 { if ok { print(" PASS {}\n", label); return 0; } print(" FAIL {}\n", label); return 1; } main :: () -> s32 { failures : s32 = 0; failures += run_case("good .apk/android_apk -> valid/ok", check_good_apk()); failures += run_case("good .ipa/ios -> valid/ok", check_good_ipa()); failures += run_case("wrong size -> size_mismatch", check_size_mismatch()); failures += run_case("wrong sha256 -> digest_mismatch", check_digest_mismatch()); failures += run_case(".apk declared as ios -> extension_mismatch", check_extension_mismatch()); failures += run_case("disallowed content type -> content_type_denied", check_content_type_denied()); failures += run_case("missing file -> missing_file", check_missing_file()); print("------------------------------------------------\n"); if failures == 0 { print("validate_artifact_file: ALL CASES PASS\n"); return 0; } print("validate_artifact_file: {} CASE(S) FAILED\n", failures); return 1; }