@@ -59,19 +59,7 @@ impl JsonPatcher {
5959
6060 for & ( section, section_key) in DEPENDENCY_SECTIONS {
6161 if let Some ( serde_json:: Value :: Object ( deps) ) = parsed. get ( section_key) {
62- // Find the byte position of this section key in the text
63- let Some ( section_key_pos) = find_json_key_position ( text, section_key, 0 ) else {
64- continue ;
65- } ;
66-
67- // Find the opening { of the section's value object
68- let search_from = section_key_pos + section_key. len ( ) + 2 ; // skip past `"key"`
69- let Some ( obj_start) = find_char_skipping_strings ( text, '{' , search_from) else {
70- continue ;
71- } ;
72-
73- // Find the matching closing }
74- let Some ( obj_end) = find_matching_brace ( text, obj_start) else {
62+ let Some ( ( obj_start, obj_end) ) = find_section_bounds ( text, section_key) else {
7563 continue ;
7664 } ;
7765
@@ -128,17 +116,7 @@ impl JsonPatcher {
128116 continue ;
129117 } ;
130118
131- // Find the byte position of this section key in the text
132- let Some ( section_key_pos) = find_json_key_position ( text, section_key, 0 ) else {
133- continue ;
134- } ;
135-
136- let search_from = section_key_pos + section_key. len ( ) + 2 ;
137- let Some ( obj_start) = find_char_skipping_strings ( text, '{' , search_from) else {
138- continue ;
139- } ;
140-
141- let Some ( obj_end) = find_matching_brace ( text, obj_start) else {
119+ let Some ( ( obj_start, obj_end) ) = find_section_bounds ( text, section_key) else {
142120 continue ;
143121 } ;
144122
@@ -198,6 +176,19 @@ impl JsonPatcher {
198176 }
199177}
200178
179+ /// Find the byte range `(obj_start, obj_end)` of a dependency-section object
180+ /// in the raw JSON text.
181+ ///
182+ /// Returns `None` if the section key cannot be located, the opening `{` is
183+ /// missing, or the matching `}` is missing.
184+ fn find_section_bounds ( text : & str , section_key : & str ) -> Option < ( usize , usize ) > {
185+ let section_key_pos = find_json_key_position ( text, section_key, 0 ) ?;
186+ let search_from = section_key_pos + section_key. len ( ) + 2 ; // skip past `"key"`
187+ let obj_start = find_char_skipping_strings ( text, '{' , search_from) ?;
188+ let obj_end = find_matching_brace ( text, obj_start) ?;
189+ Some ( ( obj_start, obj_end) )
190+ }
191+
201192/// Find the byte position of a JSON key string in the text.
202193///
203194/// Searches for `"key"` as a JSON key (followed by `:`), starting from `from`.
@@ -988,4 +979,57 @@ mod tests {
988979 let result = find_char_skipping_strings ( text, ':' , 0 ) ;
989980 assert ! ( result. is_none( ) ) ;
990981 }
982+
983+ #[ test]
984+ fn test_find_section_bounds_key_not_found ( ) {
985+ // Section key doesn't exist in text at all
986+ assert ! ( find_section_bounds( "{}" , "dependencies" ) . is_none( ) ) ;
987+ }
988+
989+ #[ test]
990+ fn test_find_section_bounds_no_brace_after_key ( ) {
991+ // Key exists and is followed by `:`, but no `{` after it (truncated text)
992+ let text = r#"{"dependencies": "# ;
993+ assert ! ( find_section_bounds( text, "dependencies" ) . is_none( ) ) ;
994+ }
995+
996+ #[ test]
997+ fn test_find_section_bounds_no_matching_close_brace ( ) {
998+ // Key exists and `{` found, but no matching `}`
999+ let text = r#"{"dependencies": {"# ;
1000+ assert ! ( find_section_bounds( text, "dependencies" ) . is_none( ) ) ;
1001+ }
1002+
1003+ #[ test]
1004+ fn test_find_section_bounds_valid ( ) {
1005+ let text = r#"{"dependencies": {"react": "^17.0.0"}}"# ;
1006+ let bounds = find_section_bounds ( text, "dependencies" ) ;
1007+ assert ! ( bounds. is_some( ) ) ;
1008+ let ( start, end) = bounds. unwrap ( ) ;
1009+ assert_eq ! ( & text[ start..=end] , r#"{"react": "^17.0.0"}"# ) ;
1010+ }
1011+
1012+ #[ test]
1013+ fn test_scan_version_locations_unicode_escaped_section_key ( ) {
1014+ // serde_json decodes \u0065 → 'e', seeing "dependencies" as the key.
1015+ // But find_section_bounds searches for literal "dependencies" in the
1016+ // raw text and won't find "depend\u0065ncies" — exercises the continue.
1017+ let input = "{\" depend\\ u0065ncies\" : {\" react\" : \" ^17.0.0\" }}" ;
1018+ let locations = JsonPatcher :: scan_version_locations ( input) . unwrap ( ) ;
1019+ assert ! ( locations. is_empty( ) ) ;
1020+ }
1021+
1022+ #[ test]
1023+ fn test_find_char_skipping_whitespace_empty_from_end ( ) {
1024+ // When `from == text.len()`, the slice is empty → None
1025+ let text = "abc" ;
1026+ assert_eq ! ( find_char_skipping_whitespace( text, ':' , text. len( ) ) , None ) ;
1027+ }
1028+
1029+ #[ test]
1030+ fn test_find_next_quote_empty_from_end ( ) {
1031+ // When `from == text.len()`, the slice is empty → None
1032+ let text = "abc" ;
1033+ assert_eq ! ( find_next_quote( text, text. len( ) ) , None ) ;
1034+ }
9911035}
0 commit comments