Skip to content

Commit 70a0089

Browse files
test: close mull-discovered contract gaps across subsystems
Mutation-guided hardening of the io and parser suites, following Conventions/MULL-DISCOVERY-CONVENTIONS.md: add contract-level tests for (A) caller-observable gaps only; strategy/equivalent survivors are left accepted. - Io.Write/File/ArgParse: write/format and arg-parse error + edge contracts. - Elf/MachO/Pe: malformed-input rejection (bad magic, truncation, OOB offsets) and known-fixture field values. - Pdb/Dwarf: multi-function RVA resolution, malformed-superblock rejection; self-contained .debug_info/.debug_line fixtures. Fixes a brittle Dwarf CFI test that failed the unmutated baseline under -O0 (blocking all Dwarf discovery) -- now contract-correct and unblocked. - Json/KvConfig/Http: malformed-input rejection, round-trip, parsed values.
1 parent e907302 commit 70a0089

11 files changed

Lines changed: 2596 additions & 13 deletions

File tree

Tests/Json/Read.EdgeCases.c

Lines changed: 338 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ bool test_nested_empty_containers(void);
4242
bool test_mixed_empty_and_filled(void);
4343
bool test_boundary_integers(void);
4444
bool test_boundary_floats(void);
45+
bool test_scalar_readers_reject_malformed(void);
46+
bool test_scalar_readers_value_and_advance(void);
47+
bool test_truncated_unicode_escape_rejected(void);
48+
bool test_unknown_keys_of_every_type_skipped(void);
49+
bool test_malformed_object_rejected(void);
50+
bool test_negative_number_exact_values(void);
4551

4652
// Test 1: Empty object reading
4753
bool test_empty_object_reading(void) {
@@ -689,6 +695,331 @@ bool test_boundary_floats(void) {
689695
return success;
690696
}
691697

698+
// Test 14: Scalar readers reject malformed tokens (FAILURE contract:
699+
// "returns original StrIter on error" -- the iterator must NOT advance,
700+
// so a caller can detect the failure and fall back).
701+
bool test_scalar_readers_reject_malformed(void) {
702+
WriteFmtLn("Testing scalar readers reject malformed tokens");
703+
704+
DefaultAllocator alloc = DefaultAllocatorInit();
705+
bool success = true;
706+
707+
// JReadBool on a token that starts like a bool but isn't.
708+
{
709+
Str j = StrInitFromZstr("tru3", &alloc);
710+
StrIter si = StrIterFromStr(j);
711+
bool b = true;
712+
StrIter out = JReadBool(si, &b);
713+
if (StrIterIndex(&out) != StrIterIndex(&si)) {
714+
WriteFmtLn("[DEBUG] JReadBool advanced on malformed 'tru3'");
715+
success = false;
716+
}
717+
StrDeinit(&j);
718+
}
719+
{
720+
Str j = StrInitFromZstr("fXlse", &alloc);
721+
StrIter si = StrIterFromStr(j);
722+
bool b = true;
723+
StrIter out = JReadBool(si, &b);
724+
if (StrIterIndex(&out) != StrIterIndex(&si)) {
725+
WriteFmtLn("[DEBUG] JReadBool advanced on malformed 'fXlse'");
726+
success = false;
727+
}
728+
StrDeinit(&j);
729+
}
730+
// Input too short to spell a bool -- must fail (the >= length guard).
731+
{
732+
Str j = StrInitFromZstr("tr", &alloc);
733+
StrIter si = StrIterFromStr(j);
734+
bool b = true;
735+
StrIter out = JReadBool(si, &b);
736+
if (StrIterIndex(&out) != StrIterIndex(&si)) {
737+
WriteFmtLn("[DEBUG] JReadBool advanced on too-short 'tr'");
738+
success = false;
739+
}
740+
StrDeinit(&j);
741+
}
742+
// JReadNull on a 'n...' token that isn't "null".
743+
{
744+
Str j = StrInitFromZstr("nuXX", &alloc);
745+
StrIter si = StrIterFromStr(j);
746+
bool is_null = true;
747+
StrIter out = JReadNull(si, &is_null);
748+
if (StrIterIndex(&out) != StrIterIndex(&si)) {
749+
WriteFmtLn("[DEBUG] JReadNull advanced on malformed 'nuXX'");
750+
success = false;
751+
}
752+
StrDeinit(&j);
753+
}
754+
755+
return success;
756+
}
757+
758+
// Test 15: Scalar readers parse valid tokens to the right value AND
759+
// consume exactly the token (SUCCESS contract: advance past the token).
760+
bool test_scalar_readers_value_and_advance(void) {
761+
WriteFmtLn("Testing scalar readers parse + advance");
762+
763+
DefaultAllocator alloc = DefaultAllocatorInit();
764+
bool success = true;
765+
766+
{
767+
Str j = StrInitFromZstr("true rest", &alloc);
768+
StrIter si = StrIterFromStr(j);
769+
bool b = false;
770+
StrIter out = JReadBool(si, &b);
771+
// value correct and exactly 4 bytes consumed
772+
if (!(b == true && StrIterIndex(&out) == 4)) {
773+
WriteFmtLn("[DEBUG] JReadBool 'true' value/advance wrong: b={}, idx={}", b, StrIterIndex(&out));
774+
success = false;
775+
}
776+
StrDeinit(&j);
777+
}
778+
{
779+
Str j = StrInitFromZstr("false rest", &alloc);
780+
StrIter si = StrIterFromStr(j);
781+
bool b = true;
782+
StrIter out = JReadBool(si, &b);
783+
if (!(b == false && StrIterIndex(&out) == 5)) {
784+
WriteFmtLn("[DEBUG] JReadBool 'false' value/advance wrong: b={}, idx={}", b, StrIterIndex(&out));
785+
success = false;
786+
}
787+
StrDeinit(&j);
788+
}
789+
{
790+
Str j = StrInitFromZstr("null rest", &alloc);
791+
StrIter si = StrIterFromStr(j);
792+
bool is_null = false;
793+
StrIter out = JReadNull(si, &is_null);
794+
if (!(is_null == true && StrIterIndex(&out) == 4)) {
795+
WriteFmtLn("[DEBUG] JReadNull 'null' value/advance wrong: n={}, idx={}", is_null, StrIterIndex(&out));
796+
success = false;
797+
}
798+
StrDeinit(&j);
799+
}
800+
801+
// Exact-length tokens (no trailing bytes): the readers must still
802+
// accept these. A token that exactly fills the remaining input is the
803+
// boundary case for the minimum-length guard.
804+
{
805+
Str j = StrInitFromZstr("true", &alloc);
806+
StrIter si = StrIterFromStr(j);
807+
bool b = false;
808+
StrIter out = JReadBool(si, &b);
809+
if (!(b == true && StrIterIndex(&out) == 4)) {
810+
WriteFmtLn("[DEBUG] JReadBool exact 'true' wrong: b={}, idx={}", b, StrIterIndex(&out));
811+
success = false;
812+
}
813+
StrDeinit(&j);
814+
}
815+
{
816+
Str j = StrInitFromZstr("false", &alloc);
817+
StrIter si = StrIterFromStr(j);
818+
bool b = true;
819+
StrIter out = JReadBool(si, &b);
820+
if (!(b == false && StrIterIndex(&out) == 5)) {
821+
WriteFmtLn("[DEBUG] JReadBool exact 'false' wrong: b={}, idx={}", b, StrIterIndex(&out));
822+
success = false;
823+
}
824+
StrDeinit(&j);
825+
}
826+
{
827+
Str j = StrInitFromZstr("null", &alloc);
828+
StrIter si = StrIterFromStr(j);
829+
bool is_null = false;
830+
StrIter out = JReadNull(si, &is_null);
831+
if (!(is_null == true && StrIterIndex(&out) == 4)) {
832+
WriteFmtLn("[DEBUG] JReadNull exact 'null' wrong: n={}, idx={}", is_null, StrIterIndex(&out));
833+
success = false;
834+
}
835+
StrDeinit(&j);
836+
}
837+
838+
return success;
839+
}
840+
841+
// Test 16: A truncated \uXXXX escape must be rejected, not over-read.
842+
// (Reader FAILURE contract: returns the original iterator.)
843+
bool test_truncated_unicode_escape_rejected(void) {
844+
WriteFmtLn("Testing truncated \\u escape rejection");
845+
846+
DefaultAllocator alloc = DefaultAllocatorInit();
847+
bool success = true;
848+
849+
// "\u" with no following hex digits, then closing quote/EOF.
850+
Zstr cases[] = {"\"\\u\"", "\"\\u12\"", "\"ab\\u\""};
851+
for (u64 i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
852+
Str j = StrInitFromZstr(cases[i], &alloc);
853+
StrIter si = StrIterFromStr(j);
854+
Str out = StrInit(&alloc);
855+
StrIter r = JReadString(si, &out);
856+
// Truncated escape -> failure -> iterator unchanged.
857+
if (StrIterIndex(&r) != StrIterIndex(&si)) {
858+
WriteFmtLn("[DEBUG] JReadString advanced on truncated escape case {}", i);
859+
success = false;
860+
}
861+
StrDeinit(&out);
862+
StrDeinit(&j);
863+
}
864+
865+
// A complete \uXXXX escape is the minimal valid case: the string must
866+
// parse (iterator advances past the closing quote) even though the
867+
// escape itself is skipped rather than decoded.
868+
{
869+
Str j = StrInitFromZstr("\"a\\u00e9b\"", &alloc);
870+
StrIter si = StrIterFromStr(j);
871+
Str out = StrInit(&alloc);
872+
StrIter r = JReadString(si, &out);
873+
if (StrIterIndex(&r) == StrIterIndex(&si) || StrIterIndex(&r) != StrIterLength(&r)) {
874+
WriteFmtLn("[DEBUG] JReadString rejected a valid \\u escape: idx={}", StrIterIndex(&r));
875+
success = false;
876+
}
877+
StrDeinit(&out);
878+
StrDeinit(&j);
879+
}
880+
881+
DefaultAllocatorDeinit(&alloc);
882+
return success;
883+
}
884+
885+
// Test 17: Unknown keys (every value shape) are skipped, and a later
886+
// recognized key still parses correctly. This exercises the JSkipValue /
887+
// JSkipObject / JSkipArray dispatch the reader relies on.
888+
bool test_unknown_keys_of_every_type_skipped(void) {
889+
WriteFmtLn("Testing unknown keys of every value type are skipped");
890+
891+
DefaultAllocator alloc = DefaultAllocatorInit();
892+
bool success = true;
893+
894+
Str json = StrInitFromZstr(
895+
"{"
896+
"\"u_str\":\"ignore\","
897+
"\"u_int\":-42,"
898+
"\"u_zero\":0,"
899+
"\"u_flt\":3.5,"
900+
"\"u_bool\":true,"
901+
"\"u_null\":null,"
902+
"\"u_obj\":{\"a\":1,\"b\":[2,3]},"
903+
"\"u_arr\":[1,{\"x\":9},\"s\"],"
904+
"\"wanted\":7"
905+
"}",
906+
&alloc
907+
);
908+
StrIter si = StrIterFromStr(json);
909+
i64 wanted = 0;
910+
911+
JR_OBJ(si, { JR_INT_KV(si, "wanted", wanted); });
912+
913+
// The recognized value past all the skipped ones must come through,
914+
// and the iterator must finish at the closing brace (whole object
915+
// consumed).
916+
if (wanted != 7) {
917+
WriteFmtLn("[DEBUG] wanted parsed wrong after skips: {}", wanted);
918+
success = false;
919+
}
920+
if (StrIterIndex(&si) != StrIterLength(&si)) {
921+
WriteFmtLn(
922+
"[DEBUG] iterator did not consume whole object: idx={}, len={}",
923+
StrIterIndex(&si),
924+
StrIterLength(&si)
925+
);
926+
success = false;
927+
}
928+
929+
StrDeinit(&json);
930+
DefaultAllocatorDeinit(&alloc);
931+
return success;
932+
}
933+
934+
// Test 18: Structurally broken objects are rejected -- the reader rewinds
935+
// the iterator to the start (FAILURE contract) instead of silently
936+
// accepting a partial parse.
937+
bool test_malformed_object_rejected(void) {
938+
WriteFmtLn("Testing malformed objects are rejected (iterator rewinds)");
939+
940+
DefaultAllocator alloc = DefaultAllocatorInit();
941+
bool success = true;
942+
943+
// missing ':' separator, missing closing brace, missing comma.
944+
Zstr cases[] = {
945+
"{\"a\" 1}", // no colon
946+
"{\"a\":1", // truncated, no closing brace
947+
"{\"a\":1 \"b\":2}" // missing comma between pairs
948+
};
949+
950+
for (u64 i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
951+
Str json = StrInitFromZstr(cases[i], &alloc);
952+
StrIter si = StrIterFromStr(json);
953+
i64 a = 0;
954+
i64 b = 0;
955+
956+
JR_OBJ(si, {
957+
JR_INT_KV(si, "a", a);
958+
JR_INT_KV(si, "b", b);
959+
});
960+
(void)a;
961+
(void)b;
962+
963+
// On structural failure the macro restores si to its start.
964+
if (StrIterIndex(&si) != 0) {
965+
WriteFmtLn("[DEBUG] malformed object case {} did not rewind: idx={}", i, StrIterIndex(&si));
966+
success = false;
967+
}
968+
969+
StrDeinit(&json);
970+
}
971+
972+
DefaultAllocatorDeinit(&alloc);
973+
return success;
974+
}
975+
976+
// Test 19: Negative numbers round-trip to their exact value. This pins
977+
// the sign handling in JReadNumber (the negate step) as caller-observable.
978+
bool test_negative_number_exact_values(void) {
979+
WriteFmtLn("Testing negative number exact values");
980+
981+
DefaultAllocator alloc = DefaultAllocatorInit();
982+
bool success = true;
983+
984+
{
985+
Str j = StrInitFromZstr("-12345", &alloc);
986+
StrIter si = StrIterFromStr(j);
987+
i64 v = 0;
988+
StrIter out = JReadInteger(si, &v);
989+
if (!(StrIterIndex(&out) != StrIterIndex(&si) && v == -12345)) {
990+
WriteFmtLn("[DEBUG] JReadInteger '-12345' -> {}", v);
991+
success = false;
992+
}
993+
StrDeinit(&j);
994+
}
995+
{
996+
// Positive control: a sign-flip mutation would make this negative.
997+
Str j = StrInitFromZstr("12345", &alloc);
998+
StrIter si = StrIterFromStr(j);
999+
i64 v = 0;
1000+
StrIter out = JReadInteger(si, &v);
1001+
if (!(StrIterIndex(&out) != StrIterIndex(&si) && v == 12345)) {
1002+
WriteFmtLn("[DEBUG] JReadInteger '12345' -> {}", v);
1003+
success = false;
1004+
}
1005+
StrDeinit(&j);
1006+
}
1007+
{
1008+
Str j = StrInitFromZstr("-2.5", &alloc);
1009+
StrIter si = StrIterFromStr(j);
1010+
f64 v = 0.0;
1011+
StrIter out = JReadFloat(si, &v);
1012+
if (!(StrIterIndex(&out) != StrIterIndex(&si) && v == -2.5)) {
1013+
WriteFmtLn("[DEBUG] JReadFloat '-2.5' -> {}", v);
1014+
success = false;
1015+
}
1016+
StrDeinit(&j);
1017+
}
1018+
1019+
DefaultAllocatorDeinit(&alloc);
1020+
return success;
1021+
}
1022+
6921023
// Main function that runs all edge case reading tests
6931024
int main(void) {
6941025
// Array of test functions
@@ -705,7 +1036,13 @@ int main(void) {
7051036
test_nested_empty_containers,
7061037
test_mixed_empty_and_filled,
7071038
test_boundary_integers,
708-
test_boundary_floats
1039+
test_boundary_floats,
1040+
test_scalar_readers_reject_malformed,
1041+
test_scalar_readers_value_and_advance,
1042+
test_truncated_unicode_escape_rejected,
1043+
test_unknown_keys_of_every_type_skipped,
1044+
test_malformed_object_rejected,
1045+
test_negative_number_exact_values
7091046
};
7101047

7111048
int test_count = sizeof(tests) / sizeof(tests[0]);

0 commit comments

Comments
 (0)