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