artifactize-core 0.13.0

The library behind artifactize: Artifacts, evals, reviews, the monitor and review models, and the shared operations every interface calls.
Documentation
use std::fs;

use base64::{Engine, engine::general_purpose::STANDARD};
use serde_json::json;

use super::*;
use crate::{
    test_os::{symlink_dir, symlink_file},
    tools::result,
};

const PNG_SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
// The public decoded-image limit, not the implementation's bound: these fixtures
// must keep checking the 4 MiB contract if the production constant changes.
const IMAGE_BYTES: usize = 4 * 1024 * 1024;

pub(in crate::tools) fn fixtures() -> [(&'static str, Vec<u8>); 3] {
    let mut png = PNG_SIGNATURE.to_vec();
    chunk(&mut png, b"IHDR", &[0, 0, 0, 1, 0, 0, 0, 1, 8, 0, 0, 0, 0]);
    chunk(&mut png, b"tEXt", b"note\0acTL");
    chunk(
        &mut png,
        b"IDAT",
        &[0x78, 1, 1, 2, 0, 0xfd, 0xff, 0, 0xff, 1, 1, 1, 0],
    );
    chunk(&mut png, b"IEND", &[]);
    // One-pixel white JPEG and WebP, materialized in memory without decoder dependencies.
    let jpeg = STANDARD
        .decode(
            "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AVN//2Q==",
        )
        .unwrap();
    let webp = STANDARD
        .decode("UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAgA0JaQAA3AA/vuUAAA=")
        .unwrap();
    [
        ("image/png", png),
        ("image/jpeg", jpeg),
        ("image/webp", webp),
    ]
}

fn chunk(png: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
    png.extend_from_slice(&(data.len() as u32).to_be_bytes());
    png.extend_from_slice(kind);
    png.extend_from_slice(data);
    let mut crc = u32::MAX;
    for byte in kind.iter().chain(data) {
        crc ^= *byte as u32;
        for _ in 0..8 {
            crc = (crc >> 1) ^ (0xedb88320 * (crc & 1));
        }
    }
    png.extend_from_slice(&(!crc).to_be_bytes());
}

pub(in crate::tools) fn unsupported() -> Vec<Vec<u8>> {
    let png = &fixtures()[0].1;
    let mut animated = png[..33].to_vec();
    chunk(&mut animated, b"acTL", &[0, 0, 0, 1, 0, 0, 0, 0]);
    animated.extend_from_slice(&png[33..]);
    let mut bad_png = png.clone();
    bad_png[12] |= 0x80;
    let mut bad_webp = fixtures()[2].1.clone();
    bad_webp[8] |= 0x80;
    let mut bad_length = png.clone();
    bad_length[33..37].copy_from_slice(&u32::MAX.to_be_bytes());
    vec![
        vec![],
        b"not an image".to_vec(),
        b"GIF89a\x01\0\x01\0".to_vec(),
        b"BM\0\0\0\0".to_vec(),
        b"\xff\xd8\xff\xf7".to_vec(),
        PNG_SIGNATURE.to_vec(),
        png[..20].to_vec(),
        animated,
        bad_png,
        bad_webp,
        bad_length,
    ]
}

#[test]
fn image_blocks_normalize_all_formats_and_serialize_without_paths() {
    let directory = crate::test_os::tempdir();
    for (mime, bytes) in fixtures() {
        let encoded = STANDARD.encode(&bytes);
        let block = json!({"type":"image","data":encoded,"mimeType":mime});
        let file = directory.path().join("nested/image.wrong-extension");
        fs::create_dir_all(file.parent().unwrap()).unwrap();
        fs::write(&file, &bytes).unwrap();
        for source in [
            block.clone(),
            json!({"type":"image","data":encoded.trim_end_matches('='),"mimeType":mime}),
            json!({"type":"image","path":"nested/image.wrong-extension","mimeType":mime}),
            json!({"type":"image","path":crate::test_os::path_text(&file),"mimeType":mime}),
        ] {
            let result = result::parse(
                &serde_json::to_vec(&json!({"content":[source]})).unwrap(),
                directory.path(),
            )
            .unwrap();
            assert_eq!(
                serde_json::to_value(&result).unwrap(),
                json!({"content":[block]})
            );
            let roundtrip =
                serde_json::from_value::<crate::tools::ToolResult>(json!({"content":[block]}))
                    .unwrap();
            assert_eq!(result, roundtrip);
        }
    }
}

#[test]
fn image_validation_rejects_unsupported_signatures_mime_and_base64() {
    for bytes in unsupported() {
        assert!(normalize(&bytes, None).is_err());
        for mime in ["image/png", "image/jpeg", "image/webp"] {
            assert!(from_base64(&STANDARD.encode(&bytes), mime).is_err());
        }
    }
    for (mime, bytes) in fixtures() {
        for other in [
            "image/png",
            "image/jpeg",
            "image/webp",
            "image/gif",
            "IMAGE/PNG",
            "",
        ] {
            if other != mime {
                assert!(from_base64(&STANDARD.encode(&bytes), other).is_err());
            }
        }
    }
    for invalid in [
        "",
        "=",
        "%%%",
        "abcd===",
        "data:image/png;base64,aGVsbG8=",
        " aGVsbG8=",
    ] {
        assert!(from_base64(invalid, "image/png").is_err());
    }
    let output = crate::test_os::tempdir();
    let encoded = STANDARD.encode(&fixtures()[0].1);
    for block in [
        json!({"type":"image","mimeType":"image/png"}),
        json!({"type":"image","mimeType":"image/png","data":encoded,"path":"file"}),
        json!({"type":"image","mimeType":"image/png","data":encoded,"extra":1}),
        json!({"type":"image","mimeType":"image/png","data":42}),
    ] {
        assert!(
            result::parse(
                &serde_json::to_vec(&json!({"content":[block]})).unwrap(),
                output.path()
            )
            .is_err()
        );
    }
    assert!(
        result::parse(
            &serde_json::to_vec(&json!({
                "isError":true,
                "content":[{"type":"image","mimeType":"image/png","data":encoded}],
            }))
            .unwrap(),
            output.path()
        )
        .is_err()
    );
}

#[test]
fn decoded_limit_is_inclusive_and_normalized_result_size_is_bounded() {
    let output = crate::test_os::tempdir();
    let mut bytes = fixtures()[1].1.clone();
    bytes.resize(IMAGE_BYTES, 0);
    let file = output.path().join("large");
    fs::write(&file, &bytes).unwrap();
    assert!(from_base64(&STANDARD.encode(&bytes), "image/jpeg").is_ok());
    assert!(from_output(output.path(), "large", "image/jpeg").is_ok());
    let block = json!({"type":"image","mimeType":"image/jpeg","path":"large"});
    assert!(
        result::parse(
            &serde_json::to_vec(&json!({"content":[block.clone(),block]})).unwrap(),
            output.path()
        )
        .is_err()
    );
    bytes.push(0);
    assert!(from_base64(&STANDARD.encode(&bytes), "image/jpeg").is_err());
    fs::write(&file, &bytes).unwrap();
    assert!(from_output(output.path(), "large", "image/jpeg").is_err());
    bytes.resize(IMAGE_BYTES + 3, 0);
    assert!(from_base64(&STANDARD.encode(&bytes), "image/jpeg").is_err());
}

#[test]
fn output_paths_reject_traversal_symlinks_and_nonregular_files() {
    let directory = crate::test_os::tempdir();
    let output = directory.path().join("output");
    fs::create_dir_all(output.join("nested")).unwrap();
    let bytes = &fixtures()[0].1;
    fs::write(output.join("image"), bytes).unwrap();
    fs::write(directory.path().join("outside"), bytes).unwrap();
    fs::write(output.join("nested/image"), bytes).unwrap();
    let outside = crate::test_os::path_text(&directory.path().join("outside"));
    let other = crate::test_os::path_text(&directory.path().join("output-other/image"));
    let mut paths = vec![
        "../outside",
        "nested/../../outside",
        "nested/../image",
        "nested",
        "",
        "./image",
        "nested\\image",
        "missing",
        outside.as_str(),
        other.as_str(),
    ];
    if symlink_file("image", output.join("link")).is_some() {
        paths.push("link");
    }
    if symlink_dir("nested", output.join("linkdir")).is_some() {
        paths.push("linkdir/image");
    }
    if symlink_file(
        std::path::Path::new("..").join("outside"),
        output.join("escape"),
    )
    .is_some()
    {
        paths.push("escape");
    }
    let special_files = crate::test_os::special_files(&output);
    paths.extend(special_files.paths().iter().copied());
    crate::test_os::link_dir(&output.join("nested"), &output.join("joined"));
    paths.push("joined/image");
    for path in paths {
        assert!(from_output(&output, path, "image/png").is_err(), "{path}");
    }
    fs::rename(&output, directory.path().join("old-output")).unwrap();
    let symlinked = symlink_dir("old-output", &output).is_some();
    if symlinked {
        assert!(from_output(&output, "image", "image/png").is_err());
        crate::test_os::remove_link_dir(&output);
    }
    crate::test_os::link_dir(&directory.path().join("old-output"), &output);
    assert!(from_output(&output, "image", "image/png").is_err());
}