1#![allow(
2 clippy::result_large_err,
3 reason = "the public fragment error preserves the loader registry error as its source"
4)]
5
6use std::fs::File;
7use std::io::{self, Read};
8use std::path::{Path, PathBuf};
9
10use player_plugin_loader::EmbeddedPluginRegistry;
11use serde::Serialize;
12use sha2::{Digest, Sha256};
13use thiserror::Error;
14
15use crate::{CanonicalPluginDescriptor, PluginDescriptorError};
16
17const HASH_BUFFER_BYTES: usize = 64 * 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum EmbeddedRegistryTarget {
21 AndroidNativeLibrary {
22 target: String,
23 architecture: String,
24 minimum_os: String,
25 library_name: String,
26 artifact_path: PathBuf,
27 },
28 AppleFramework {
29 target: String,
30 architecture: String,
31 minimum_os: String,
32 framework_name: String,
33 bundle_identifier: String,
34 },
35}
36
37impl EmbeddedRegistryTarget {
38 fn target(&self) -> &str {
39 match self {
40 Self::AndroidNativeLibrary { target, .. } | Self::AppleFramework { target, .. } => {
41 target
42 }
43 }
44 }
45
46 fn architecture(&self) -> &str {
47 match self {
48 Self::AndroidNativeLibrary { architecture, .. }
49 | Self::AppleFramework { architecture, .. } => architecture,
50 }
51 }
52
53 fn minimum_os(&self) -> &str {
54 match self {
55 Self::AndroidNativeLibrary { minimum_os, .. }
56 | Self::AppleFramework { minimum_os, .. } => minimum_os,
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct EmbeddedRegistryFragment {
63 file_name: String,
64 json: Vec<u8>,
65}
66
67impl EmbeddedRegistryFragment {
68 pub fn generate(
69 descriptor: &CanonicalPluginDescriptor,
70 target: &EmbeddedRegistryTarget,
71 ) -> Result<Self, EmbeddedRegistryFragmentError> {
72 descriptor.descriptor().validate()?;
73 let plugin = &descriptor.descriptor().plugin;
74 let (locator, integrity) = match target {
75 EmbeddedRegistryTarget::AndroidNativeLibrary {
76 library_name,
77 artifact_path,
78 ..
79 } => (
80 FragmentArtifactLocator::AndroidNativeLibrary {
81 name: library_name.clone(),
82 },
83 FragmentIntegrity::Sha256 {
84 digest: sha256_file(artifact_path)?,
85 },
86 ),
87 EmbeddedRegistryTarget::AppleFramework {
88 framework_name,
89 bundle_identifier,
90 ..
91 } => (
92 FragmentArtifactLocator::AppleFramework {
93 name: framework_name.clone(),
94 bundle_identifier: bundle_identifier.clone(),
95 },
96 FragmentIntegrity::AppleCodeSignature {
97 validation: AppleCodeSignatureValidation::SameTeamAsHostOrSimulatorAdHoc,
98 },
99 ),
100 };
101 let artifact = FragmentArtifact {
102 plugin_id: plugin.id.clone(),
103 transport: FragmentTransport::Native,
104 locator,
105 integrity,
106 package: FragmentPackage {
107 version: plugin.version.clone(),
108 publisher: plugin.publisher.clone(),
109 descriptor_sha256: descriptor.sha256().to_owned(),
110 },
111 capabilities: descriptor
112 .descriptor()
113 .capabilities
114 .iter()
115 .map(FragmentCapability::from)
116 .collect(),
117 };
118 let wire = FragmentRegistry {
119 schema_version: 1,
120 target: target.target().to_owned(),
121 architecture: target.architecture().to_owned(),
122 minimum_os: target.minimum_os().to_owned(),
123 artifacts: vec![artifact],
124 };
125 let json = serde_json::to_vec(&wire)?;
126 EmbeddedPluginRegistry::parse(&json, target.target(), target.architecture())?;
127 Ok(Self {
128 file_name: format!("{}.json", plugin.id),
129 json,
130 })
131 }
132
133 pub fn file_name(&self) -> &str {
134 &self.file_name
135 }
136
137 pub fn canonical_json(&self) -> &[u8] {
139 &self.json
140 }
141}
142
143#[derive(Debug, Error)]
144pub enum EmbeddedRegistryFragmentError {
145 #[error(transparent)]
146 Descriptor(#[from] PluginDescriptorError),
147 #[error("embedded registry Android artifact `{path}` is not a regular file")]
148 ArtifactNotFile { path: String },
149 #[error("failed to read embedded registry Android artifact `{path}`: {source}")]
150 ReadArtifact {
151 path: String,
152 #[source]
153 source: io::Error,
154 },
155 #[error("failed to serialize embedded registry fragment: {0}")]
156 Json(#[from] serde_json::Error),
157 #[error("generated embedded registry fragment is invalid: {0}")]
158 Registry(#[from] player_plugin_loader::EmbeddedPluginRegistryError),
159}
160
161#[derive(Serialize)]
162struct FragmentRegistry {
163 schema_version: u32,
164 target: String,
165 architecture: String,
166 minimum_os: String,
167 artifacts: Vec<FragmentArtifact>,
168}
169
170#[derive(Serialize)]
171struct FragmentArtifact {
172 plugin_id: String,
173 transport: FragmentTransport,
174 locator: FragmentArtifactLocator,
175 integrity: FragmentIntegrity,
176 package: FragmentPackage,
177 capabilities: Vec<FragmentCapability>,
178}
179
180#[derive(Serialize)]
181#[serde(rename_all = "lowercase")]
182enum FragmentTransport {
183 Native,
184}
185
186#[derive(Serialize)]
187#[serde(tag = "kind", rename_all = "kebab-case")]
188enum FragmentArtifactLocator {
189 AndroidNativeLibrary {
190 name: String,
191 },
192 AppleFramework {
193 name: String,
194 bundle_identifier: String,
195 },
196}
197
198#[derive(Serialize)]
199#[serde(tag = "kind", rename_all = "kebab-case")]
200enum FragmentIntegrity {
201 Sha256 {
202 digest: String,
203 },
204 AppleCodeSignature {
205 validation: AppleCodeSignatureValidation,
206 },
207}
208
209#[derive(Serialize)]
210#[serde(rename_all = "kebab-case")]
211enum AppleCodeSignatureValidation {
212 SameTeamAsHostOrSimulatorAdHoc,
213}
214
215#[derive(Serialize)]
216struct FragmentPackage {
217 version: String,
218 publisher: String,
219 descriptor_sha256: String,
220}
221
222#[derive(Serialize)]
223struct FragmentCapability {
224 interface_id: String,
225 instance_id: String,
226 interface_major: u16,
227 interface_minor: u16,
228}
229
230impl From<&crate::PluginCapabilityDescriptor> for FragmentCapability {
231 fn from(capability: &crate::PluginCapabilityDescriptor) -> Self {
232 Self {
233 interface_id: capability.interface_id.clone(),
234 instance_id: capability.instance_id.clone(),
235 interface_major: capability.interface_major,
236 interface_minor: capability.interface_minor,
237 }
238 }
239}
240
241fn sha256_file(path: &Path) -> Result<String, EmbeddedRegistryFragmentError> {
242 if !path.is_file() {
243 return Err(EmbeddedRegistryFragmentError::ArtifactNotFile {
244 path: path.display().to_string(),
245 });
246 }
247 let mut file =
248 File::open(path).map_err(|source| EmbeddedRegistryFragmentError::ReadArtifact {
249 path: path.display().to_string(),
250 source,
251 })?;
252 let mut hasher = Sha256::new();
253 let mut buffer = [0_u8; HASH_BUFFER_BYTES];
254 loop {
255 let read = file.read(&mut buffer).map_err(|source| {
256 EmbeddedRegistryFragmentError::ReadArtifact {
257 path: path.display().to_string(),
258 source,
259 }
260 })?;
261 if read == 0 {
262 break;
263 }
264 hasher.update(&buffer[..read]);
265 }
266 Ok(hex::encode(hasher.finalize()))
267}
268
269#[cfg(test)]
270mod tests {
271 use std::fs;
272 use std::sync::atomic::{AtomicU64, Ordering};
273
274 use super::*;
275 use crate::PluginDescriptor;
276
277 static NEXT_TEMP_ID: AtomicU64 = AtomicU64::new(1);
278
279 fn descriptor() -> CanonicalPluginDescriptor {
280 PluginDescriptor::from_toml(
281 r#"
282schema_version = 1
283
284[plugin]
285id = "dev.vesper.fixture"
286name = "Fixture"
287version = "1.2.3"
288description = "Fixture plugin"
289license = "Apache-2.0"
290publisher = "dev.vesper.publisher"
291
292[compatibility]
293host_sdk = ">=0.4.0, <0.5.0"
294abi_major = 1
295abi_minor_min = 0
296abi_minor_max = 0
297
298[[capabilities]]
299interface_id = "e9479dbc-42d2-575e-b39e-a24bc512fbc7"
300instance_id = "dev.vesper.fixture.post-download"
301interface_major = 1
302interface_minor = 0
303stability = "stable"
304"#,
305 )
306 .expect("valid descriptor")
307 .canonicalize()
308 .expect("canonical descriptor")
309 }
310
311 fn temporary_artifact() -> PathBuf {
312 let id = NEXT_TEMP_ID.fetch_add(1, Ordering::Relaxed);
313 let path = std::env::temp_dir().join(format!(
314 "vesper-registry-fragment-{}-{id}.so",
315 std::process::id()
316 ));
317 fs::write(&path, b"fixture artifact bytes").expect("write artifact");
318 path
319 }
320
321 #[test]
322 fn android_fragment_hashes_the_runtime_artifact_and_round_trips() {
323 let descriptor = descriptor();
324 let artifact_path = temporary_artifact();
325 let target = EmbeddedRegistryTarget::AndroidNativeLibrary {
326 target: "aarch64-linux-android".to_owned(),
327 architecture: "arm64-v8a".to_owned(),
328 minimum_os: "26".to_owned(),
329 library_name: "vesper_fixture".to_owned(),
330 artifact_path: artifact_path.clone(),
331 };
332
333 let fragment = EmbeddedRegistryFragment::generate(&descriptor, &target)
334 .expect("Android registry fragment");
335 let value: serde_json::Value =
336 serde_json::from_slice(fragment.canonical_json()).expect("registry JSON");
337
338 assert_eq!(fragment.file_name(), "dev.vesper.fixture.json");
339 assert_eq!(
340 value["artifacts"][0]["package"]["descriptor_sha256"],
341 descriptor.sha256()
342 );
343 assert_eq!(
344 value["artifacts"][0]["integrity"]["digest"],
345 hex::encode(Sha256::digest(b"fixture artifact bytes"))
346 );
347 let _ = fs::remove_file(artifact_path);
348 }
349
350 #[test]
351 fn apple_fragment_uses_bundle_identity_and_code_signature_policy() {
352 let descriptor = descriptor();
353 let target = EmbeddedRegistryTarget::AppleFramework {
354 target: "aarch64-apple-ios".to_owned(),
355 architecture: "arm64".to_owned(),
356 minimum_os: "17.0".to_owned(),
357 framework_name: "VesperPluginFixture".to_owned(),
358 bundle_identifier: "dev.vesper.plugin-fixture".to_owned(),
359 };
360
361 let fragment = EmbeddedRegistryFragment::generate(&descriptor, &target)
362 .expect("Apple registry fragment");
363 let value: serde_json::Value =
364 serde_json::from_slice(fragment.canonical_json()).expect("registry JSON");
365
366 assert_eq!(
367 value["artifacts"][0]["locator"]["bundle_identifier"],
368 "dev.vesper.plugin-fixture"
369 );
370 assert_eq!(
371 value["artifacts"][0]["integrity"]["validation"],
372 "same-team-as-host-or-simulator-ad-hoc"
373 );
374 }
375
376 #[test]
377 fn missing_android_artifact_fails_before_writing_a_fragment() {
378 let descriptor = descriptor();
379 let target = EmbeddedRegistryTarget::AndroidNativeLibrary {
380 target: "aarch64-linux-android".to_owned(),
381 architecture: "arm64-v8a".to_owned(),
382 minimum_os: "26".to_owned(),
383 library_name: "vesper_fixture".to_owned(),
384 artifact_path: PathBuf::from("/definitely/missing/vesper_fixture.so"),
385 };
386
387 assert!(matches!(
388 EmbeddedRegistryFragment::generate(&descriptor, &target),
389 Err(EmbeddedRegistryFragmentError::ArtifactNotFile { .. })
390 ));
391 }
392}