1use crate::controls::Profile;
2use crate::controls::root;
3use crate::de::{self, Error, ErrorKind, Obj, fail};
4use crate::digest::Digest;
5use crate::json::Value;
6use crate::model::{BranchRef, ObjectFormat, Oid, RepositoryIdentity};
7
8pub const EVALUATION_REQUEST_SCHEMA: &str = "amiss/scanner-evaluation-request/v1";
9pub const SNAPSHOT_REQUEST_SCHEMA: &str = "amiss/scanner-snapshot-request/v1";
10pub const CONTROLS_REQUEST_SCHEMA: &str = "amiss/scanner-controls-request/v1";
11
12pub const REQUEST_STREAM_BYTES: u64 = 16_777_216;
16
17pub const REPOSITORY_HANDLE_ORDINAL: i64 = 3;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum RequestMode {
23 CommitPair,
24 Index,
25}
26
27impl RequestMode {
28 #[must_use]
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::CommitPair => "commit-pair",
32 Self::Index => "index",
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
41pub struct EvaluationRequest {
42 pub profile: Profile,
43 pub mode: RequestMode,
44 pub object_format: ObjectFormat,
45 pub repository: Option<RepositoryIdentity>,
46 pub ref_name: Option<BranchRef>,
47 pub default_branch_ref: Option<BranchRef>,
48 pub base_commit: Oid,
49 pub candidate_commit: Option<Oid>,
50}
51
52impl EvaluationRequest {
53 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
58 let value = root(bytes)?;
59 let mut obj = Obj::new("$", value)?;
60 de::const_str(
61 &obj.field("schema"),
62 obj.take("schema")?,
63 EVALUATION_REQUEST_SCHEMA,
64 )?;
65 let profile = Profile::decode(&obj.field("profile"), obj.take("profile")?)?;
66 let mode_path = obj.field("mode");
67 let mode = match de::string(&mode_path, obj.take("mode")?)?.as_str() {
68 "commit-pair" => RequestMode::CommitPair,
69 "index" => RequestMode::Index,
70 _ => return fail(&mode_path, ErrorKind::InvalidValue),
71 };
72 let format_path = obj.field("object_format");
73 let object_format = match de::string(&format_path, obj.take("object_format")?)?.as_str() {
74 "sha1" => ObjectFormat::Sha1,
75 "sha256" => ObjectFormat::Sha256,
76 _ => return fail(&format_path, ErrorKind::InvalidValue),
77 };
78 let repository_path = obj.field("repository");
79 let repository = match de::nullable(obj.take("repository")?) {
80 None => None,
81 Some(value) => Some(crate::controls::decode_repository(&repository_path, value)?),
82 };
83 let ref_path = obj.field("ref");
84 let ref_name = match de::nullable(obj.take("ref")?) {
85 None => None,
86 Some(value) => Some(decode_ref(&ref_path, value)?),
87 };
88 let default_path = obj.field("default_branch_ref");
89 let default_branch_ref = match de::nullable(obj.take("default_branch_ref")?) {
90 None => None,
91 Some(value) => Some(decode_ref(&default_path, value)?),
92 };
93 let base_path = obj.field("base_commit_oid");
94 let base_commit = Oid::new(
95 object_format,
96 de::string(&base_path, obj.take("base_commit_oid")?)?,
97 )
98 .ok_or_else(|| Error::new(&base_path, ErrorKind::InvalidValue))?;
99 let candidate_path = obj.field("candidate_commit_oid");
100 let candidate_commit = match de::nullable(obj.take("candidate_commit_oid")?) {
101 None => None,
102 Some(value) => Some(
103 Oid::new(object_format, de::string(&candidate_path, value)?)
104 .ok_or_else(|| Error::new(&candidate_path, ErrorKind::InvalidValue))?,
105 ),
106 };
107 obj.finish()?;
108 let consistent = match mode {
109 RequestMode::CommitPair => candidate_commit.is_some(),
110 RequestMode::Index => candidate_commit.is_none(),
111 };
112 if !consistent {
113 return fail(&candidate_path, ErrorKind::Inconsistent);
114 }
115 Ok(Self {
116 profile,
117 mode,
118 object_format,
119 repository,
120 ref_name,
121 default_branch_ref,
122 base_commit,
123 candidate_commit,
124 })
125 }
126}
127
128fn decode_ref(path: &str, value: Value) -> Result<BranchRef, Error> {
129 BranchRef::new(de::string(path, value)?)
130 .ok_or_else(|| Error::new(path, ErrorKind::InvalidValue))
131}
132
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub struct SnapshotRequest {
138 pub materialization: RequestMode,
139}
140
141impl SnapshotRequest {
142 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
147 let value = root(bytes)?;
148 let mut obj = Obj::new("$", value)?;
149 de::const_str(
150 &obj.field("schema"),
151 obj.take("schema")?,
152 SNAPSHOT_REQUEST_SCHEMA,
153 )?;
154 let materialization_path = obj.field("materialization");
155 let materialization =
156 match de::string(&materialization_path, obj.take("materialization")?)?.as_str() {
157 "git-objects" => RequestMode::CommitPair,
158 "index" => RequestMode::Index,
159 _ => return fail(&materialization_path, ErrorKind::InvalidValue),
160 };
161 let handle_path = obj.field("repository_handle");
162 if de::integer(&handle_path, obj.take("repository_handle")?)? != REPOSITORY_HANDLE_ORDINAL {
163 return fail(&handle_path, ErrorKind::InvalidValue);
164 }
165 let acquired_path = obj.field("pre_acquired");
166 if obj.take("pre_acquired")? != Value::Bool(true) {
167 return fail(&acquired_path, ErrorKind::InvalidValue);
168 }
169 obj.finish()?;
170 Ok(Self { materialization })
171 }
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct SuppliedControl {
179 pub value: Value,
180 pub expected_digest: Digest,
181 pub trust_source: RequestTrust,
182}
183
184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
185pub enum RequestTrust {
186 ExternalRequiredWorkflow,
187 OrganizationRuleset,
188}
189
190impl RequestTrust {
191 #[must_use]
192 pub const fn as_str(self) -> &'static str {
193 match self {
194 Self::ExternalRequiredWorkflow => "external-required-workflow",
195 Self::OrganizationRuleset => "organization-ruleset",
196 }
197 }
198
199 fn decode(path: &str, value: Value) -> Result<Self, Error> {
200 match de::string(path, value)?.as_str() {
201 "external-required-workflow" => Ok(Self::ExternalRequiredWorkflow),
202 "organization-ruleset" => Ok(Self::OrganizationRuleset),
203 _ => fail(path, ErrorKind::InvalidValue),
204 }
205 }
206}
207
208#[derive(Clone, Debug, PartialEq, Eq)]
211pub struct SuppliedTime {
212 pub value: Value,
213 pub expected_digest: Digest,
214 pub provider_run_id: String,
215 pub provider_run_attempt: u64,
216}
217
218#[derive(Clone, Debug, Default, PartialEq, Eq)]
220pub struct ControlsRequest {
221 pub organization_floor: Option<SuppliedControl>,
222 pub debt_snapshot: Option<SuppliedControl>,
223 pub waiver_bundle: Option<SuppliedControl>,
224 pub trusted_time: Option<SuppliedTime>,
225 pub execution_constraint: Option<SuppliedControl>,
226}
227
228impl ControlsRequest {
229 pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
235 let value = root(bytes)?;
236 let mut obj = Obj::new("$", value)?;
237 de::const_str(
238 &obj.field("schema"),
239 obj.take("schema")?,
240 CONTROLS_REQUEST_SCHEMA,
241 )?;
242 let organization_floor = decode_supplied(
243 &obj.field("organization_floor"),
244 obj.take("organization_floor")?,
245 )?;
246 let debt_snapshot =
247 decode_supplied(&obj.field("debt_snapshot"), obj.take("debt_snapshot")?)?;
248 let waiver_bundle =
249 decode_supplied(&obj.field("waiver_bundle"), obj.take("waiver_bundle")?)?;
250 let trusted_time = decode_time(&obj.field("trusted_time"), obj.take("trusted_time")?)?;
251 let execution_constraint = decode_supplied(
252 &obj.field("execution_constraint"),
253 obj.take("execution_constraint")?,
254 )?;
255 obj.finish()?;
256 Ok(Self {
257 organization_floor,
258 debt_snapshot,
259 waiver_bundle,
260 trusted_time,
261 execution_constraint,
262 })
263 }
264}
265
266fn embedded_value(path: &str, value: Value) -> Result<Value, Error> {
267 match value {
268 Value::Object(_) => Ok(value),
269 Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
270 fail(path, ErrorKind::WrongType)
271 }
272 }
273}
274
275fn decode_supplied(path: &str, value: Value) -> Result<Option<SuppliedControl>, Error> {
276 let Some(value) = de::nullable(value) else {
277 return Ok(None);
278 };
279 let mut obj = Obj::new(path, value)?;
280 let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
281 let digest_path = obj.field("expected_digest");
282 let expected_digest =
283 Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
284 .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
285 let trust_source = RequestTrust::decode(&obj.field("trust_source"), obj.take("trust_source")?)?;
286 obj.finish()?;
287 Ok(Some(SuppliedControl {
288 value: embedded,
289 expected_digest,
290 trust_source,
291 }))
292}
293
294fn decode_time(path: &str, value: Value) -> Result<Option<SuppliedTime>, Error> {
295 let Some(value) = de::nullable(value) else {
296 return Ok(None);
297 };
298 let mut obj = Obj::new(path, value)?;
299 let embedded = embedded_value(&obj.field("value"), obj.take("value")?)?;
300 let digest_path = obj.field("expected_digest");
301 let expected_digest =
302 Digest::from_wire(&de::string(&digest_path, obj.take("expected_digest")?)?)
303 .ok_or_else(|| Error::new(&digest_path, ErrorKind::InvalidValue))?;
304 let run_id_path = obj.field("provider_run_id");
305 let provider_run_id = de::string(&run_id_path, obj.take("provider_run_id")?)?;
306 let run_id_bytes = provider_run_id.as_bytes();
307 if run_id_bytes.is_empty()
308 || run_id_bytes.len() > 32
309 || !matches!(run_id_bytes.first(), Some(b'1'..=b'9'))
310 || !run_id_bytes.iter().all(u8::is_ascii_digit)
311 {
312 return fail(&run_id_path, ErrorKind::InvalidValue);
313 }
314 let attempt_path = obj.field("provider_run_attempt");
315 let attempt_raw = de::integer(&attempt_path, obj.take("provider_run_attempt")?)?;
316 let provider_run_attempt = u64::try_from(attempt_raw)
317 .ok()
318 .filter(|attempt| *attempt >= 1)
319 .ok_or_else(|| Error::new(&attempt_path, ErrorKind::InvalidValue))?;
320 obj.finish()?;
321 Ok(Some(SuppliedTime {
322 value: embedded,
323 expected_digest,
324 provider_run_id,
325 provider_run_attempt,
326 }))
327}