1use cm_rust::offer::OfferDeclCommon;
6use cm_rust::{ExposeDeclCommon, NativeIntoFidl};
7use flex_client::ProxyHasDomain;
8use flex_client::fidl::{ClientEnd, DiscoverableProtocolMarker};
9use flex_fuchsia_component as fcomponent;
10use flex_fuchsia_component_decl::Offer;
11use flex_fuchsia_io as fio;
12use flex_fuchsia_sys2 as fsys;
13use moniker::Moniker;
14use thiserror::Error;
15const CAPABILITY_REQUESTED_EVENT: &str = "capability_requested";
16
17#[derive(Debug, Error)]
18pub enum RealmValidationError {
19 #[error("Realm should expose {}", fcomponent::RealmMarker::PROTOCOL_NAME)]
20 RealmProtocol,
21
22 #[error(
23 "Realm should offer {} event stream to the test collection",
24 CAPABILITY_REQUESTED_EVENT
25 )]
26 CapabilityRequested,
27
28 #[error("The realm does not contain '{0}' named collection")]
29 TestCollectionNotFound(String),
30}
31
32#[derive(Debug, Error)]
33pub enum RealmError {
34 #[error(transparent)]
35 Fidl(#[from] fidl::Error),
36
37 #[error(transparent)]
38 Validation(#[from] RealmValidationError),
39
40 #[error("Invalid realm, it should contain test collection: /realm/collection")]
41 InvalidRealmStr,
42
43 #[error("cannot resolve provided realm: {0:?}")]
44 InstanceNotResolved(component_debug::lifecycle::ResolveError),
45
46 #[error(transparent)]
47 BadMoniker(#[from] moniker::MonikerError),
48
49 #[error("Cannot connect to exposed directory: {0:?}")]
50 ConnectExposedDir(fsys::OpenError),
51
52 #[error(transparent)]
53 GetManifest(#[from] component_debug::realm::GetDeclarationError),
54}
55
56pub struct Realm {
57 exposed_dir: fio::DirectoryProxy,
58 offers: Vec<flex_fuchsia_component_decl::Offer>,
59 realm_str: String,
60 test_collection: String,
61}
62
63impl std::fmt::Debug for Realm {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.write_fmt(format_args!("realm for moniker {}:{}", self.realm_str, self.test_collection))
66 }
67}
68
69impl PartialEq for Realm {
70 fn eq(&self, other: &Self) -> bool {
71 self.realm_str == other.realm_str && self.test_collection == other.test_collection
72 }
73}
74
75impl Realm {
76 pub fn get_realm_client(&self) -> Result<ClientEnd<fcomponent::RealmMarker>, fidl::Error> {
77 let (realm_client, server_end) =
78 self.exposed_dir.domain().create_endpoints::<fcomponent::RealmMarker>();
79 self.exposed_dir.open(
80 fcomponent::RealmMarker::PROTOCOL_NAME,
81 fio::Flags::PROTOCOL_SERVICE,
82 &Default::default(),
83 server_end.into_channel(),
84 )?;
85 Ok(realm_client)
86 }
87
88 pub fn offers(&self) -> Vec<flex_fuchsia_component_decl::Offer> {
89 self.offers.clone()
90 }
91
92 pub fn collection<'a>(&'a self) -> &'a str {
93 self.test_collection.as_str()
94 }
95}
96
97fn validate_and_get_offers(
98 manifest: cm_rust::ComponentDecl,
99 test_collection: &str,
100) -> Result<Vec<Offer>, RealmValidationError> {
101 let collection_found = manifest.collections.iter().any(|c| c.name == test_collection);
102 if !collection_found {
103 return Err(RealmValidationError::TestCollectionNotFound(test_collection.to_string()));
104 }
105
106 let exposes_realm_protocol =
107 manifest.exposes.iter().any(|e| *e.target_name() == fcomponent::RealmMarker::PROTOCOL_NAME);
108 if !exposes_realm_protocol {
109 return Err(RealmValidationError::RealmProtocol);
110 }
111
112 let mut capability_requested = false;
113 let mut offers = vec![];
114 for offer in manifest.offers {
115 if let cm_rust::offer::OfferTarget::Collection(collection) = &offer.target() {
116 if collection.as_str() != test_collection {
117 continue;
118 }
119
120 if let cm_rust::offer::OfferDecl::EventStream(decl) = &offer {
121 let cm_rust::offer::OfferEventStreamDecl { target_name, source, scope, .. } =
122 &**decl;
123
124 if *target_name == CAPABILITY_REQUESTED_EVENT
125 && source == &cm_rust::offer::OfferSource::Parent
126 && scope
127 .as_ref()
128 .map(|s| {
129 s.iter().any(|s| match s {
130 cm_rust::EventScope::Collection(s) => s.as_str() == test_collection,
131 _ => false,
132 })
133 })
134 .unwrap_or(false)
135 {
136 capability_requested =
137 capability_requested || *target_name == CAPABILITY_REQUESTED_EVENT;
138 }
139 }
140 offers.push(offer.native_into_fidl());
141 }
142 }
143 if !capability_requested {
144 return Err(RealmValidationError::CapabilityRequested);
145 }
146 Ok(offers)
147}
148
149pub async fn parse_provided_realm(
150 lifecycle_controller: &fsys::LifecycleControllerProxy,
151 realm_query: &fsys::RealmQueryProxy,
152 realm_str: &str,
153) -> Result<Realm, RealmError> {
154 let (mut moniker, mut test_collection) = match realm_str.rsplit_once('/') {
155 Some(s) => s,
156 None => {
157 return Err(RealmError::InvalidRealmStr);
158 }
159 };
160 if test_collection.contains(":") {
162 (moniker, test_collection) = match realm_str.rsplit_once(':') {
163 Some(s @ (moniker_head, collection_name)) => {
164 eprintln!(
165 "You are using old realm format. Please switch to standard realm moniker format: '{}/{}'",
166 moniker_head, collection_name
167 );
168 s
169 }
170 None => {
171 return Err(RealmError::InvalidRealmStr);
172 }
173 };
174 }
175 if moniker == "" {
176 return Err(RealmError::InvalidRealmStr);
177 }
178 let moniker = Moniker::try_from(moniker)?;
179
180 component_debug::lifecycle::resolve_instance(&lifecycle_controller, &moniker)
181 .await
182 .map_err(RealmError::InstanceNotResolved)?;
183
184 let manifest = component_debug::realm::get_resolved_declaration(&moniker, &realm_query).await?;
185
186 let offers = validate_and_get_offers(manifest, test_collection)?;
187
188 let (exposed_dir, server_end) = realm_query.domain().create_proxy();
189 realm_query
190 .open_directory(moniker.as_ref(), fsys::OpenDirType::ExposedDir, server_end)
191 .await?
192 .map_err(RealmError::ConnectExposedDir)?;
193
194 Ok(Realm {
195 exposed_dir,
196 offers,
197 realm_str: realm_str.to_string(),
198 test_collection: test_collection.to_owned(),
199 })
200}
201
202#[cfg(test)]
203mod test {
204 use super::*;
205 use assert_matches::assert_matches;
206 use cm_rust::FidlIntoNative;
207
208 #[fuchsia::test]
209 async fn valid_realm() {
210 let lifecycle_controller =
211 fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
212 .unwrap();
213 let realm_query =
214 fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
215 let realm =
216 parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/echo_test_coll")
217 .await
218 .unwrap();
219
220 assert_eq!(realm.test_collection, "echo_test_coll");
221 assert_eq!(realm.realm_str, "/test_realm/echo_test_coll");
222
223 let offers: Vec<cm_rust::OfferDecl> =
224 realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
225 assert_eq!(offers.len(), 3, "{:?}", offers);
226 offers.iter().for_each(|o| {
227 assert_eq!(
228 o.target(),
229 &cm_rust::offer::OfferTarget::Collection("echo_test_coll".parse().unwrap())
230 )
231 });
232 assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
233 assert!(offers.iter().any(|o| *o.target_name() == "fidl.examples.routing.echo.Echo"));
234
235 let realm = parse_provided_realm(
236 &lifecycle_controller,
237 &realm_query,
238 "/test_realm/hermetic_test_coll",
239 )
240 .await
241 .unwrap();
242
243 assert_eq!(realm.test_collection, "hermetic_test_coll");
244 assert_eq!(realm.realm_str, "/test_realm/hermetic_test_coll");
245
246 let offers = realm.offers.into_iter().map(|o| o.fidl_into_native()).collect::<Vec<_>>();
247 assert_eq!(offers.len(), 2, "{:?}", offers);
248 offers.iter().for_each(|o: &cm_rust::OfferDecl| {
249 assert_eq!(
250 o.target(),
251 &cm_rust::offer::OfferTarget::Collection("hermetic_test_coll".parse().unwrap())
252 )
253 });
254 assert!(offers.iter().any(|o| *o.target_name() == CAPABILITY_REQUESTED_EVENT));
255 }
256
257 #[fuchsia::test]
258 async fn invalid_realm() {
259 let lifecycle_controller =
260 fuchsia_component::client::connect_to_protocol::<fsys::LifecycleControllerMarker>()
261 .unwrap();
262 let realm_query =
263 fuchsia_component::client::connect_to_protocol::<fsys::RealmQueryMarker>().unwrap();
264 assert_matches!(
265 parse_provided_realm(
266 &lifecycle_controller,
267 &realm_query,
268 "/nonexistent_realm/test_coll"
269 )
270 .await,
271 Err(RealmError::InstanceNotResolved(_))
272 );
273
274 assert_matches!(
275 parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm").await,
276 Err(RealmError::InvalidRealmStr)
277 );
278
279 assert_matches!(
280 parse_provided_realm(&lifecycle_controller, &realm_query, "/test_realm/invalid_col")
281 .await,
282 Err(RealmError::Validation(RealmValidationError::TestCollectionNotFound(_)))
283 );
284
285 assert_matches!(
286 parse_provided_realm(
287 &lifecycle_controller,
288 &realm_query,
289 "/test_realm/no_capability_requested_event"
290 )
291 .await,
292 Err(RealmError::Validation(RealmValidationError::CapabilityRequested))
293 );
294
295 assert_matches!(
296 parse_provided_realm(
297 &lifecycle_controller,
298 &realm_query,
299 "/no_realm_protocol_realm/hermetic_test_coll"
300 )
301 .await,
302 Err(RealmError::Validation(RealmValidationError::RealmProtocol))
303 );
304 }
305}