1use amiss_wire::controls::ConstraintPlatform;
2use amiss_wire::digest::{Digest, RAW_EVIDENCE_DOMAIN, hb, hj, sha256};
3use amiss_wire::json::{Value, canonical};
4use amiss_wire::manifest::{
5 DEPENDENCY_LOCK_DOMAIN, DEPENDENCY_LOCK_SCHEMA, ENVIRONMENT_CONTRACT, MANIFEST_DOMAIN,
6 MANIFEST_SCHEMA, RUNTIME_CONTRACT, RuntimeRole,
7};
8
9use crate::ENGINE_DOMAIN;
10
11pub const RELEASE_MANIFEST_PATH: &str = "release-manifest.json";
12pub const RELEASE_MANIFEST_DIGEST_PATH: &str = "release-manifest.digest";
13
14pub struct StagedFile<'bytes> {
17 pub path: String,
18 pub role: RuntimeRole,
19 pub executable: bool,
20 pub bytes: &'bytes [u8],
21}
22
23pub struct StagedArtifact<'bytes> {
27 pub platform: ConstraintPlatform,
28 pub artifact_name: String,
29 pub files: Vec<StagedFile<'bytes>>,
30}
31
32pub struct StagedBuild<'bytes> {
34 pub engine_version: String,
35 pub host: String,
36 pub owner: String,
37 pub repository: String,
38 pub object_format: &'static str,
39 pub commit_oid: String,
40 pub locks: Vec<(String, &'bytes [u8])>,
41}
42
43pub fn build_manifest(
55 build: &StagedBuild<'_>,
56 artifacts: &mut [StagedArtifact<'_>],
57) -> Result<(Vec<u8>, Digest), &'static str> {
58 let mut locks: Vec<(String, Digest)> = build
59 .locks
60 .iter()
61 .map(|(path, bytes)| (path.clone(), hb(RAW_EVIDENCE_DOMAIN, bytes)))
62 .collect();
63 locks.sort_by(|a, b| a.0.cmp(&b.0));
64 let lock_value = object(vec![
65 ("schema", string(DEPENDENCY_LOCK_SCHEMA)),
66 (
67 "files",
68 Value::Array(
69 locks
70 .iter()
71 .map(|(path, digest)| {
72 object(vec![
73 ("path", string(path)),
74 ("raw_digest", string(&digest.to_string())),
75 ])
76 })
77 .collect(),
78 ),
79 ),
80 ]);
81 let lock_digest = hj(DEPENDENCY_LOCK_DOMAIN, &lock_value);
82
83 artifacts.sort_by_key(|artifact| artifact.platform.as_str());
84 let mut rows: Vec<Value> = Vec::with_capacity(artifacts.len());
85 for artifact in artifacts.iter_mut() {
86 rows.push(artifact_value(artifact)?);
87 }
88
89 let manifest = object(vec![
90 ("schema", string(MANIFEST_SCHEMA)),
91 ("engine_version", string(&build.engine_version)),
92 (
93 "build_source",
94 object(vec![
95 (
96 "repository",
97 object(vec![
98 ("host", string(&build.host)),
99 ("owner", string(&build.owner)),
100 ("name", string(&build.repository)),
101 ]),
102 ),
103 ("object_format", string(build.object_format)),
104 ("commit_oid", string(&build.commit_oid)),
105 ]),
106 ),
107 ("dependency_lock", lock_value),
108 ("dependency_lock_digest", string(&lock_digest.to_string())),
109 ("artifacts", Value::Array(rows)),
110 ]);
111 let digest = hj(MANIFEST_DOMAIN, &manifest);
112 let mut bytes = canonical(&manifest);
113 bytes.push(b'\n');
114 Ok((bytes, digest))
115}
116
117fn artifact_value(artifact: &mut StagedArtifact<'_>) -> Result<Value, &'static str> {
118 artifact.files.sort_by(|a, b| a.path.cmp(&b.path));
119 let mut executables = artifact
120 .files
121 .iter()
122 .filter(|file| file.role == RuntimeRole::Executable);
123 let engine = executables.next().ok_or("no executable row")?;
124 if executables.next().is_some() {
125 return Err("more than one executable row");
126 }
127 if !engine.executable {
128 return Err("the executable row is not mode 100755");
129 }
130 let mut launchers = artifact
131 .files
132 .iter()
133 .filter(|file| file.role == RuntimeRole::Launcher);
134 let launcher = launchers.next().ok_or("no launcher row")?;
135 if launchers.next().is_some() {
136 return Err("more than one launcher row");
137 }
138 if launcher.executable {
139 return Err("the launcher row is not mode 100644");
140 }
141 let binary_sha256 = sha256(engine.bytes);
142 let engine_digest = hb(ENGINE_DOMAIN, engine.bytes);
143 let tree_path = engine.path.clone();
144
145 let files: Vec<Value> = artifact
146 .files
147 .iter()
148 .map(|file| {
149 object(vec![
150 ("path", string(&file.path)),
151 ("role", string(file.role.as_str())),
152 (
153 "git_mode",
154 string(if file.executable { "100755" } else { "100644" }),
155 ),
156 ("file_sha256", string(&sha256(file.bytes).to_string())),
157 ])
158 })
159 .collect();
160
161 Ok(object(vec![
162 ("platform", string(artifact.platform.as_str())),
163 ("artifact_name", string(&artifact.artifact_name)),
164 ("tree_path", string(&tree_path)),
165 ("binary_sha256", string(&binary_sha256.to_string())),
166 ("engine_digest", string(&engine_digest.to_string())),
167 ("runtime_contract", string(RUNTIME_CONTRACT)),
168 ("environment_contract", string(ENVIRONMENT_CONTRACT)),
169 ("runtime_files", Value::Array(files)),
170 ]))
171}
172
173#[must_use]
177fn string(text: &str) -> Value {
178 Value::String(text.to_owned())
179}
180
181fn object(members: Vec<(&str, Value)>) -> Value {
182 Value::Object(
183 members
184 .into_iter()
185 .map(|(key, value)| (key.to_owned(), value))
186 .collect(),
187 )
188}