void-cli 0.0.3

CLI for void — anonymous encrypted source control
//! Read and display shard contents.
//!
//! Decrypts a shard and shows basic size information. Shards are opaque
//! compressed blocks — file indexing is in the TreeManifest.

use std::path::Path;

use serde::Serialize;
use void_core::crypto::{AAD_SHARD, EncryptedShard};
use void_core::shard::read_padding_info;
use void_core::store::{FsStore, ObjectStoreExt};
use void_core::cid;

use crate::context::{open_repo, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};

/// Command-line arguments for read-shard.
#[derive(Debug)]
pub struct ReadShardArgs {
    /// CID of the shard to read.
    pub cid: String,
}

/// JSON output for the read-shard command.
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReadShardOutput {
    /// The CID of the shard.
    pub cid: String,
    /// Total encrypted blob size.
    pub encrypted_size: usize,
    /// Decrypted size (includes padding if any).
    pub decrypted_size: usize,
    /// Compressed data size (excluding padding).
    pub compressed_size: usize,
    /// Padding size (0 if unpadded).
    pub padding_size: usize,
}

/// Run the read-shard command.
pub fn run(cwd: &Path, args: ReadShardArgs, opts: &CliOptions) -> Result<(), CliError> {
    run_command("read-shard", opts, |ctx| {
        ctx.progress(format!("Reading shard {}...", args.cid));

        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)?;

        // Parse CID
        let parsed_cid = cid::parse(&args.cid)
            .map_err(|e| CliError::invalid_args(format!("invalid CID: {}", e)))?;

        // Load and decrypt
        let encrypted: EncryptedShard = store.get_blob(&parsed_cid).map_err(void_err_to_cli)?;
        let encrypted_size = encrypted.as_bytes().len();
        let decrypted = vault.decrypt_blob(encrypted.as_bytes(), AAD_SHARD)
            .map_err(|e| CliError::encryption_error(format!("decryption failed: {}", e)))?;
        let decrypted_size = decrypted.len();

        // Check for padding
        let padding_size = read_padding_info(&decrypted).unwrap_or(0);
        let padding_footer = if padding_size > 0 { 16 } else { 0 };
        let compressed_size = decrypted_size.saturating_sub(padding_size + padding_footer);

        // Human-readable output
        if !ctx.use_json() {
            ctx.info(format!("Shard: {}", args.cid));
            ctx.info(format!("Encrypted size: {} bytes", encrypted_size));
            ctx.info(format!("Compressed size: {} bytes", compressed_size));
            if padding_size > 0 {
                ctx.info(format!("Padding: {} bytes", padding_size));
            }
            ctx.info("(Use TreeManifest for file index details)".to_string());
        }

        Ok(ReadShardOutput {
            cid: args.cid,
            encrypted_size,
            decrypted_size,
            compressed_size,
            padding_size,
        })
    })
}