Skip to main content

amiss_wire/
requests.rs

1use crate::controls::{
2    Profile, decode_provider_id, decode_provider_run_id, decode_repository, root,
3};
4use crate::de::{self, Error, ErrorKind, Obj, fail};
5use crate::digest::Digest;
6use crate::json::Value;
7use crate::model::{BranchRef, ForgeDialect, ObjectFormat, Oid, RepositoryIdentity};
8
9pub const EVALUATION_REQUEST_SCHEMA: &str = "amiss/scanner-evaluation-request";
10pub const SNAPSHOT_REQUEST_SCHEMA: &str = "amiss/scanner-snapshot-request";
11pub const CONTROLS_REQUEST_SCHEMA: &str = "amiss/scanner-controls-request";
12
13/// Every request stream is one complete bounded byte capture from byte zero
14/// through EOF; its diagnostic digest exists exactly when EOF was obtained
15/// within this cap.
16pub const REQUEST_STREAM_BYTES: u64 = 16_777_216;
17
18/// The published handle table's repository ordinal, constant across the
19/// in-process and future subprocess lanes.
20pub const REPOSITORY_HANDLE_ORDINAL: i64 = 3;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum RequestMode {
24    CommitPair,
25    Index,
26}
27
28impl RequestMode {
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            Self::CommitPair => "commit-pair",
33            Self::Index => "index",
34        }
35    }
36}
37
38/// The run-identity request: profile, mode, and the exact snapshot
39/// identities to evaluate. The candidate commit is null exactly when the
40/// mode is `index`.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct EvaluationRequest {
43    pub profile: Profile,
44    pub mode: RequestMode,
45    pub object_format: ObjectFormat,
46    pub repository: Option<RepositoryIdentity>,
47    pub forge: Option<ForgeDialect>,
48    pub ref_name: Option<BranchRef>,
49    pub default_branch_ref: Option<BranchRef>,
50    pub base_commit: Oid,
51    pub candidate_commit: Option<Oid>,
52}
53
54impl EvaluationRequest {
55    /// # Errors
56    ///
57    /// Fails on strict-JSON defects, schema-shape violations, invalid
58    /// grammar values, and a candidate commit inconsistent with the mode.
59    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
60        let value = root(bytes)?;
61        let mut obj = Obj::new("$", value)?;
62        de::const_str(
63            &obj.field("schema"),
64            obj.take("schema")?,
65            EVALUATION_REQUEST_SCHEMA,
66        )?;
67        let profile = Profile::decode(&obj.field("profile"), obj.take("profile")?)?;
68        let mode_path = obj.field("mode");
69        let mode = match de::string(&mode_path, obj.take("mode")?)?.as_str() {
70            "commit-pair" => RequestMode::CommitPair,
71            "index" => RequestMode::Index,
72            _ => return fail(&mode_path, ErrorKind::InvalidValue),
73        };
74        let format_path = obj.field("object_format");
75        let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
76            "sha1" => ObjectFormat::Sha1,
77            "sha256" => ObjectFormat::Sha256,
78            _ => return fail(&format_path, ErrorKind::InvalidValue),
79        };
80        let repository_path = obj.field("repository");
81        let repository = match de::nullable(obj.take("repository")?) {
82            None => None,
83            Some(value) => Some(decode_repository(&repository_path, value)?),
84        };
85        let forge_path = obj.field("forge");
86        let forge = de::nullable(obj.take("forge")?)
87            .map(|value| decode_forge(&forge_path, value))
88            .transpose()?;
89        let ref_path = obj.field("ref");
90        let ref_name = match de::nullable(obj.take("ref")?) {
91            None => None,
92            Some(value) => Some(decode_ref(&ref_path, value)?),
93        };
94        let default_path = obj.field("default_branch_ref");
95        let default_branch_ref = match de::nullable(obj.take("default_branch_ref")?) {
96            None => None,
97            Some(value) => Some(decode_ref(&default_path, value)?),
98        };
99        let base_path = obj.field("base_commit_oid");
100        let base_commit = Oid::new(
101            object_format,
102            de::string(&base_path, obj.take("base_commit_oid")?)?,
103        )
104        .ok_or_else(|| Error::new(&base_path, ErrorKind::InvalidValue))?;
105        let candidate_path = obj.field("candidate_commit_oid");
106        let candidate_commit = match de::nullable(obj.take("candidate_commit_oid")?) {
107            None => None,
108            Some(value) => Some(
109                Oid::new(object_format, de::string(&candidate_path, value)?)
110                    .ok_or_else(|| Error::new(&candidate_path, ErrorKind::InvalidValue))?,
111            ),
112        };
113        obj.finish()?;
114        let consistent = match mode {
115            RequestMode::CommitPair => candidate_commit.is_some(),
116            RequestMode::Index => candidate_commit.is_none(),
117        };
118        if !consistent {
119            return fail(&candidate_path, ErrorKind::Inconsistent);
120        }
121        if forge.is_some() && repository.is_none()
122            || matches!(forge, Some(ForgeDialect::Github | ForgeDialect::Gitea))
123                && repository
124                    .as_ref()
125                    .is_some_and(|identity| identity.owner.contains('/'))
126        {
127            return fail(&forge_path, ErrorKind::Inconsistent);
128        }
129        Ok(Self {
130            profile,
131            mode,
132            object_format,
133            repository,
134            forge,
135            ref_name,
136            default_branch_ref,
137            base_commit,
138            candidate_commit,
139        })
140    }
141}
142
143fn decode_forge(path: &str, value: Value) -> Result<ForgeDialect, Error> {
144    let raw = de::string(path, value)?;
145    raw.parse()
146        .map_err(|_unknown| Error::new(path, ErrorKind::InvalidValue))
147}
148
149fn decode_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
150    BranchRef::new(de::string(path, value)?)
151        .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
152}
153
154/// The materialization request. `git-objects` pairs with mode `commit-pair`
155/// and `index` with mode `index`; the pairing law is checked against the
156/// evaluation request by the consumer, since each request parses alone.
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct SnapshotRequest {
159    pub materialization: RequestMode,
160}
161
162impl SnapshotRequest {
163    /// # Errors
164    ///
165    /// Fails on strict-JSON defects, schema-shape violations, and invalid
166    /// grammar values.
167    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
168        let value = root(bytes)?;
169        let mut obj = Obj::new("$", value)?;
170        de::const_str(
171            &obj.field("schema"),
172            obj.take("schema")?,
173            SNAPSHOT_REQUEST_SCHEMA,
174        )?;
175        let materialization_path = obj.field("materialization");
176        let materialization =
177            match de::string(&materialization_path, obj.take("materialization")?)?.as_str() {
178                "git-objects" => RequestMode::CommitPair,
179                "index" => RequestMode::Index,
180                _ => return fail(&materialization_path, ErrorKind::InvalidValue),
181            };
182        let handle_path = obj.field("repository_handle");
183        if de::integer(&handle_path, obj.take("repository_handle")?)? != REPOSITORY_HANDLE_ORDINAL {
184            return fail(&handle_path, ErrorKind::InvalidValue);
185        }
186        let acquired_path = obj.field("pre_acquired");
187        if obj.take("pre_acquired")? != Value::Bool(true) {
188            return fail(&acquired_path, ErrorKind::InvalidValue);
189        }
190        obj.finish()?;
191        Ok(Self { materialization })
192    }
193}
194
195/// One supplied external control: the exact embedded JSON value, the
196/// independently acquired expected semantic digest, and the external trust
197/// source that authorized it.
198#[derive(Clone, Debug, PartialEq, Eq)]
199pub struct SuppliedControl {
200    pub value: Value,
201    pub expected_digest: Digest,
202    pub trust_source: RequestTrust,
203}
204
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub enum RequestTrust {
207    ExternalRequiredCheck,
208    OrganizationPolicy,
209}
210
211impl RequestTrust {
212    #[must_use]
213    pub const fn as_str(self) -> &'static str {
214        match self {
215            Self::ExternalRequiredCheck => "external-required-check",
216            Self::OrganizationPolicy => "organization-policy",
217        }
218    }
219
220    fn decode(path: &str, value: Value) -> Result<Self, Error> {
221        match de::string(path, value)?.as_str() {
222            "external-required-check" => Ok(Self::ExternalRequiredCheck),
223            "organization-policy" => Ok(Self::OrganizationPolicy),
224            _ => fail(path, ErrorKind::InvalidValue),
225        }
226    }
227}
228
229/// The supplied trusted-time statement with the provider-authenticated run
230/// context the statement must identify. Its trust source is fixed.
231#[derive(Clone, Debug, PartialEq, Eq)]
232pub struct SuppliedTime {
233    pub value: Value,
234    pub expected_digest: Digest,
235    pub provider: String,
236    pub provider_run_id: String,
237    pub provider_run_attempt: u64,
238}
239
240/// The external-control request: five nullable supplied controls.
241#[derive(Clone, Debug, Default, PartialEq, Eq)]
242pub struct ControlsRequest {
243    pub organization_floor: Option<SuppliedControl>,
244    pub debt_snapshot: Option<SuppliedControl>,
245    pub waiver_bundle: Option<SuppliedControl>,
246    pub trusted_time: Option<SuppliedTime>,
247    pub execution_constraint: Option<SuppliedControl>,
248}
249
250impl ControlsRequest {
251    /// # Errors
252    ///
253    /// Fails on strict-JSON defects, schema-shape violations, and invalid
254    /// grammar values. Embedded control values are shape-checked as objects
255    /// only; their own schemas and digests are the consumer's verification.
256    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
257        let value = root(bytes)?;
258        let mut obj = Obj::new("$", value)?;
259        de::const_str(
260            &obj.field("schema"),
261            obj.take("schema")?,
262            CONTROLS_REQUEST_SCHEMA,
263        )?;
264        let organization_floor = decode_supplied(
265            &obj.field("organization_floor"),
266            obj.take("organization_floor")?,
267        )?;
268        let debt_snapshot =
269            decode_supplied(&obj.field("debt_snapshot"), obj.take("debt_snapshot")?)?;
270        let waiver_bundle =
271            decode_supplied(&obj.field("waiver_bundle"), obj.take("waiver_bundle")?)?;
272        let trusted_time = decode_time(&obj.field("trusted_time"), obj.take("trusted_time")?)?;
273        let execution_constraint = decode_supplied(
274            &obj.field("execution_constraint"),
275            obj.take("execution_constraint")?,
276        )?;
277        obj.finish()?;
278        Ok(Self {
279            organization_floor,
280            debt_snapshot,
281            waiver_bundle,
282            trusted_time,
283            execution_constraint,
284        })
285    }
286}
287
288fn embedded_value(path: &str, value: Value) -> Result<Value, Error> {
289    match value {
290        Value::Object(_) => Ok(value),
291        Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
292            fail(path, ErrorKind::WrongType)
293        }
294    }
295}
296
297fn decode_supplied(path: &str, value: Value) -> Result<Option<SuppliedControl>, Error> {
298    let Some(value) = de::nullable(value) else {
299        return Ok(None);
300    };
301    let mut obj = Obj::new(path, value)?;
302    let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
303    let digest_path = obj.field("expected_digest");
304    let expected_digest =
305        Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
306            .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
307    let trust_source = RequestTrust::decode(&obj.field("trust_source"), obj.take("trust_source")?)?;
308    obj.finish()?;
309    Ok(Some(SuppliedControl {
310        value: embedded,
311        expected_digest,
312        trust_source,
313    }))
314}
315
316fn decode_time(path: &str, value: Value) -> Result<Option<SuppliedTime>, Error> {
317    let Some(value) = de::nullable(value) else {
318        return Ok(None);
319    };
320    let mut obj = Obj::new(path, value)?;
321    let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
322    let digest_path = obj.field("expected_digest");
323    let expected_digest =
324        Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
325            .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
326    let provider = decode_provider_id(&obj.field("provider"), obj.take("provider")?)?;
327    let run_id_path = obj.field("provider_run_id");
328    let provider_run_id = decode_provider_run_id(&run_id_path, obj.take("provider_run_id")?)?;
329    let attempt_path = obj.field("provider_run_attempt");
330    let attempt_raw = de::integer(&attempt_path, obj.take("provider_run_attempt")?)?;
331    let provider_run_attempt = u64::try_from(attempt_raw)
332        .ok()
333        .filter(|attempt| *attempt >= 1)
334        .ok_or_else(|| Error::new(&attempt_path, ErrorKind::InvalidValue))?;
335    obj.finish()?;
336    Ok(Some(SuppliedTime {
337        value: embedded,
338        expected_digest,
339        provider,
340        provider_run_id,
341        provider_run_attempt,
342    }))
343}