parx-cli 0.1.0

CLI tool for building and inspecting PARX sidecar files
/*
 * Copyright 2026 PARX Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
use anyhow::{Context, Result};
use parx_rs::ParxReader;
use std::time::{Duration, UNIX_EPOCH};

use super::storage;

/// Run the inspect command.
pub async fn run(input: &str) -> Result<()> {
    // Read PARX file
    let parx_bytes = storage::read_all(input)
        .await
        .context("Failed to read PARX file")?;

    // Parse PARX file
    let reader = ParxReader::open(&parx_bytes).context("Failed to parse PARX file")?;

    let manifest = reader.manifest();
    let header = reader.header();

    println!("PARX File: {}", input);
    println!();
    println!("Header:");
    println!(
        " Version: {}.{}",
        header.version_major, header.version_minor
    );
    println!(" Flags: {:#06x}", header.flags);

    // Show flag details
    if header.is_footer_compressed() {
        if let Some(algo) = header.compression_algorithm() {
            println!(" - Footer compressed ({})", algo);
        }
    }

    println!();
    println!("Manifest:");
    println!(" Version: {}", manifest.version);
    println!();
    println!("Source Binding:");
    if !manifest.source_uri.is_empty() {
        println!(" URI: {}", manifest.source_uri);
    }
    println!(" Size: {} bytes", manifest.source_size);
    println!(
        " Footer hash: {}",
        hex_string(&manifest.source_footer_checksum)
    );
    println!();
    println!("Footer Payload:");
    println!(" Offset: {}", manifest.footer_offset);
    println!(" Length: {} bytes", manifest.footer_length);

    // Show compression info
    if reader.is_compressed() {
        println!(
            " Uncompressed size: {} bytes",
            manifest.footer_uncompressed_size
        );
        let ratio = if manifest.footer_uncompressed_size > 0 {
            (manifest.footer_length as f64 / manifest.footer_uncompressed_size as f64) * 100.0
        } else {
            100.0
        };
        println!(" Compression ratio: {:.1}%", ratio);
    }

    println!(" Checksum: {}", hex_string(&manifest.footer_checksum));

    println!();
    println!("Build Metadata:");

    if manifest.created_at_ms > 0 {
        let created = UNIX_EPOCH + Duration::from_millis(manifest.created_at_ms);
        if let Ok(datetime) = created.duration_since(UNIX_EPOCH) {
            let secs = datetime.as_secs();
            println!(" Created: {} (unix timestamp)", secs);
        }
    }

    Ok(())
}

fn hex_string(bytes: &[u8]) -> String {
    if bytes.is_empty() {
        return "(empty)".to_string();
    }
    bytes.iter().map(|b| format!("{:02x}", b)).collect()
}