From f887b8c20627d0649e872880319ece7c96b65d8a Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Wed, 18 Mar 2026 14:11:09 -0700 Subject: [PATCH 1/5] Add csv output option to crucible-verify-raw Added an option to crucible-verify-raw that will produce csv output instead of a formatted print. This will allow easier post processing of the output and ease comparing output from different extent files. --- verify-raw/src/main.rs | 242 +++++++++++++++++++++++++---------------- 1 file changed, 149 insertions(+), 93 deletions(-) diff --git a/verify-raw/src/main.rs b/verify-raw/src/main.rs index 1c5f000f2..281d34488 100644 --- a/verify-raw/src/main.rs +++ b/verify-raw/src/main.rs @@ -17,6 +17,11 @@ struct Args { #[clap(long, short)] verbose: bool, + /// Output CSV to stdout with one row per block; metadata is + /// written as #-prefixed comment lines + #[clap(long, conflicts_with = "verbose")] + verbose_csv: bool, + /// Block size in the extent (usually autodetected) #[clap(long)] block_size: Option, @@ -24,13 +29,14 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); - check_one(&args.file, args.block_size, args.verbose) + check_one(&args.file, args.block_size, args.verbose, args.verbose_csv) } fn check_one( p: &std::path::Path, block_size: Option, verbose: bool, + verbose_csv: bool, ) -> Result<()> { let data = std::fs::read(p)?; let data_len = data.len() - BLOCK_META_SIZE_BYTES as usize; @@ -65,16 +71,39 @@ fn check_one( ); } })?; - print!("bs:{block_size} bytes bc:{block_count:>6}"); - println!( - " dirty:{:>5} gen:{} flush_number:{:>6} ext_ver:{} bonus_sync:{} defrag:{}", - meta.dirty, - meta.gen_number, - meta.flush_number, - meta.ext_version, - meta.bonus_sync_count, - meta.defrag_count, - ); + + if verbose_csv { + println!( + "# file:{} bs:{block_size} bytes bc:{block_count} \ + dirty:{} gen:{} flush_number:{} ext_ver:{} \ + bonus_sync:{} defrag:{}", + p.display(), + meta.dirty, + meta.gen_number, + meta.flush_number, + meta.ext_version, + meta.bonus_sync_count, + meta.defrag_count, + ); + println!( + "file,block,status,\ + slot_a_result,slot_a_selected,slot_a_flush_id,\ + slot_b_result,slot_b_selected,slot_b_flush_id,\ + all_zeros" + ); + } else { + print!("bs:{block_size} bytes bc:{block_count:>6}"); + println!( + " dirty:{:>5} gen:{} flush_number:{:>6} ext_ver:{} \ + bonus_sync:{} defrag:{}", + meta.dirty, + meta.gen_number, + meta.flush_number, + meta.ext_version, + meta.bonus_sync_count, + meta.defrag_count, + ); + } let slot_selected = if !meta.dirty { let mut selected = vec![]; @@ -99,122 +128,149 @@ fn check_one( .collect::>>(); let (ctx_a, ctx_b) = context_slots.split_at(block_count); + let filename = p.display().to_string(); let mut failed = false; + // Check each block and emit one output line per block for (i, chunk) in data[..block_size * block_count] .chunks_exact(block_size) .enumerate() { let hash = integrity_hash(&[chunk]); - let mut printed = false; let ra = check_block(chunk, hash, ctx_a[i]); let rb = check_block(chunk, hash, ctx_b[i]); + let all_zeros = chunk.iter().all(|b| *b == 0u8); + + if verbose_csv { + let sel_a = slot_selected.as_ref().map(|s| s[i]); + let sel_b = slot_selected.as_ref().map(|s| !s[i]); + let status = match (&slot_selected, ra, rb) { + (Some(s), ra, _) if s[i] && ra.is_err() => "error", + (Some(s), _, rb) if !s[i] && rb.is_err() => "error", + (None, Err(_), Err(_)) => "error", + _ => "ok", + }; + if status == "error" { + failed = true; + } + println!( + "{filename},{i},{status},{},{},{},{},{},{},{all_zeros}", + ra.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), + sel_a.map_or("unknown".to_owned(), |s| s.to_string()), + ctx_a[i].map_or(String::new(), |c| c.flush_id.to_string()), + rb.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), + sel_b.map_or("unknown".to_owned(), |s| s.to_string()), + ctx_b[i].map_or(String::new(), |c| c.flush_id.to_string()), + ); + } else { + let mut printed = false; - if let Some(slot_selected) = &slot_selected { - // If the slot selected array is valid (i.e. the extent file is not - // dirty), then it must be correct. - let s = slot_selected[i]; - let ctx = if s { ctx_a[i] } else { ctx_b[i] }; - let r = if s { ra } else { rb }; - if r.is_err() { + if let Some(slot_selected) = &slot_selected { + // If the slot selected array is valid (i.e. the extent file + // is not dirty), then it must be correct. + let s = slot_selected[i]; + let ctx = if s { ctx_a[i] } else { ctx_b[i] }; + let r = if s { ra } else { rb }; + if r.is_err() { + failed = true; + printed = true; + print!("Error at block {:>6}:", i); + print!( + " slot {} [selected]: {r:?}{}", + if s { "A" } else { "B" }, + if let Some(ctx) = ctx { + format!(", flush id: {}", ctx.flush_id) + } else { + "".to_owned() + } + ); + let other_ctx = if s { ctx_b[i] } else { ctx_a[i] }; + let other_r = if s { rb } else { ra }; + print!( + " | slot {} [deselected]: {:?}{}", + if s { "B" } else { "A" }, + other_r, + if let Some(ctx) = other_ctx { + format!(", flush id: {}", ctx.flush_id) + } else { + "".to_owned() + } + ); + if all_zeros { + print!(" Block is all zeros"); + } + println!(); + } + } else if let Err(ea) = ra + && let Err(eb) = rb + { + // Otherwise, both context slots are invalid, so print that failed = true; printed = true; - print!("Error at block {:>6}:", i); + print!("Error at block {i}:"); print!( - " slot {} [selected]: {r:?}{}", - if s { "A" } else { "B" }, - if let Some(ctx) = ctx { + " slot A: {ea:?}{}", + if let Some(ctx) = ctx_a[i] { format!(", flush id: {}", ctx.flush_id) } else { "".to_owned() } ); - let other_ctx = if s { ctx_b[i] } else { ctx_a[i] }; - let other_r = if s { rb } else { ra }; print!( - " | slot {} [deselected]: {:?}{}", - if s { "B" } else { "A" }, - other_r, - if let Some(ctx) = other_ctx { + " | slot B: {eb:?}{}", + if let Some(ctx) = ctx_b[i] { format!(", flush id: {}", ctx.flush_id) } else { "".to_owned() } ); - if chunk.iter().all(|b| *b == 0u8) { + if all_zeros { print!(" Block is all zeros"); } println!(); } - } else if let Err(ea) = ra - && let Err(eb) = rb - { - // Otherwise, both context slots are invalid, so print that - failed = true; - printed = true; - print!("Error at block {i}:"); - print!( - " slot A: {ea:?}{}", - if let Some(ctx) = ctx_a[i] { - format!(", flush id: {}", ctx.flush_id) - } else { - "".to_owned() - } - ); - print!( - " | slot B: {eb:?}{}", - if let Some(ctx) = ctx_b[i] { - format!(", flush id: {}", ctx.flush_id) - } else { - "".to_owned() - } - ); - if chunk.iter().all(|b| *b == 0u8) { - print!(" Block is all zeros"); - } - println!(); - } - // Print a log line for each block if the verbose flag is set - if verbose && !printed { - print!("Success at block {i}:"); - print!( - " slot A{}: {ra:?}{}", - if let Some(slot_selected) = &slot_selected { - if slot_selected[i] { - " [selected]" + // Print a log line for each block if the verbose flag is set + if verbose && !printed { + print!("Success at block {i}:"); + print!( + " slot A{}: {ra:?}{}", + if let Some(slot_selected) = &slot_selected { + if slot_selected[i] { + " [selected]" + } else { + " [deselected]" + } + } else { + "" + }, + if let Some(ctx) = ctx_a[i] { + format!(", flush id: {}", ctx.flush_id) } else { - " [deselected]" + "".to_owned() } - } else { - "" - }, - if let Some(ctx) = ctx_a[i] { - format!(", flush id: {}", ctx.flush_id) - } else { - "".to_owned() - } - ); - print!( - " | slot B{}: {rb:?}{}", - if let Some(slot_selected) = &slot_selected { - if !slot_selected[i] { - " [selected]" + ); + print!( + " | slot B{}: {rb:?}{}", + if let Some(slot_selected) = &slot_selected { + if !slot_selected[i] { + " [selected]" + } else { + " [deselected]" + } + } else { + "" + }, + if let Some(ctx) = ctx_b[i] { + format!(", flush id: {}", ctx.flush_id) } else { - " [deselected]" + "".to_owned() } - } else { - "" - }, - if let Some(ctx) = ctx_b[i] { - format!(", flush id: {}", ctx.flush_id) - } else { - "".to_owned() + ); + if all_zeros { + print!(" Block is all zeros"); } - ); - if chunk.iter().all(|b| *b == 0u8) { - print!(" Block is all zeros"); + println!(); } - println!(); } } From acf4d3659697f5e7f9791ebf4c1c332ad69cc354 Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Wed, 18 Mar 2026 14:30:09 -0700 Subject: [PATCH 2/5] Fix unquoted filename in CSV output of crucible-verify-raw --- verify-raw/src/main.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/verify-raw/src/main.rs b/verify-raw/src/main.rs index 281d34488..3426ebc7e 100644 --- a/verify-raw/src/main.rs +++ b/verify-raw/src/main.rs @@ -128,7 +128,7 @@ fn check_one( .collect::>>(); let (ctx_a, ctx_b) = context_slots.split_at(block_count); - let filename = p.display().to_string(); + let filename = csv_quote(&p.display().to_string()); let mut failed = false; // Check each block and emit one output line per block for (i, chunk) in data[..block_size * block_count] @@ -310,6 +310,15 @@ enum Failure { EmptySlotWithNonzeroData, } +/// Wrap a string in double quotes, escaping internal double quotes for CSV +fn csv_quote(s: &str) -> String { + if s.contains([',', '"', '\n']) { + format!("\"{}\"", s.replace('"', "\"\"")) + } else { + s.to_owned() + } +} + /// Brute force strategy to get block count fn get_block_count(data_len: usize, block_size: usize) -> Option { let estimated_block_count = From 5b91136a924b01ac2dce2250bce95da0447c6c04 Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Thu, 19 Mar 2026 13:36:16 -0700 Subject: [PATCH 3/5] also print out actual hash and expected hashes --- verify-raw/src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/verify-raw/src/main.rs b/verify-raw/src/main.rs index 3426ebc7e..a529bc10d 100644 --- a/verify-raw/src/main.rs +++ b/verify-raw/src/main.rs @@ -87,9 +87,9 @@ fn check_one( ); println!( "file,block,status,\ - slot_a_result,slot_a_selected,slot_a_flush_id,\ - slot_b_result,slot_b_selected,slot_b_flush_id,\ - all_zeros" + slot_a_result,slot_a_selected,slot_a_flush_id,slot_a_hash,\ + slot_b_result,slot_b_selected,slot_b_flush_id,slot_b_hash,\ + data_hash,all_zeros" ); } else { print!("bs:{block_size} bytes bc:{block_count:>6}"); @@ -153,13 +153,16 @@ fn check_one( failed = true; } println!( - "{filename},{i},{status},{},{},{},{},{},{},{all_zeros}", + "{filename},{i},{status},{},{},{},{},{},{},{},{},{},{all_zeros}", ra.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), sel_a.map_or("unknown".to_owned(), |s| s.to_string()), ctx_a[i].map_or(String::new(), |c| c.flush_id.to_string()), + ctx_a[i].map_or(String::new(), |c| c.on_disk_hash.to_string()), rb.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), sel_b.map_or("unknown".to_owned(), |s| s.to_string()), ctx_b[i].map_or(String::new(), |c| c.flush_id.to_string()), + ctx_b[i].map_or(String::new(), |c| c.on_disk_hash.to_string()), + hash, ); } else { let mut printed = false; From a2afbf15b4eefb014bd3a505e0ddaac486ddee7a Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Tue, 24 Mar 2026 10:56:03 -0700 Subject: [PATCH 4/5] Add dirty and selected_slot columns to crucible-verify-raw CSV output Add a dirty column (reflecting the extent's dirty bit) to each row of the CSV output. Replace the redundant slot_a_selected/slot_b_selected columns with a single selected_slot column: true means slot A is selected, false means slot B, unknown when the extent is dirty. --- verify-raw/src/main.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/verify-raw/src/main.rs b/verify-raw/src/main.rs index a529bc10d..b7d1e4b09 100644 --- a/verify-raw/src/main.rs +++ b/verify-raw/src/main.rs @@ -86,10 +86,10 @@ fn check_one( meta.defrag_count, ); println!( - "file,block,status,\ - slot_a_result,slot_a_selected,slot_a_flush_id,slot_a_hash,\ - slot_b_result,slot_b_selected,slot_b_flush_id,slot_b_hash,\ - data_hash,all_zeros" + "file,block,dirty,status,\ + slot_a_result,slot_a_flush_id,slot_a_hash,\ + slot_b_result,slot_b_flush_id,slot_b_hash,\ + selected_slot,data_hash,all_zeros" ); } else { print!("bs:{block_size} bytes bc:{block_count:>6}"); @@ -142,7 +142,6 @@ fn check_one( if verbose_csv { let sel_a = slot_selected.as_ref().map(|s| s[i]); - let sel_b = slot_selected.as_ref().map(|s| !s[i]); let status = match (&slot_selected, ra, rb) { (Some(s), ra, _) if s[i] && ra.is_err() => "error", (Some(s), _, rb) if !s[i] && rb.is_err() => "error", @@ -153,15 +152,15 @@ fn check_one( failed = true; } println!( - "{filename},{i},{status},{},{},{},{},{},{},{},{},{},{all_zeros}", + "{filename},{i},{},{status},{},{},{},{},{},{},{},{},{all_zeros}", + meta.dirty, ra.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), - sel_a.map_or("unknown".to_owned(), |s| s.to_string()), ctx_a[i].map_or(String::new(), |c| c.flush_id.to_string()), ctx_a[i].map_or(String::new(), |c| c.on_disk_hash.to_string()), rb.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), - sel_b.map_or("unknown".to_owned(), |s| s.to_string()), ctx_b[i].map_or(String::new(), |c| c.flush_id.to_string()), ctx_b[i].map_or(String::new(), |c| c.on_disk_hash.to_string()), + sel_a.map_or("unknown".to_owned(), |s| s.to_string()), hash, ); } else { From 720a78e739177d69dce517342ba5211c9f1c28b1 Mon Sep 17 00:00:00 2001 From: Alan Hanson Date: Tue, 24 Mar 2026 11:49:54 -0700 Subject: [PATCH 5/5] Always populate selected_slot in crucible-verify-raw CSV output Read the slot selection bitmap unconditionally, even when the dirty bit is set. The selected_slot column now always shows true/false instead of "unknown". Post-processing tools can use the dirty column to decide whether to trust the slot selection value. --- verify-raw/src/main.rs | 44 +++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/verify-raw/src/main.rs b/verify-raw/src/main.rs index b7d1e4b09..dca31d610 100644 --- a/verify-raw/src/main.rs +++ b/verify-raw/src/main.rs @@ -105,17 +105,12 @@ fn check_one( ); } - let slot_selected = if !meta.dirty { - let mut selected = vec![]; - for d in &data[data_len - block_count.div_ceil(8)..data_len] { - for i in 0..8 { - selected.push((d & (1 << i)) == 0); - } + let mut slot_selected = vec![]; + for d in &data[data_len - block_count.div_ceil(8)..data_len] { + for i in 0..8 { + slot_selected.push((d & (1 << i)) == 0); } - Some(selected) - } else { - None - }; + } let context_slots = (0..block_count * 2) .map(|i| { @@ -141,12 +136,17 @@ fn check_one( let all_zeros = chunk.iter().all(|b| *b == 0u8); if verbose_csv { - let sel_a = slot_selected.as_ref().map(|s| s[i]); - let status = match (&slot_selected, ra, rb) { - (Some(s), ra, _) if s[i] && ra.is_err() => "error", - (Some(s), _, rb) if !s[i] && rb.is_err() => "error", - (None, Err(_), Err(_)) => "error", - _ => "ok", + let sel_a = slot_selected[i]; + let status = if !meta.dirty { + if (sel_a && ra.is_err()) || (!sel_a && rb.is_err()) { + "error" + } else { + "ok" + } + } else if ra.is_err() && rb.is_err() { + "error" + } else { + "ok" }; if status == "error" { failed = true; @@ -160,15 +160,15 @@ fn check_one( rb.map_or_else(|e| format!("{e:?}"), |s| format!("{s:?}")), ctx_b[i].map_or(String::new(), |c| c.flush_id.to_string()), ctx_b[i].map_or(String::new(), |c| c.on_disk_hash.to_string()), - sel_a.map_or("unknown".to_owned(), |s| s.to_string()), + sel_a, hash, ); } else { let mut printed = false; - if let Some(slot_selected) = &slot_selected { - // If the slot selected array is valid (i.e. the extent file - // is not dirty), then it must be correct. + if !meta.dirty { + // If the extent is not dirty, the slot selected array is + // valid and must be correct. let s = slot_selected[i]; let ctx = if s { ctx_a[i] } else { ctx_b[i] }; let r = if s { ra } else { rb }; @@ -236,7 +236,7 @@ fn check_one( print!("Success at block {i}:"); print!( " slot A{}: {ra:?}{}", - if let Some(slot_selected) = &slot_selected { + if !meta.dirty { if slot_selected[i] { " [selected]" } else { @@ -253,7 +253,7 @@ fn check_one( ); print!( " | slot B{}: {rb:?}{}", - if let Some(slot_selected) = &slot_selected { + if !meta.dirty { if !slot_selected[i] { " [selected]" } else {