parcode 0.6.1

A high-performance, lazy load and parallelized caching library for complex Rust data structures.
Documentation
//! Tools for inspecting the physical structure of Parcode files.
//!
//! This module provides the [`ParcodeInspector`] tool, which can analyze a Parcode file
//! and report on its internal structure, chunk sizes, compression algorithms, and data distribution.
//!
//! This is useful for:
//! - **Forensics:** Understanding why a file is large or corrupt.
//! - **Optimization:** Verifying that your `#[parcode(chunkable)]` and `#[parcode(map)]` attributes are working as intended.
//! - **Debugging:** visualizing the dependency graph of your serialized data.

use crate::error::Result;
use crate::reader::{ChunkNode, ParcodeFile};
use serde::{Deserialize, Serialize};
use std::path::Path;

/// A structural report of a Parcode file.
#[derive(Debug, Serialize)]
pub struct DebugReport {
    /// Total size of the file on disk.
    pub file_size: u64,
    /// Offset where the Root chunk starts.
    pub root_offset: u64,
    /// Format version.
    pub global_version: u16,
    /// The hierarchical tree of chunks.
    pub tree: ChunkInfo,
}

/// Metadata for a single chunk in the graph.
#[derive(Debug, Serialize)]
pub struct ChunkInfo {
    /// Absolute offset.
    pub offset: u64,
    /// Total chunk length (header + payload + footer).
    pub total_length: u64,
    /// Size of the data payload.
    pub payload_size: u64,
    /// Number of children dependencies.
    pub child_count: u32,
    /// Compression algorithm used.
    pub compression_algo: String,
    /// Whether it flagged as having children.
    pub is_chunkable: bool,
    /// Inferred type info (e.g., "Vec Container", "Map Shard", "Blob").
    pub node_type_hint: String,
    /// Extra info about distribution (e.g., "32 shards").
    pub distribution_info: Option<String>,
    /// Child nodes.
    pub children: Vec<Self>,
}

/// The Parcode Inspector tool.
#[derive(Debug)]
pub struct ParcodeInspector;

impl ParcodeInspector {
    /// Inspects an already open file.
    pub fn inspect_file(file: &ParcodeFile) -> Result<DebugReport> {
        let root = file.root_node()?;
        let tree = Self::inspect_node(&root)?;

        Ok(DebugReport {
            file_size: file.file_size(),
            root_offset: root.offset(),
            global_version: 4,
            tree,
        })
    }

    /// Convenience wrapper to inspect from path.
    pub fn inspect<P>(path: P) -> Result<DebugReport>
    where
        P: AsRef<Path>,
    {
        let file = ParcodeFile::open(path)?;
        Self::inspect_file(&file)
    }

    fn inspect_node(node: &ChunkNode<'_>) -> Result<ChunkInfo> {
        let raw_meta = node.meta();
        let algo_id = raw_meta.compression_method();
        let algo_name = match algo_id {
            0 => "None".to_string(),
            1 => "LZ4".to_string(),
            _ => format!("Unknown({})", algo_id),
        };

        let (hint, distrib) = Self::analyze_payload(node);

        let mut children_info = Vec::new();
        if node.child_count() > 0 {
            let children = node.children()?;
            for child in children {
                children_info.push(Self::inspect_node(&child)?);
            }
        }

        Ok(ChunkInfo {
            offset: node.offset(),
            total_length: node.length(),
            payload_size: node.payload_len(),
            child_count: node.child_count(),
            compression_algo: algo_name,
            is_chunkable: raw_meta.is_chunkable(),
            node_type_hint: hint,
            distribution_info: distrib,
            children: children_info,
        })
    }

    fn analyze_payload(node: &ChunkNode<'_>) -> (String, Option<String>) {
        if node.child_count() == 0 {
            return ("Leaf/Blob".to_string(), None);
        }

        let payload = match node.read_raw() {
            Ok(p) => p,
            Err(_) => return ("Corrupted".to_string(), None),
        };

        // CHECK 1: Vec Container
        if payload.len() >= 8 {
            #[derive(Deserialize)]
            struct ShardRun {
                _item_count: u32,
                repeat: u32,
            }

            if let Some(slice) = payload.get(8..) {
                // We attempt to decode. If it fails, it's not a Vec header.
                if let Ok((runs, _)) = bincode::serde::decode_from_slice::<Vec<ShardRun>, _>(
                    slice,
                    bincode::config::standard(),
                ) {
                    let total_items = if let Some(header_bytes) = payload.get(0..8) {
                        u64::from_le_bytes(header_bytes.try_into().unwrap_or([0; 8]))
                    } else {
                        0
                    };

                    let distribution = format!(
                        "Vec<{}> items across {} logical shards",
                        total_items,
                        runs.iter().map(|r| r.repeat).sum::<u32>()
                    );
                    return ("Vec Container".to_string(), Some(distribution));
                }
            }
        }

        // CHECK 2: Map Container (4 bytes exactly)
        if payload.len() == 4
            && payload
                .get(0..4)
                .and_then(|b| b.try_into().ok())
                .map(u32::from_le_bytes)
                == Some(node.child_count())
        {
            let num_shards = node.child_count();
            return (
                "Map Container".to_string(),
                Some(format!("Hashtable with {} buckets", num_shards)),
            );
        }

        ("Generic Container".to_string(), None)
    }
}

impl std::fmt::Display for DebugReport {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "=== PARCODE INSPECTOR REPORT ===")?;
        writeln!(f, "Root Offset:    {}", self.root_offset)?;
        writeln!(f, "\n[GRAPH LAYOUT]")?;
        self.tree.fmt_recursive(f, "", true)
    }
}

impl ChunkInfo {
    fn fmt_recursive(
        &self,
        f: &mut std::fmt::Formatter<'_>,
        prefix: &str,
        is_last: bool,
    ) -> std::fmt::Result {
        let connector = if is_last { "└── " } else { "├── " };
        let child_prefix = if is_last { "    " } else { "" };
        let distrib = self
            .distribution_info
            .as_deref()
            .map(|d| format!(" [{}]", d))
            .unwrap_or_default();

        writeln!(
            f,
            "{}{}[{}] Size: {}b | Algo: {} | Children: {}{}",
            prefix,
            connector,
            self.node_type_hint,
            self.payload_size,
            self.compression_algo,
            self.child_count,
            distrib
        )?;

        for (i, child) in self.children.iter().enumerate() {
            let is_last_child = i == self.children.len() - 1;
            child.fmt_recursive(f, &format!("{}{}", prefix, child_prefix), is_last_child)?;
        }
        Ok(())
    }
}