P3.3: common artifact validation pass via std.hash
Add the platform-agnostic on-disk validation subset (subplan 05). Pure sx
consuming std.hash + the P2.1/P3.2 domain; no sx-repo changes.
validate_artifact_file(path, expected_size, expected_sha256, platform,
content_type) -> ValidationOutcome { status, reason } runs, in order:
file exists, on-disk size == expected_size, SHA-256 (recomputed via
std.hash) == expected_sha256, content_type on the allow-list, and the
extension matches the platform policy (.apk->android_apk, .ipa->ios,
.dmg->macos, .appimage->linux, .exe->windows). First failing check sets a
specific ValidationReason; all pass -> ValidationStatus.valid/.ok (reusing
the P2.1 enum). Deep IPA/APK zip inspection deferred.
tests/validate_artifact_file.sx exercises both good fixtures and every
tampered class (wrong size, digest mismatch, .apk-as-ios, disallowed
content type, missing file), asserting the exact reason for each.
This commit is contained in:
109
tests/validate_artifact_file.sx
Normal file
109
tests/validate_artifact_file.sx
Normal file
@@ -0,0 +1,109 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user