1use std::{path::Path, sync::Arc};
2
3use exocore_protos::{
4 generated::exocore_apps::{manifest_schema::Source, Manifest},
5 reflect::{FileDescriptorSet, Message},
6};
7use libp2p::PeerId;
8
9use super::{Error, ManifestExt};
10use crate::{
11 dir::DynDirectory,
12 sec::{
13 hash::{multihash_decode_bs58, multihash_sha3_256, MultihashExt},
14 keys::{Keypair, PublicKey},
15 },
16};
17
18const MANIFEST_FILE_NAME: &str = "app.yaml";
19
20#[derive(Clone)]
23pub struct Application {
24 identity: Arc<Identity>,
25 schemas: Arc<[FileDescriptorSet]>,
26 dir: DynDirectory,
27}
28
29struct Identity {
30 public_key: PublicKey,
31 id: ApplicationId,
32 manifest: Manifest,
33}
34
35impl Application {
36 pub fn generate(
37 dir: impl Into<DynDirectory>,
38 name: String,
39 ) -> Result<(Keypair, Application), Error> {
40 let keypair = Keypair::generate_ed25519();
41 let dir = dir.into();
42
43 let manifest = Manifest {
44 name,
45 public_key: keypair.public().encode_base58_string(),
46 version: "0.0.1".to_string(),
47 ..Default::default()
48 };
49
50 Ok((keypair, Application::from_manifest(dir, manifest)?))
51 }
52
53 pub fn from_directory(dir: impl Into<DynDirectory>) -> Result<Application, Error> {
54 let dir = dir.into();
55
56 let manifest = {
57 let manifest_file = dir.open_read(Path::new(MANIFEST_FILE_NAME))?;
58 Manifest::read_yaml(manifest_file)?
59 };
60
61 Self::from_manifest(dir, manifest)
62 }
63
64 pub fn from_manifest(
65 dir: impl Into<DynDirectory>,
66 manifest: Manifest,
67 ) -> Result<Application, Error> {
68 let dir = dir.into();
69 let public_key = PublicKey::decode_base58_string(&manifest.public_key).map_err(|err| {
70 Error::Application(
71 manifest.name.clone(),
72 anyhow!("Error parsing application public_key: {}", err),
73 )
74 })?;
75
76 let id = ApplicationId::from_public_key(&public_key);
77
78 let mut schemas = Vec::new();
79 for app_schema in &manifest.schemas {
80 match &app_schema.source {
81 Some(Source::File(schema_path)) => {
82 let schema_file = dir.open_read(Path::new(schema_path))?;
83 let fd_set = read_file_descriptor_set(&manifest.name, schema_file)?;
84 schemas.push(fd_set);
85 }
86 other => {
87 return Err(Error::Application(
88 manifest.name.clone(),
89 anyhow!("Unsupported application schema source: {:?}", other),
90 ));
91 }
92 }
93 }
94
95 Ok(Application {
96 identity: Arc::new(Identity {
97 public_key,
98 id,
99 manifest,
100 }),
101 schemas: schemas.into(),
102 dir,
103 })
104 }
105
106 pub fn public_key(&self) -> &PublicKey {
107 &self.identity.public_key
108 }
109
110 pub fn id(&self) -> &ApplicationId {
111 &self.identity.id
112 }
113
114 pub fn name(&self) -> &str {
115 &self.identity.manifest.name
116 }
117
118 pub fn version(&self) -> &str {
119 &self.identity.manifest.version
120 }
121
122 pub fn manifest(&self) -> &Manifest {
123 &self.identity.manifest
124 }
125
126 pub fn schemas(&self) -> &[FileDescriptorSet] {
127 self.schemas.as_ref()
128 }
129
130 pub fn directory(&self) -> &DynDirectory {
131 &self.dir
132 }
133
134 pub fn validate(&self) -> Result<(), Error> {
135 if let Some(module) = &self.manifest().module {
137 let module_file = self.directory().open_read(Path::new(&module.file))?;
138
139 let module_multihash = multihash_sha3_256(module_file).map_err(|err| {
140 Error::Application(
141 self.name().to_string(),
142 anyhow!("Couldn't multihash module file at: {}", err),
143 )
144 })?;
145
146 let expected_multihash =
147 multihash_decode_bs58::<32>(&module.multihash).map_err(|err| {
148 Error::Application(
149 self.name().to_string(),
150 anyhow!(
151 "{}: Couldn't decode expected module multihash in manifest: {}",
152 self.name(),
153 err
154 ),
155 )
156 })?;
157
158 if expected_multihash != module_multihash {
159 return Err(Error::Application(
160 self.name().to_string(),
161 anyhow!(
162 "Module multihash in manifest doesn't match module file (expected={} module={})",
163 expected_multihash.encode_bs58(),
164 module_multihash.encode_bs58(),
165 ),
166 ));
167 }
168 }
169
170 Ok(())
171 }
172
173 pub fn save_manifest(&self, manifest: &Manifest) -> Result<(), Error> {
174 let manifest_file = self.dir.open_write(Path::new(MANIFEST_FILE_NAME))?;
175 manifest.write_yaml(manifest_file)?;
176 Ok(())
177 }
178
179 pub fn manifest_exists(dir: impl Into<DynDirectory>) -> bool {
180 dir.into().exists(Path::new(MANIFEST_FILE_NAME))
181 }
182}
183
184#[derive(PartialEq, Eq, Clone, Debug, Hash)]
189pub struct ApplicationId(String);
190
191impl ApplicationId {
192 pub fn from_public_key(public_key: &PublicKey) -> ApplicationId {
195 let peer_id = PeerId::from_public_key(public_key.to_libp2p());
196 ApplicationId(peer_id.to_string())
197 }
198
199 pub fn from_base58_public_key(public_key: &str) -> Result<ApplicationId, Error> {
200 let public_key = PublicKey::decode_base58_string(public_key)?;
201 Ok(ApplicationId::from_public_key(&public_key))
202 }
203
204 pub fn from_string(id: String) -> ApplicationId {
205 ApplicationId(id)
206 }
207
208 pub fn from_bytes(id: &[u8]) -> ApplicationId {
209 ApplicationId(String::from_utf8_lossy(id).to_string())
210 }
211
212 #[inline]
213 pub fn as_bytes(&self) -> &[u8] {
214 self.0.as_bytes()
215 }
216}
217
218impl std::fmt::Display for ApplicationId {
219 #[inline]
220 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
221 std::fmt::Display::fmt(&self.0, f)
222 }
223}
224
225impl std::str::FromStr for ApplicationId {
226 type Err = ();
227
228 fn from_str(s: &str) -> Result<Self, Self::Err> {
229 Ok(ApplicationId(s.to_string()))
230 }
231}
232
233fn read_file_descriptor_set(
234 app_name: &str,
235 mut content: impl std::io::Read,
236) -> Result<FileDescriptorSet, Error> {
237 let fd_set = FileDescriptorSet::parse_from_reader(&mut content).map_err(|err| {
238 Error::Application(
239 app_name.to_string(),
240 anyhow!(
241 "Couldn't parse application schema file descriptor set: {}",
242 err
243 ),
244 )
245 })?;
246
247 Ok(fd_set)
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use crate::dir::ram::RamDirectory;
254
255 #[test]
256 fn generate_and_validate() -> anyhow::Result<()> {
257 let dir = RamDirectory::new();
258 let (_kp, app) = Application::generate(dir, "some_app".to_string())?;
259 app.validate()?;
260 Ok(())
261 }
262
263 #[test]
264 fn app_id_conversion() {
265 let kp = crate::sec::keys::Keypair::generate_ed25519();
266 let app_id = ApplicationId::from_public_key(&kp.public());
267
268 assert_eq!(app_id, ApplicationId::from_string(app_id.to_string()));
269 assert_eq!(app_id, ApplicationId::from_bytes(app_id.as_bytes()));
270 assert_eq!(
271 app_id,
272 ApplicationId::from_base58_public_key(kp.public().encode_base58_string().as_str())
273 .unwrap()
274 );
275 }
276}