recon-cli 0.92.0

Versatile network reconnaissance CLI: HTTP/TLS/DNS, multi-protocol probes, and a Rhai script engine
Documentation
//! Integration test for `--export-pdf-page`. Gated on `pdftoppm`
//! availability — when poppler-utils isn't installed, the test exits
//! early with a printed skip line so CI without poppler doesn't fail.
//!
//! Uses `docs/MANUAL.pdf` as the source PDF (committed, generated by
//! `recon --md-to-pdf` in the project's release flow).

use std::path::PathBuf;
use std::process::Command;

fn recon_bin() -> PathBuf {
    let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    p.push("target");
    p.push("release");
    p.push("recon");
    p
}

fn pdftoppm_present() -> bool {
    Command::new("pdftoppm")
        .arg("-v")
        .output()
        .map(|o| o.status.success() || !o.stderr.is_empty())
        .unwrap_or(false)
}

fn run_export(args: &[&str]) -> (bool, String, String) {
    let out = Command::new(recon_bin()).args(args).output().expect("spawn recon");
    (
        out.status.success(),
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
    )
}

const MAGIC_PNG: &[u8] = &[0x89, 0x50, 0x4E, 0x47];
const MAGIC_JPEG: &[u8] = &[0xFF, 0xD8, 0xFF];

fn assert_magic(path: &PathBuf, magic: &[u8]) {
    let bytes = std::fs::read(path).expect("read output");
    assert!(
        bytes.starts_with(magic),
        "magic mismatch for {}: got {:02x?}",
        path.display(),
        &bytes[..magic.len().min(bytes.len())]
    );
}

fn assert_webp(path: &PathBuf) {
    let bytes = std::fs::read(path).expect("read output");
    assert_eq!(&bytes[0..4], b"RIFF", "RIFF header missing");
    assert_eq!(&bytes[8..12], b"WEBP", "WEBP signature missing");
}

/// Decode a PNG and assert the rendered content has visible variation —
/// not the flat-color result the old agent-browser backend produced
/// when Chromium failed to render the PDF. Scans every Nth pixel
/// across the whole image (cheap on the test sizes used here) and
/// requires a wide enough channel range to rule out a flat fill.
fn assert_png_has_content(path: &PathBuf) {
    let bytes = std::fs::read(path).expect("read png");
    let decoder = png::Decoder::new(bytes.as_slice());
    let mut reader = decoder.read_info().expect("png header");
    let mut buf = vec![0u8; reader.output_buffer_size()];
    let info = reader.next_frame(&mut buf).expect("png frame");
    let channels = match info.color_type {
        png::ColorType::Rgb => 3,
        png::ColorType::Rgba => 4,
        png::ColorType::Grayscale => 1,
        png::ColorType::GrayscaleAlpha => 2,
        other => panic!("unexpected color type: {other:?}"),
    };
    let stride = (info.width as usize) * channels;
    let mut min = u8::MAX;
    let mut max = u8::MIN;
    // Step by every ~8 pixels in both dimensions; on a 2049x2651 image
    // that's ~85k samples — fast and dense enough to land on any
    // glyph the renderer drew. Cover-style pages with centered text
    // would slip through a coarse 5x5 grid.
    let step = 8usize;
    let mut y = 0usize;
    while y < info.height as usize {
        let row = y * stride;
        let mut x = 0usize;
        while x < info.width as usize {
            let v = buf[row + x * channels];
            if v < min {
                min = v;
            }
            if v > max {
                max = v;
            }
            x += step;
        }
        y += step;
    }
    let range = max as i32 - min as i32;
    assert!(
        range > 32,
        "rendered PNG looks like a flat color (sample range {min}..={max}); \
         likely a renderer failure"
    );
}

#[test]
fn export_png_default() {
    if !pdftoppm_present() {
        eprintln!("SKIP: pdftoppm not on PATH");
        return;
    }
    let out = std::env::temp_dir().join("recon-it-page.png");
    let _ = std::fs::remove_file(&out);
    let out_str = out.to_str().unwrap();
    let (ok, _stdout, stderr) = run_export(&[
        "--export-pdf-page", "1", "docs/MANUAL.pdf",
        "-o", out_str,
    ]);
    assert!(ok, "non-zero exit; stderr: {stderr}");
    assert_magic(&out, MAGIC_PNG);
    assert_png_has_content(&out);
    std::fs::remove_file(&out).ok();
}

#[test]
fn export_jpeg_quality() {
    if !pdftoppm_present() {
        eprintln!("SKIP: pdftoppm not on PATH");
        return;
    }
    let out = std::env::temp_dir().join("recon-it-page.jpg");
    let _ = std::fs::remove_file(&out);
    let out_str = out.to_str().unwrap();
    let (ok, _stdout, stderr) = run_export(&[
        "--export-pdf-page", "2", "docs/MANUAL.pdf",
        "-o", out_str,
        "--pdf-quality", "60",
    ]);
    assert!(ok, "non-zero exit; stderr: {stderr}");
    assert_magic(&out, MAGIC_JPEG);
    std::fs::remove_file(&out).ok();
}

#[test]
fn export_webp_transcode() {
    if !pdftoppm_present() {
        eprintln!("SKIP: pdftoppm not on PATH");
        return;
    }
    let out = std::env::temp_dir().join("recon-it-page.webp");
    let _ = std::fs::remove_file(&out);
    let out_str = out.to_str().unwrap();
    let (ok, _stdout, stderr) = run_export(&[
        "--export-pdf-page", "1", "docs/MANUAL.pdf",
        "-o", out_str,
    ]);
    assert!(ok, "non-zero exit; stderr: {stderr}");
    assert_webp(&out);
    std::fs::remove_file(&out).ok();
}

#[test]
fn export_page_out_of_range() {
    if !pdftoppm_present() {
        eprintln!("SKIP: pdftoppm not on PATH");
        return;
    }
    let out = std::env::temp_dir().join("recon-it-oor.png");
    let _ = std::fs::remove_file(&out);
    let out_str = out.to_str().unwrap();
    let (ok, _stdout, stderr) = run_export(&[
        "--export-pdf-page", "99999", "docs/MANUAL.pdf",
        "-o", out_str,
    ]);
    assert!(!ok, "expected non-zero exit for out-of-range page");
    assert!(
        stderr.contains("out of range"),
        "expected 'out of range' in stderr, got: {stderr}"
    );
    // No file should have been written.
    assert!(!out.exists(), "file should not exist for failed render");
}