gaze-cli 0.11.1

Gaze command-line interface
mod manifest;
mod session;

use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;

use gaze::{
    Action, ConflictTier, DocumentKind, PiiClass, Pipeline, RedactionEntry, RestorePolicy,
    RestoreTelemetry, SensitiveSnapshot, Session,
};
use gaze_audit::SqliteLogger;

use crate::error::{CliError, RestoreMode};
use crate::io::{read_stdin_bytes, require_json_format};
use crate::restore::manifest::{RestoreRequest, RestoreResponse};
use crate::restore::session::{restore_pass1, restore_pass2_validate};

pub(crate) fn run_restore(
    format: &str,
    restore_mode: RestoreMode,
    telemetry_enabled: bool,
    audit_db: Option<&Path>,
    max_bytes: u64,
) -> std::result::Result<(), CliError> {
    require_json_format(format)?;
    let stdin_bytes = read_stdin_bytes(max_bytes)?;

    let request: RestoreRequest =
        serde_json::from_slice(&stdin_bytes).map_err(|_| CliError::StdinParse)?;

    let blob_bytes = BASE64
        .decode(request.session_blob.as_bytes())
        .map_err(|_| CliError::StdinParse)?;

    let session =
        Session::import(SensitiveSnapshot::from(blob_bytes)).map_err(|err| match err {
            gaze::Error::InvalidSnapshotSignature => CliError::InvalidSignature,
            gaze::Error::InvalidSnapshotVersion(_) => CliError::InvalidBlobVersion,
            gaze::Error::InvalidSnapshotPayload => CliError::InvalidBlobVersion,
            gaze::Error::BlobExpired { .. } => CliError::BlobExpired,
            _ => CliError::Pipeline,
        })?;

    let pass1 = restore_pass1(&session, &request.text)?;
    let restore_telemetry = if telemetry_enabled || audit_db.is_some() {
        let pipeline = Pipeline::builder()
            .build()
            .map_err(|_| CliError::Pipeline)?;
        let (_, telemetry) = pipeline
            .restore_with_policy_telemetry(&session, &request.text, restore_policy(restore_mode))
            .map_err(|_| CliError::Pipeline)?;
        if let Some(path) = audit_db {
            persist_restore_telemetry(path, &session, telemetry.clone())?;
        }
        telemetry_enabled.then_some(telemetry)
    } else {
        None
    };
    let restore_warning = restore_pass2_validate(
        &pass1.text,
        &pass1.substitution_spans,
        &session,
        restore_mode,
    )?;

    let response = RestoreResponse {
        text: pass1.text,
        restore_warning,
        restore_telemetry,
    };
    let json = serde_json::to_string(&response).map_err(|_| CliError::Pipeline)?;
    println!("{json}");
    Ok(())
}

fn restore_policy(mode: RestoreMode) -> RestorePolicy {
    match mode {
        RestoreMode::Strict => RestorePolicy::Strict,
        RestoreMode::Tolerant => RestorePolicy::Lenient,
    }
}

fn persist_restore_telemetry(
    audit_db: &Path,
    session: &Session,
    telemetry: RestoreTelemetry,
) -> std::result::Result<(), CliError> {
    let logger = SqliteLogger::new(audit_db).map_err(|_| CliError::Pipeline)?;
    let entry = RedactionEntry::new(
        "restore",
        PiiClass::Custom("restore.telemetry".to_string()),
        Action::Preserve,
        None,
        DocumentKind::Text,
        false,
        ConflictTier::None,
        current_epoch_ms(),
        Some(session.audit_session_id().to_string()),
    )
    .with_restore_telemetry(telemetry);
    logger.log(&entry).map_err(|_| CliError::Pipeline)
}

fn current_epoch_ms() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
        .unwrap_or(0)
}