use std::path::Path;
use serde::Serialize;
use void_core::crypto::{CommitReader, EncryptedCommit, EncryptedMetadata};
use void_core::store::{FsStore, ObjectStoreExt};
use void_core::{cid, metadata};
use void_core::support::ToVoidCid;
use crate::context::{open_repo, resolve_ref, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug)]
pub struct DebugShardsArgs {
pub commit_ref: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugShardsOutput {
pub commit_cid: String,
pub metadata_cid: String,
pub range_count: usize,
pub populated_ranges: usize,
pub total_compressed_size: u64,
pub shards: Vec<ShardRangeOutput>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShardRangeOutput {
pub shard_id: u64,
pub range_start: String,
pub range_end: String,
pub cid: String,
pub compressed_size: u64,
}
pub fn run(cwd: &Path, args: DebugShardsArgs, opts: &CliOptions) -> Result<(), CliError> {
run_command("debug-shards", opts, |ctx| {
let repo = open_repo(cwd)?;
let vault = repo.vault();
let objects_dir = repo.void_dir().join("objects");
let store = FsStore::new(objects_dir).map_err(void_err_to_cli)?;
let commit_ref = args.commit_ref.as_deref().unwrap_or("HEAD");
ctx.progress(format!("Loading commit {}...", commit_ref));
let commit_cid_typed = resolve_ref(repo.void_dir().as_std_path(), commit_ref)?;
let commit_cid = cid::from_bytes(commit_cid_typed.as_bytes())
.map_err(|e| CliError::internal(format!("invalid CID: {}", e)))?;
let commit_cid_str = commit_cid.to_string();
let commit_encrypted: EncryptedCommit = store.get_blob(&commit_cid).map_err(void_err_to_cli)?;
let (commit_decrypted, reader) = CommitReader::open_with_vault(vault, &commit_encrypted)
.map_err(|e| CliError::encryption_error(format!("commit decryption failed: {}", e)))?;
let commit = commit_decrypted.parse().map_err(void_err_to_cli)?;
ctx.progress("Loading metadata bundle...");
let metadata_cid = commit.metadata_bundle.to_void_cid()
.map_err(|e| CliError::internal(format!("invalid metadata CID: {}", e)))?;
let metadata_cid_str = metadata_cid.to_string();
let metadata_encrypted: EncryptedMetadata = store.get_blob(&metadata_cid).map_err(void_err_to_cli)?;
let bundle: metadata::MetadataBundle = reader
.decrypt_metadata::<metadata::MetadataBundle>(&metadata_encrypted)
.map_err(|e| {
CliError::encryption_error(format!("metadata decryption failed: {}", e))
})?;
let range_count = bundle.shard_map.ranges.len();
let mut shards = Vec::new();
let mut total_compressed_size = 0u64;
for range in bundle.shard_map.ranges.iter() {
if let Some(shard_cid_typed) = &range.cid {
let shard_cid_str = cid::from_bytes(shard_cid_typed.as_bytes())
.map(|c| c.to_string())
.unwrap_or_else(|_| "<invalid>".to_string());
total_compressed_size += range.compressed_size;
shards.push(ShardRangeOutput {
shard_id: range.shard_id,
range_start: format!("{:016x}", range.start),
range_end: format!("{:016x}", range.end),
cid: shard_cid_str,
compressed_size: range.compressed_size,
});
}
}
let populated_ranges = shards.len();
if !ctx.use_json() {
ctx.info(format!("Commit: {}", commit_cid_str));
ctx.info(format!("Metadata: {}", metadata_cid_str));
ctx.info(format!(
"Shard map: {} ranges, {} populated",
range_count, populated_ranges
));
ctx.info(format!(
"Total compressed size: {} bytes",
total_compressed_size
));
if !shards.is_empty() {
ctx.info(format!("\nShards:"));
for shard in &shards {
let short_cid = if shard.cid.len() > 16 {
&shard.cid[..16]
} else {
&shard.cid
};
ctx.info(format!(
" ID {} [{}-{}]: {}... ({} bytes)",
shard.shard_id,
&shard.range_start[..8],
&shard.range_end[..8],
short_cid,
shard.compressed_size
));
}
}
}
Ok(DebugShardsOutput {
commit_cid: commit_cid_str,
metadata_cid: metadata_cid_str,
range_count,
populated_ranges,
total_compressed_size,
shards,
})
})
}