@@ -42,6 +42,12 @@ bool test_nested_empty_containers(void);
4242bool test_mixed_empty_and_filled (void );
4343bool test_boundary_integers (void );
4444bool 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
4753bool 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
6931024int 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