Skip to main content

heddle_api/
transport.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use crate::heddle::api::common::{
4    AuthorizationAccess, CallContext, DeploymentTarget, ErrorDetail, ErrorReason,
5    HumanVerificationChallenge, RetryBehavior, RpcEffect, ServiceMaturity, SigningTier,
6    error_detail,
7};
8
9/// Production ALPN for the first transport-neutral hosted-call protocol.
10pub const HOSTED_ALPN_V1: &[u8] = b"heddle-api/1";
11
12/// Production ALPN for an opaque-ticket provider extent transfer.
13pub const PROVIDER_ALPN_V1: &[u8] = b"heddle-provider/1";
14
15/// Build an `ErrorDetail` carrying a human-verification challenge (policy-denied
16/// with an actionable challenge), for the `ErrorDetail.human_verification` arm.
17pub fn human_verification_error_detail(challenge: HumanVerificationChallenge) -> ErrorDetail {
18    ErrorDetail {
19        reason: ErrorReason::PolicyDenied as i32,
20        resource: String::new(),
21        field: String::new(),
22        context: Some(error_detail::Context::HumanVerification(challenge)),
23    }
24}
25
26/// Extract a human-verification challenge from an `ErrorDetail`, if present.
27pub fn human_verification_challenge(detail: &ErrorDetail) -> Option<HumanVerificationChallenge> {
28    match &detail.context {
29        Some(error_detail::Context::HumanVerification(challenge)) => Some(challenge.clone()),
30        _ => None,
31    }
32}
33
34/// Message cardinality on each side of a contract method.
35#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
36pub enum StreamingShape {
37    /// One request and one response.
38    Unary,
39    /// A stream of requests and one response.
40    ClientStreaming,
41    /// One request and a stream of responses.
42    ServerStreaming,
43    /// Streams in both directions.
44    Bidirectional,
45}
46
47include!(concat!(env!("OUT_DIR"), "/heddle_api_methods.rs"));
48
49impl MethodDescriptor {
50    /// Whether the contract permits this method on a replayable 0-RTT path.
51    pub const fn allows_zero_rtt(&self) -> bool {
52        matches!(self.effect, RpcEffect::ReadOnly)
53            && matches!(self.retry_behavior, RetryBehavior::Safe)
54    }
55
56    /// Extracts the request's declared `client_operation_id`, when its input
57    /// message has that field. The generated field number keeps clients and
58    /// producers from maintaining handwritten per-route catalogs.
59    pub fn client_operation_id<'a>(
60        &self,
61        request: &'a [u8],
62    ) -> Result<Option<&'a str>, RequestMetadataError> {
63        let Some(field_number) = self.client_operation_id_field_number else {
64            return Ok(None);
65        };
66        protobuf_string_field(request, field_number)
67    }
68}
69
70/// Malformed protobuf while extracting transport-level request metadata.
71#[derive(Debug, thiserror::Error)]
72#[error("invalid request metadata protobuf: {0}")]
73pub struct RequestMetadataError(&'static str);
74
75pub(crate) fn protobuf_string_field(
76    mut request: &[u8],
77    target_field: u32,
78) -> Result<Option<&str>, RequestMetadataError> {
79    let mut operation_id = None;
80    while !request.is_empty() {
81        let key = take_varint(&mut request)?;
82        let field = u32::try_from(key >> 3).map_err(|_| RequestMetadataError("field overflow"))?;
83        let wire = (key & 0x07) as u8;
84        if field == 0 || field > 0x1fff_ffff {
85            return Err(RequestMetadataError("invalid field number"));
86        }
87        if field == target_field && wire != 2 {
88            return Err(RequestMetadataError("operation id has wrong wire type"));
89        }
90        match wire {
91            0 => {
92                let _ = take_varint(&mut request)?;
93            }
94            1 => {
95                let _ = take_bytes(&mut request, 8)?;
96            }
97            2 => {
98                let length = usize::try_from(take_varint(&mut request)?)
99                    .map_err(|_| RequestMetadataError("length overflow"))?;
100                let value = take_bytes(&mut request, length)?;
101                if field == target_field {
102                    if operation_id.is_some() {
103                        return Err(RequestMetadataError("duplicate operation id field"));
104                    }
105                    operation_id = Some(
106                        std::str::from_utf8(value)
107                            .map_err(|_| RequestMetadataError("operation id is not UTF-8"))?,
108                    );
109                }
110            }
111            5 => {
112                let _ = take_bytes(&mut request, 4)?;
113            }
114            _ => return Err(RequestMetadataError("unsupported wire type")),
115        }
116    }
117    Ok(operation_id)
118}
119
120fn take_varint(input: &mut &[u8]) -> Result<u64, RequestMetadataError> {
121    let mut value = 0_u64;
122    for shift in (0..70).step_by(7) {
123        let (&byte, rest) = input
124            .split_first()
125            .ok_or(RequestMetadataError("truncated varint"))?;
126        *input = rest;
127        if shift == 63 && byte > 1 {
128            return Err(RequestMetadataError("varint overflow"));
129        }
130        value |= u64::from(byte & 0x7f) << shift;
131        if byte & 0x80 == 0 {
132            return Ok(value);
133        }
134    }
135    Err(RequestMetadataError("varint overflow"))
136}
137
138fn take_bytes<'a>(input: &mut &'a [u8], length: usize) -> Result<&'a [u8], RequestMetadataError> {
139    if input.len() < length {
140        return Err(RequestMetadataError("truncated field"));
141    }
142    let (value, rest) = input.split_at(length);
143    *input = rest;
144    Ok(value)
145}
146
147/// Transport-neutral information passed from an operation-stream decoder to a
148/// contract router before the request body is decoded.
149#[derive(Debug)]
150pub struct RoutedCall<'a> {
151    /// Generated contract descriptor selected by the fully-qualified path.
152    pub method: &'static MethodDescriptor,
153    /// Typed authentication, deadline, idempotency, and trace fields.
154    pub context: &'a CallContext,
155}
156
157impl<'a> RoutedCall<'a> {
158    /// Selects a generated route or returns `None` for an unknown method path.
159    pub fn new(path: &str, context: &'a CallContext) -> Option<Self> {
160        method_descriptor(path).map(|method| Self { method, context })
161    }
162}