@@ -3,6 +3,7 @@ module builder
33import os
44import strings
55import crypto.sha256
6+ import encoding.base64
67import v.pref
78import v.gen.c as cgen
89import v.util.version
@@ -26,6 +27,11 @@ const c_error_bug_report_truncation_notice = '\n... report truncated before uplo
2627// compatibility fallback. The final upload is separately bounded by
2728// c_error_bug_report_max_body_bytes.
2829const c_error_bug_report_max_env_c_output_bytes = 64 * 1024
30+ // Keep the complete parsed-input manifest below Linux's per-environment-string
31+ // exec limit. If a very large project exceeds this bound, the retry still runs,
32+ // but its fallback report is conservatively suppressed because exact equivalence
33+ // cannot be proved.
34+ const v3_report_max_env_input_digests_bytes = 64 * 1024
2935
3036struct CErrorReportLine {
3137pub :
9096 v_file string // informational base filename of the failing source (no directory)
9197 v_source string // already-bounded source snippet; never a whole file
9298 source_inline bool // true: use v_file/v_source as-is and touch no filesystem path
99+ // Every path/digest pair below describes source bytes V3 actually parsed. The
100+ // stable parser compares these values only with its own trusted parsed-file
101+ // paths and scanner digests; it never opens a path supplied by this report.
102+ input_digests map [string ]string
103+ input_digests_complete bool
93104}
94105
95106@[unsafe ]
@@ -260,7 +271,49 @@ const v3_report_env_prefix = 'V_MACOS_V3_REPORT_'
260271// and the take paths agree on exactly what to set and clear. Every variable carries
261272// CONTENT only — never a filesystem path the receiver would read or a directory it
262273// would delete.
263- const v3_report_env_suffixes = ['PRESENT' , 'KIND' , 'CCOMPILER' , 'COUTPUT' , 'TAG' , 'VFILE' , 'VSOURCE' ]
274+ const v3_report_env_suffixes = ['PRESENT' , 'KIND' , 'CCOMPILER' , 'COUTPUT' , 'TAG' , 'VFILE' , 'VSOURCE' ,
275+ 'INPUT_DIGESTS' , 'INPUT_DIGESTS_COMPLETE' ]
276+
277+ // encode_v3_report_input_digests serializes arbitrary source paths without allowing a
278+ // newline or separator in a filename to corrupt the manifest. Paths are content used
279+ // only for equality checks against files the stable parser already opened itself.
280+ fn encode_v3_report_input_digests (input_digests map [string ]string ) ? string {
281+ mut paths := input_digests.keys ()
282+ paths.sort ()
283+ mut encoded := strings.new_builder (paths.len * 128 )
284+ for path in paths {
285+ digest := input_digests[path]
286+ if digest.len != sha256 .size * 2 {
287+ return none
288+ }
289+ encoded.write_string (base64 .encode_str (path))
290+ encoded.write_u8 (` ` )
291+ encoded.write_string (digest)
292+ encoded.write_u8 (`\n ` )
293+ if encoded.len > v3_ report_max_env_input_digests_bytes {
294+ return none
295+ }
296+ }
297+ return encoded.str ()
298+ }
299+
300+ fn decode_v3_report_input_digests (encoded string ) ? map [string ]string {
301+ mut input_digests := map [string ]string {}
302+ for line in encoded.split_into_lines () {
303+ separator := line.index_u8 (` ` )
304+ if separator < = 0 || separator + 1 > = line.len {
305+ return none
306+ }
307+ encoded_path := line[..separator]
308+ path := base64 .decode_str (encoded_path)
309+ digest := line[separator + 1 ..]
310+ if path == '' || base64 .encode_str (path) != encoded_path || digest.len != sha256 .size * 2 {
311+ return none
312+ }
313+ input_digests[path] = digest
314+ }
315+ return input_digests
316+ }
264317
265318// export_external_v3_report_to_env hands `report` to the next external builder (launched
266319// via os.execvp) as self-contained content. That builder cannot authenticate anything
@@ -290,6 +343,17 @@ pub fn export_external_v3_report_to_env(report ExternalCErrorBugReport) {
290343 os.setenv ('${v3_report_env_prefix }TAG' , report.tag, true )
291344 os.setenv ('${v3_report_env_prefix }VFILE' , report.v_file, true )
292345 os.setenv ('${v3_report_env_prefix }VSOURCE' , report.v_source, true )
346+ if encoded := encode_v3_report_input_digests (report.input_digests) {
347+ os.setenv ('${v3_report_env_prefix }INPUT_DIGESTS' , encoded, true )
348+ os.setenv ('${v3_report_env_prefix }INPUT_DIGESTS_COMPLETE' , if report.input_digests_complete {
349+ '1'
350+ } else {
351+ '0'
352+ }, true )
353+ } else {
354+ os.setenv ('${v3_report_env_prefix }INPUT_DIGESTS' , '' , true )
355+ os.setenv ('${v3_report_env_prefix }INPUT_DIGESTS_COMPLETE' , '0' , true )
356+ }
293357}
294358
295359// bounded_v3_fallback_source extracts the bounded V source snippet to upload for a V3->V1
@@ -384,14 +448,20 @@ fn bounded_v_source_for_generated_c(c_output string, generated_c_file string, al
384448// submission happen relative to the tool's own build outcome (only on success).
385449pub fn take_external_v3_report_from_env () ? ExternalCErrorBugReport {
386450 present := os.getenv ('${v3_report_env_prefix }PRESENT' )
451+ input_digests := decode_v3_report_input_digests (os.getenv ('${v3_report_env_prefix }INPUT_DIGESTS' )) or {
452+ map [string ]string {}
453+ }
387454 report := ExternalCErrorBugReport{
388- kind: os.getenv ('${v3_report_env_prefix }KIND' )
389- ccompiler: os.getenv ('${v3_report_env_prefix }CCOMPILER' )
390- c_output: os.getenv ('${v3_report_env_prefix }COUTPUT' )
391- tag: os.getenv ('${v3_report_env_prefix }TAG' )
392- v_file: os.getenv ('${v3_report_env_prefix }VFILE' )
393- v_source: os.getenv ('${v3_report_env_prefix }VSOURCE' )
394- source_inline: true
455+ kind: os.getenv ('${v3_report_env_prefix }KIND' )
456+ ccompiler: os.getenv ('${v3_report_env_prefix }CCOMPILER' )
457+ c_output: os.getenv ('${v3_report_env_prefix }COUTPUT' )
458+ tag: os.getenv ('${v3_report_env_prefix }TAG' )
459+ v_file: os.getenv ('${v3_report_env_prefix }VFILE' )
460+ v_source: os.getenv ('${v3_report_env_prefix }VSOURCE' )
461+ source_inline: true
462+ input_digests: input_digests
463+ input_digests_complete: os.getenv ('${v3_report_env_prefix }INPUT_DIGESTS_COMPLETE' ) == '1'
464+ && input_digests.len > 0
395465 // c_file and cleanup_dir are intentionally left empty: the builder must not read
396466 // a file or delete a directory named by the environment.
397467 }
@@ -404,6 +474,35 @@ pub fn take_external_v3_report_from_env() ?ExternalCErrorBugReport {
404474 return report
405475}
406476
477+ // matches_v3_fallback_inputs confirms that every source V3 recorded was parsed from the
478+ // same bytes by the stable compiler. Report paths are never opened: they are compared
479+ // only with canonical paths belonging to the stable parser's own AST files.
480+ fn (b &Builder) matches_v3_fallback_inputs (report ExternalCErrorBugReport) bool {
481+ if report.kind == external_v3_ notice_only_kind {
482+ return true
483+ }
484+ if ! report.input_digests_complete || report.input_digests.len == 0 {
485+ return false
486+ }
487+ mut stable_digests := map [string ]string {}
488+ for file in b.parsed_files {
489+ if file.path == '' || file.source_digest == '' {
490+ continue
491+ }
492+ stable_digests[os.real_path (file.path)] = file.source_digest
493+ }
494+ for path, v3_ digest in report.input_digests {
495+ if stable_digests[path] != v3_ digest {
496+ return false
497+ }
498+ }
499+ return true
500+ }
501+
502+ fn discard_unverified_v3_fallback_report () {
503+ eprintln ('note: source inputs changed before the stable compiler retry completed; the V3 fallback report was not submitted.' )
504+ }
505+
407506// submit_external_v3_compiler_error_bug_report reports metadata for a V3 internal
408507// compiler error after the stable compiler has confirmed the program is buildable.
409508// Source may only be supplied through the pre-V3 snapshot/content path, so this legacy
0 commit comments