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
13pub const REQUEST_STREAM_BYTES: u64 = 16_777_216;
17
18pub 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#[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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub struct SnapshotRequest {
159 pub materialization: RequestMode,
160}
161
162impl SnapshotRequest {
163 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#[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#[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#[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 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}