elio 1.5.1

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
mod elf;
mod macho;
mod pe;

use super::{PreviewContent, PreviewKind};
use crate::preview::appearance as theme;
use ratatui::{
    style::Style,
    text::{Line, Span},
};
use std::{fs::File, io::Read, path::Path};

const BINARY_METADATA_LIMIT_BYTES: usize = 4096;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum ByteOrder {
    Little,
    Big,
}

impl ByteOrder {
    pub(super) const fn label(self) -> &'static str {
        match self {
            Self::Little => "Little-endian",
            Self::Big => "Big-endian",
        }
    }
}

pub(super) struct BinaryMetadata {
    detail: &'static str,
    format: &'static str,
    kind: Option<String>,
    architecture: Option<String>,
    bits: Option<&'static str>,
    endianness: Option<&'static str>,
    abi: Option<String>,
    subsystem: Option<String>,
    entry_point: Option<String>,
    section_count: Option<usize>,
    command_count: Option<usize>,
}

impl BinaryMetadata {
    fn fields(&self) -> [(&'static str, Option<String>); 9] {
        [
            ("Format", Some(self.format.to_string())),
            ("Kind", self.kind.clone()),
            ("Architecture", self.architecture.clone()),
            ("Bits", self.bits.map(str::to_string)),
            ("Endianness", self.endianness.map(str::to_string)),
            ("ABI", self.abi.clone()),
            ("Subsystem", self.subsystem.clone()),
            ("Entry Point", self.entry_point.clone()),
            (
                "Sections",
                self.section_count.map(|count| count.to_string()),
            ),
        ]
    }

    fn extra_fields(&self) -> [(&'static str, Option<String>); 1] {
        [(
            "Load Commands",
            self.command_count.map(|count| count.to_string()),
        )]
    }
}

pub(super) fn build_binary_preview(
    path: &Path,
    type_detail: Option<&'static str>,
) -> Option<PreviewContent> {
    let mut bytes = Vec::with_capacity(BINARY_METADATA_LIMIT_BYTES);
    File::open(path)
        .ok()?
        .take(BINARY_METADATA_LIMIT_BYTES as u64)
        .read_to_end(&mut bytes)
        .ok()?;

    let metadata = parse_binary_metadata(&bytes)?;
    Some(render_binary_preview(
        type_detail.unwrap_or(metadata.detail),
        metadata,
    ))
}

fn parse_binary_metadata(bytes: &[u8]) -> Option<BinaryMetadata> {
    pe::parse(bytes)
        .or_else(|| elf::parse(bytes))
        .or_else(|| macho::parse(bytes))
        .or_else(|| pe::parse_dos_mz(bytes))
}

fn render_binary_preview(detail: &str, metadata: BinaryMetadata) -> PreviewContent {
    let palette = theme::palette();
    let mut lines = Vec::new();
    push_section(&mut lines, "Details", &metadata.fields(), palette);
    push_section(&mut lines, "Metadata", &metadata.extra_fields(), palette);

    PreviewContent::new(PreviewKind::Binary, lines).with_detail(detail)
}

fn push_section(
    lines: &mut Vec<Line<'static>>,
    title: &str,
    fields: &[(&str, Option<String>)],
    palette: theme::Palette,
) {
    let visible_fields = fields
        .iter()
        .filter_map(|(label, value)| value.as_deref().map(|value| (*label, value)))
        .collect::<Vec<_>>();
    if visible_fields.is_empty() {
        return;
    }

    if !lines.is_empty() {
        lines.push(Line::from(""));
    }
    lines.push(section_line(title, palette));
    let label_width = visible_fields
        .iter()
        .map(|(label, _)| label.len())
        .max()
        .unwrap_or(6);
    for (label, value) in visible_fields {
        lines.push(field_line(label, value, label_width, palette));
    }
}

fn section_line(title: &str, palette: theme::Palette) -> Line<'static> {
    Line::from(Span::styled(
        title.to_string(),
        Style::default().fg(palette.accent),
    ))
}

fn field_line(
    label: &str,
    value: &str,
    label_width: usize,
    palette: theme::Palette,
) -> Line<'static> {
    Line::from(vec![
        Span::styled(
            format!("{label:<width$} ", width = label_width + 1),
            Style::default().fg(palette.muted),
        ),
        Span::styled(value.to_string(), Style::default().fg(palette.text)),
    ])
}

pub(super) fn format_hex(value: u64) -> String {
    format!("0x{value:X}")
}

pub(super) fn read_u16(bytes: &[u8], offset: usize, order: ByteOrder) -> Option<u16> {
    let raw = bytes.get(offset..offset + 2)?;
    Some(match order {
        ByteOrder::Little => u16::from_le_bytes(raw.try_into().ok()?),
        ByteOrder::Big => u16::from_be_bytes(raw.try_into().ok()?),
    })
}

pub(super) fn read_u32(bytes: &[u8], offset: usize, order: ByteOrder) -> Option<u32> {
    let raw = bytes.get(offset..offset + 4)?;
    Some(match order {
        ByteOrder::Little => u32::from_le_bytes(raw.try_into().ok()?),
        ByteOrder::Big => u32::from_be_bytes(raw.try_into().ok()?),
    })
}

pub(super) fn read_u64(bytes: &[u8], offset: usize, order: ByteOrder) -> Option<u64> {
    let raw = bytes.get(offset..offset + 8)?;
    Some(match order {
        ByteOrder::Little => u64::from_le_bytes(raw.try_into().ok()?),
        ByteOrder::Big => u64::from_be_bytes(raw.try_into().ok()?),
    })
}

pub(super) fn read_u16_le(bytes: &[u8], offset: usize) -> Option<u16> {
    read_u16(bytes, offset, ByteOrder::Little)
}

pub(super) fn read_u32_le(bytes: &[u8], offset: usize) -> Option<u32> {
    read_u32(bytes, offset, ByteOrder::Little)
}