monty-proto 1.0.0

A secure, snapshotable Python sandbox written in Rust.
Documentation
//! Regenerates the checked-in prost code from the `.proto` schema.
//!
//! Run via `make generate-proto`. Uses `protox` (a pure-Rust protobuf
//! compiler) so no `protoc` binary is required. All outputs are checked in
//! and diffed in CI by `make check-proto` so the schema and the
//! generated code can never drift:
//!
//! - `src/generated/monty.v1.rs` — the protocol messages, with the
//!   `monty.v1.Arena` message mapped via `extern_path` onto the hand-written
//!   [`WireArena`](../wire.rs) for borrowed encoding and generated node-by-node
//!   decoding into `monty_types::unstable::MontyGraph`. Extern-mapped index, pair and named-tuple
//!   containers decode reference buffers directly into their domain representation.
//! - `tests/oracle/monty.v1.rs` — the same schema *without* the mapping: a
//!   fully prost-generated mirror used only by `tests/differential.rs` to
//!   prove the hand-written implementation is byte-compatible with prost.
//! - `tests/oracle/repeated_fields.rs` — schema-derived inventory of repeated
//!   fields for allocation-budget tests, extended automatically on regeneration.

use std::{fmt::Write, fs, path::Path};

use heck::{ToSnakeCase, ToUpperCamelCase};
use protox::prost_reflect::{DescriptorPool, FieldDescriptor, Kind, MessageDescriptor};

/// Header prepended to the generated files: marks them as generated and
/// disables the workspace lints, which generated code cannot be expected to
/// satisfy. `allow` (not `expect`) is deliberate — which lints fire depends
/// on the prost-build version, so expectations would themselves churn.
const HEADER: &str = "\
// @generated by `make generate-proto` from proto/monty/v1/monty.proto — DO NOT EDIT.
#![allow(clippy::allow_attributes, clippy::pedantic, clippy::use_self, clippy::absolute_paths, missing_docs)]
";

/// Header for the test-only oracle: the differential tests exercise only the
/// value messages, so the rest of the schema is (expected) dead code.
const ORACLE_HEADER: &str = "\
// @generated by `make generate-proto` from proto/monty/v1/monty.proto — DO NOT EDIT.
#![allow(clippy::allow_attributes, clippy::pedantic, clippy::use_self, clippy::absolute_paths, missing_docs, dead_code)]
";

fn main() {
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let proto_dir = manifest_dir.join("proto");
    let proto_file = proto_dir.join("monty/v1/monty.proto");

    let descriptors = protox::compile([&proto_file], [&proto_dir]).expect("failed to compile monty.proto");
    let pool = DescriptorPool::from_file_descriptor_set(descriptors.clone()).expect("invalid schema");

    // protocol messages: Arena is the hand-written WireArena
    let out_dir = manifest_dir.join("src/generated");
    prost_build::Config::new()
        .out_dir(&out_dir)
        .prost_path("crate::budgeted_prost")
        .extern_path(".monty.v1.Arena", "crate::WireArena")
        .extern_path(".monty.v1.FunctionCall", "crate::WireFunctionCall")
        .extern_path(".monty.v1.Indexes", "crate::WireIndexes")
        .extern_path(".monty.v1.NodePairs", "crate::WireNodePairs")
        .extern_path(".monty.v1.NamedTupleNode", "crate::WireNamedTuple")
        .compile_fds(descriptors.clone())
        .expect("failed to generate Rust code from monty.proto");
    let generated = out_dir.join("monty.v1.rs");
    check_allocation_forms(&fs::read_to_string(&generated).expect("generated file missing"));
    prepend_header(&generated, HEADER);

    // differential-test oracle: the same schema, fully generated
    let oracle_dir = manifest_dir.join("tests/oracle");
    fs::create_dir_all(&oracle_dir).expect("failed to create tests/oracle");
    prost_build::Config::new()
        .out_dir(&oracle_dir)
        .compile_fds(descriptors)
        .expect("failed to generate oracle Rust code from monty.proto");
    prepend_header(&oracle_dir.join("monty.v1.rs"), ORACLE_HEADER);
    generate_repeated_tests(&pool, &oracle_dir.join("repeated_fields.rs"));
}

/// Generates an exhaustive fixture inventory: adding a repeated field extends
/// the decode-budget tests automatically, including fields on nested messages.
fn generate_repeated_tests(pool: &DescriptorPool, path: &Path) {
    let mut source = String::from(
        "// @generated by `make generate-proto` — DO NOT EDIT.\n\n/// Exercises every repeated field in the schema.\n#[test]\nfn every_schema_repeated_field_is_budgeted() {\n",
    );
    for message in pool.all_messages() {
        assert!(!message.is_map_entry(), "protobuf maps need a budgeted runtime adapter");
        for field in message.fields().filter(FieldDescriptor::is_list) {
            let (wire_type, payload) = match field.kind() {
                Kind::Message(element) => {
                    let payload: &[u8] = match element.full_name() {
                        // Arena nodes require a populated kind.
                        "monty.v1.MontyNode" => &[0x12, 0],
                        _ => &[],
                    };
                    ("LengthDelimited", payload)
                }
                Kind::Enum(_) => panic!(
                    "{}: prost's repeated-enum accessors require infallible push; extend budgeted_prost first",
                    field.full_name()
                ),
                Kind::String | Kind::Bytes => ("LengthDelimited", &[][..]),
                Kind::Float | Kind::Fixed32 | Kind::Sfixed32 => ("ThirtyTwoBit", &[0; 4][..]),
                Kind::Double | Kind::Fixed64 | Kind::Sfixed64 => ("SixtyFourBit", &[0; 8][..]),
                _ => ("Varint", &[0][..]),
            };
            writeln!(
                source,
                "    check_repeated(\n        \"{}\",\n        {},\n        WireType::{wire_type},\n        &{payload:?},\n        |message: &{}| &message.{},\n    );",
                field.full_name(),
                field.number(),
                message_rust_path(&message),
                if matches!(message.full_name(), "monty.v1.Arena" | "monty.v1.Indexes" | "monty.v1.NodePairs") {
                    "0".to_owned()
                } else {
                    field.name().to_snake_case()
                },
            )
            .expect("write to string");
        }
    }
    source.push_str("}\n");
    fs::write(path, source).expect("failed to write repeated field fixtures");
}

/// Resolves prost's nested modules and the protocol's extern-mapped messages.
fn message_rust_path(message: &MessageDescriptor) -> String {
    match message.full_name() {
        "monty.v1.Arena" => "WireArena".to_owned(),
        "monty.v1.FunctionCall" => "WireFunctionCall".to_owned(),
        "monty.v1.Indexes" => "WireIndexes".to_owned(),
        "monty.v1.NodePairs" => "WireNodePairs".to_owned(),
        "monty.v1.NamedTupleNode" => "WireNamedTuple".to_owned(),
        _ => {
            let mut parents = Vec::new();
            let mut parent = message.parent_message();
            while let Some(message) = parent {
                parents.push(message.name().to_snake_case());
                parent = message.parent_message();
            }
            parents.reverse();
            parents.insert(0, "pb".to_owned());
            parents.push(message.name().to_upper_camel_case());
            parents.join("::")
        }
    }
}

/// Fails closed if codegen introduces storage the budget adapter cannot handle.
/// These are prost's generated spellings; the adapter also omits their runtime
/// modules/types so bypassing this check still fails compilation.
fn check_allocation_forms(source: &str) {
    for unsupported in [
        "::boxed::",
        "::collections::",
        "::bytes::Bytes",
        "#[prost(group",
        "#[prost(map",
        "#[prost(btree_map",
    ] {
        assert!(
            !source.contains(unsupported),
            "unbudgeted protobuf allocation form {unsupported}: extend budgeted_prost before adding this field"
        );
    }
}

/// Prepends `header` to a freshly generated file and reports it.
fn prepend_header(generated: &Path, header: &str) {
    let body = fs::read_to_string(generated).expect("generated file missing");
    fs::write(generated, format!("{header}{body}")).expect("failed to write generated file");
    println!("regenerated {}", generated.display());
}