yb 0.7.1

Secure blob storage on a YubiKey
// SPDX-FileCopyrightText: 2025 - 2026 Frederic Ruget <fred@atlant.is> (GitHub: @douzebis)
//
// SPDX-License-Identifier: MIT

use anyhow::Result;
use chrono::{DateTime, Duration, Local, Utc};
use clap::Args;
use clap_complete::engine::ArgValueCompleter;
use globset::GlobBuilder;
use yb_core::{orchestrator, parse_ec_public_key_from_cert_der, store::Store, Context};

use crate::cli::util::{check_blob_signature, quote_name, SigVerdict};
use crate::complete::complete_blob_names;

#[derive(Args, Debug)]
pub struct ListArgs {
    /// Glob pattern to filter blob names (default: all).
    #[arg(add = ArgValueCompleter::new(complete_blob_names))]
    pub pattern: Option<String>,

    /// Long format: flag, chunks, date, size, name.
    #[arg(short = 'l', long = "long")]
    pub long: bool,

    /// One name per line (default; explicit flag for scripting).
    #[arg(short = '1')]
    pub one_per_line: bool,

    /// Sort by modification time, newest first.
    #[arg(short = 't', long = "sort-time")]
    pub sort_time: bool,

    /// Reverse the sort order.
    #[arg(short = 'r', long = "reverse")]
    pub reverse: bool,
}

pub fn run(ctx: &Context, args: &ListArgs) -> Result<()> {
    let matcher = match &args.pattern {
        Some(pat) => {
            let glob = GlobBuilder::new(pat)
                .case_insensitive(false)
                .build()?
                .compile_matcher();
            Some(glob)
        }
        None => None,
    };

    let store = Store::from_device(&ctx.reader, ctx.piv.as_ref())?;

    // Fetch verifying key from the store's key slot certificate — no PIN needed.
    let verifying_key = ctx
        .piv
        .read_certificate(&ctx.reader, store.store_key_slot)
        .ok()
        .and_then(|cert_der| parse_ec_public_key_from_cert_der(&cert_der).ok())
        .map(|pk| {
            use p256::ecdsa::VerifyingKey;
            VerifyingKey::from(&pk)
        });

    // Build a verdict map keyed by blob name.
    let heads: Vec<_> = store.objects.iter().filter(|o| o.is_head()).collect();
    let mut verdict_map: std::collections::HashMap<&str, SigVerdict> =
        std::collections::HashMap::new();
    let post_quantum = store.kind.is_post_quantum();
    for head in &heads {
        let v = if post_quantum {
            quick_check(head, &store)
        } else {
            check_blob_signature(head, &store, verifying_key.as_ref())
        };
        verdict_map.insert(head.blob_name.as_str(), v);
    }
    if post_quantum && args.long {
        println!("post-quantum store");
    }

    let mut blobs = orchestrator::list_blobs(&store);

    // Filter.
    if let Some(ref m) = matcher {
        blobs.retain(|b| m.is_match(&b.name));
    }

    // Sort.
    if args.sort_time {
        blobs.sort_by_key(|b| std::cmp::Reverse(b.mtime)); // newest first
    }
    // Default sort (by name ascending) is already applied by list_blobs.

    if args.reverse {
        blobs.reverse();
    }

    let mut any_corrupted = false;
    for b in &blobs {
        let verdict = verdict_map.get(b.name.as_str()).copied();
        if verdict == Some(SigVerdict::Corrupted) {
            any_corrupted = true;
        }
        let displayed_name = quote_name(&b.name);
        // CORRUPTED alone; otherwise UNSIGNED and LEGACY (spec 0034 G4).
        let suffix = match verdict {
            Some(SigVerdict::Corrupted) => "  CORRUPTED".to_owned(),
            _ => {
                let mut s = String::new();
                if verdict == Some(SigVerdict::Unsigned) {
                    s.push_str("  UNSIGNED");
                }
                if b.is_legacy_encryption {
                    s.push_str("  LEGACY");
                }
                s
            }
        };
        if args.long {
            let enc_flag = if b.is_encrypted { '-' } else { 'P' };
            let date = format_mtime(b.mtime);
            println!(
                "{enc_flag} {:2}  {}  {:6}  {displayed_name}{suffix}",
                b.chunk_count, date, b.plain_size,
            );
        } else {
            println!("{displayed_name}{suffix}");
        }
    }

    if any_corrupted {
        // A post-quantum store's checksum (spec 0028): status 4.
        std::process::exit(if post_quantum { 4 } else { 1 });
    }

    Ok(())
}

fn format_mtime(unix: u32) -> String {
    if unix == 0 {
        return "            ".to_owned();
    }
    let dt: DateTime<Local> =
        DateTime::from(DateTime::<Utc>::from_timestamp(unix as i64, 0).unwrap());
    let now = Local::now();
    if now.signed_duration_since(dt) < Duration::days(180) {
        dt.format("%b %e %H:%M").to_string()
    } else {
        dt.format("%b %e  %Y").to_string()
    }
}

/// The checksum check of a post-quantum blob (spec 0028, D5).
#[cfg(not(windows))]
fn quick_check(head: &yb_core::store::Object, store: &Store) -> SigVerdict {
    yb_core::orchestrator::post_quantum::quick_check(head, store)
}

#[cfg(windows)]
fn quick_check(_: &yb_core::store::Object, _: &Store) -> SigVerdict {
    SigVerdict::Unverifiable
}