@@ -91,86 +91,6 @@ std::string TrimAsciiWhitespace(const std::string& text) {
9191 return text.substr (start, end - start);
9292}
9393
94- bool ExtractJsonStringField (const std::string& json_text,
95- const std::string& field,
96- std::string* value,
97- std::string* err) {
98- const std::string key = " \" " + field + " \" " ;
99- const std::size_t key_pos = json_text.find (key);
100- if (key_pos == std::string::npos) {
101- if (err != nullptr ) {
102- *err = " missing field: " + field;
103- }
104- return false ;
105- }
106-
107- const std::size_t colon_pos = json_text.find (' :' , key_pos + key.size ());
108- if (colon_pos == std::string::npos) {
109- if (err != nullptr ) {
110- *err = " invalid field separator: " + field;
111- }
112- return false ;
113- }
114-
115- std::size_t quote_pos = json_text.find (' "' , colon_pos + 1 );
116- if (quote_pos == std::string::npos) {
117- if (err != nullptr ) {
118- *err = " missing string value: " + field;
119- }
120- return false ;
121- }
122-
123- std::string parsed;
124- bool escape = false ;
125- for (std::size_t i = quote_pos + 1 ; i < json_text.size (); ++i) {
126- const char ch = json_text[i];
127- if (escape) {
128- switch (ch) {
129- case ' "' :
130- case ' \\ ' :
131- case ' /' :
132- parsed.push_back (ch);
133- break ;
134- case ' b' :
135- parsed.push_back (' \b ' );
136- break ;
137- case ' f' :
138- parsed.push_back (' \f ' );
139- break ;
140- case ' n' :
141- parsed.push_back (' \n ' );
142- break ;
143- case ' r' :
144- parsed.push_back (' \r ' );
145- break ;
146- case ' t' :
147- parsed.push_back (' \t ' );
148- break ;
149- default :
150- parsed.push_back (ch);
151- break ;
152- }
153- escape = false ;
154- continue ;
155- }
156-
157- if (ch == ' \\ ' ) {
158- escape = true ;
159- continue ;
160- }
161- if (ch == ' "' ) {
162- *value = parsed;
163- return true ;
164- }
165- parsed.push_back (ch);
166- }
167-
168- if (err != nullptr ) {
169- *err = " unterminated string field: " + field;
170- }
171- return false ;
172- }
173-
17494
17595std::string BuildBudgetItemJson (bool has_declared,
17696 double declared,
@@ -604,35 +524,107 @@ bool ParseBrowserAuditHandoffJson(const std::string& json_text,
604524 }
605525 return false ;
606526 }
607- if (parsed.raw_json .front () != ' {' || parsed.raw_json .back () != ' }' ) {
527+
528+ JsonParseError parse_err;
529+ const auto root = ParseJson (parsed.raw_json , &parse_err);
530+ if (!root.has_value ()) {
531+ if (err != nullptr ) {
532+ *err = " handoff json parse failed at offset " + std::to_string (parse_err.offset ) +
533+ " : " + parse_err.message ;
534+ }
535+ return false ;
536+ }
537+ if (root->type != JsonType::kObject ) {
608538 if (err != nullptr ) {
609539 *err = " handoff json must be an object" ;
610540 }
611541 return false ;
612542 }
613543
614- if (!ExtractJsonStringField (parsed.raw_json , " audit_profile" , &parsed.audit_profile , err) ||
615- !ExtractJsonStringField (parsed.raw_json , " audit_mode" , &parsed.audit_mode , err) ||
616- !ExtractJsonStringField (parsed.raw_json , " next_action" , &parsed.next_action , err) ||
617- !ExtractJsonStringField (parsed.raw_json , " bundle_id" , &parsed.bundle_id , err) ||
618- !ExtractJsonStringField (parsed.raw_json , " tool_version" , &parsed.tool_version , err)) {
544+ const auto require_string_field = [&](std::string_view key, std::string* out) -> bool {
545+ const auto * value = root->Find (key);
546+ if (value == nullptr ) {
547+ if (err != nullptr ) {
548+ *err = " missing field: " + std::string (key);
549+ }
550+ return false ;
551+ }
552+ if (value->type != JsonType::kString ) {
553+ if (err != nullptr ) {
554+ *err = " field must be string: " + std::string (key);
555+ }
556+ return false ;
557+ }
558+ *out = value->string_value ;
559+ return true ;
560+ };
561+
562+ const auto require_bool_field = [&](std::string_view key, bool * out) -> bool {
563+ const auto * value = root->Find (key);
564+ if (value == nullptr ) {
565+ if (err != nullptr ) {
566+ *err = " missing field: " + std::string (key);
567+ }
568+ return false ;
569+ }
570+ if (value->type != JsonType::kBool ) {
571+ if (err != nullptr ) {
572+ *err = " field must be bool: " + std::string (key);
573+ }
574+ return false ;
575+ }
576+ *out = value->bool_value ;
577+ return true ;
578+ };
579+
580+ if (!require_string_field (" schema_version" , &parsed.schema_version ) ||
581+ !require_string_field (" audit_profile" , &parsed.audit_profile ) ||
582+ !require_string_field (" audit_mode" , &parsed.audit_mode ) ||
583+ !require_string_field (" policy_mode" , &parsed.policy_mode ) ||
584+ !require_string_field (" next_action" , &parsed.next_action ) ||
585+ !require_string_field (" bundle_id" , &parsed.bundle_id ) ||
586+ !require_string_field (" tool_version" , &parsed.tool_version ) ||
587+ !require_string_field (" final_verdict" , &parsed.final_verdict ) ||
588+ !require_bool_field (" release_ready" , &parsed.release_ready )) {
589+ return false ;
590+ }
591+
592+ const auto * issues = root->Find (" issues" );
593+ if (issues == nullptr ) {
594+ if (err != nullptr ) {
595+ *err = " missing field: issues" ;
596+ }
619597 return false ;
620598 }
621- if (parsed. raw_json . find ( " \" bundle_verdict \" " ) != std::string::npos ) {
622- if (! ExtractJsonStringField (parsed. raw_json , " bundle_verdict " , &parsed. verdict , err) ) {
623- return false ;
599+ if (issues-> type != JsonType:: kArray ) {
600+ if (err != nullptr ) {
601+ *err = " field must be array: issues " ;
624602 }
625- } else if (!ExtractJsonStringField (parsed.raw_json , " verdict" , &parsed.verdict , err)) {
626603 return false ;
627604 }
628- if (parsed.raw_json .find (" \" policy_mode\" " ) != std::string::npos) {
629- std::string policy_mode;
630- if (!ExtractJsonStringField (parsed.raw_json , " policy_mode" , &policy_mode, err)) {
631- return false ;
605+
606+ const auto * bundle_verdict = root->Find (" bundle_verdict" );
607+ if (bundle_verdict != nullptr && bundle_verdict->type == JsonType::kString &&
608+ !bundle_verdict->string_value .empty ()) {
609+ parsed.verdict = bundle_verdict->string_value ;
610+ } else {
611+ const auto * verdict = root->Find (" verdict" );
612+ if (verdict != nullptr && verdict->type == JsonType::kString &&
613+ !verdict->string_value .empty ()) {
614+ parsed.verdict = verdict->string_value ;
615+ } else {
616+ parsed.verdict = parsed.final_verdict ;
632617 }
633- parsed.policy_mode = ResolvePolicyMode (policy_mode);
634618 }
635619
620+ parsed.policy_mode = ResolvePolicyMode (parsed.policy_mode );
621+
622+ if (parsed.schema_version != kBrowserToCliHandoffSchemaVersion ) {
623+ if (err != nullptr ) {
624+ *err = " unsupported handoff schema_version: " + parsed.schema_version ;
625+ }
626+ return false ;
627+ }
636628 if (parsed.audit_profile != kAuditProfileSpz ) {
637629 if (err != nullptr ) {
638630 *err = " unsupported handoff audit_profile: " + parsed.audit_profile ;
@@ -651,6 +643,19 @@ bool ParseBrowserAuditHandoffJson(const std::string& json_text,
651643 }
652644 return false ;
653645 }
646+ if (!IsSupportedAuditVerdict (parsed.final_verdict )) {
647+ if (err != nullptr ) {
648+ *err = " unsupported handoff final_verdict: " + parsed.final_verdict ;
649+ }
650+ return false ;
651+ }
652+ const bool expected_release_ready = IsReleaseReadyVerdict (parsed.final_verdict );
653+ if (parsed.release_ready != expected_release_ready) {
654+ if (err != nullptr ) {
655+ *err = " handoff release_ready must match final_verdict" ;
656+ }
657+ return false ;
658+ }
654659 if (parsed.bundle_id .empty ()) {
655660 if (err != nullptr ) {
656661 *err = " handoff bundle_id is empty" ;
0 commit comments