1use crate::format::{detect_disk_format, DiskFormat};
6use anyhow::{anyhow, Context, Error};
7use fidl_fuchsia_device::{ControllerMarker, ControllerProxy};
8use fidl_fuchsia_hardware_block_partition::{Guid, PartitionMarker};
9use fidl_fuchsia_hardware_block_volume::VolumeManagerProxy;
10use fidl_fuchsia_io as fio;
11use fuchsia_async::TimeoutExt;
12use fuchsia_component_client::connect_to_named_protocol_at_dir_root;
13use fuchsia_fs::directory::{WatchEvent, Watcher};
14use futures::StreamExt;
15use zx::{self as zx, MonotonicDuration};
16
17#[derive(Default, Clone)]
24pub struct PartitionMatcher {
25 pub type_guids: Option<Vec<[u8; 16]>>,
27 pub instance_guids: Option<Vec<[u8; 16]>>,
29 pub labels: Option<Vec<String>>,
30 pub detected_disk_formats: Option<Vec<DiskFormat>>,
31 pub parent_device: Option<String>,
33 pub ignore_prefix: Option<String>,
35 pub ignore_if_path_contains: Option<String>,
37}
38
39const BLOCK_DEV_PATH: &str = "/dev/class/block/";
40
41pub async fn find_partition(
47 matcher: PartitionMatcher,
48 timeout: MonotonicDuration,
49) -> Result<ControllerProxy, Error> {
50 let dir = fuchsia_fs::directory::open_in_namespace(BLOCK_DEV_PATH, fio::Flags::empty())?;
51 find_partition_in(&dir, matcher, timeout).await
52}
53
54pub async fn find_partition_in(
58 dir: &fio::DirectoryProxy,
59 matcher: PartitionMatcher,
60 timeout: MonotonicDuration,
61) -> Result<ControllerProxy, Error> {
62 let timeout_seconds = timeout.into_seconds();
63 async {
64 let mut watcher = Watcher::new(dir).await.context("making watcher")?;
65 while let Some(message) = watcher.next().await {
66 let message = message.context("watcher channel returned error")?;
67 match message.event {
68 WatchEvent::ADD_FILE | WatchEvent::EXISTING => {
69 let filename = message.filename.to_str().unwrap();
70 if filename == "." {
71 continue;
72 }
73 let proxy = connect_to_named_protocol_at_dir_root::<ControllerMarker>(
74 &dir,
75 &format!("{filename}/device_controller"),
76 )
77 .context("opening partition path")?;
78 match partition_matches_with_proxy(&proxy, &matcher).await {
79 Ok(true) => {
80 return Ok(proxy);
81 }
82 Ok(false) => {}
83 Err(error) => {
84 log::info!(error:?; "Failure in partition match. Transient device?");
85 }
86 }
87 }
88 _ => (),
89 }
90 }
91 Err(anyhow!("Watch stream unexpectedly ended"))
92 }
93 .on_timeout(timeout, || {
94 Err(anyhow!("Timed out after {}s without finding expected partition", timeout_seconds))
95 })
96 .await
97}
98
99pub async fn partition_matches_with_proxy(
104 controller_proxy: &ControllerProxy,
105 matcher: &PartitionMatcher,
106) -> Result<bool, Error> {
107 assert!(
108 matcher.type_guids.is_some()
109 || matcher.instance_guids.is_some()
110 || matcher.detected_disk_formats.is_some()
111 || matcher.parent_device.is_some()
112 || matcher.labels.is_some()
113 );
114
115 let (partition_proxy, partition_server_end) =
116 fidl::endpoints::create_proxy::<PartitionMarker>();
117 controller_proxy
118 .connect_to_device_fidl(partition_server_end.into_channel())
119 .context("connecting to partition protocol")?;
120
121 if let Some(matcher_type_guids) = &matcher.type_guids {
122 let (status, guid_option) =
123 partition_proxy.get_type_guid().await.context("transport error on get_type_guid")?;
124 zx::Status::ok(status).context("get_type_guid failed")?;
125 let guid = guid_option.ok_or_else(|| anyhow!("Expected type guid"))?;
126 if !matcher_type_guids.into_iter().any(|x| x == &guid.value) {
127 return Ok(false);
128 }
129 }
130
131 if let Some(matcher_instance_guids) = &matcher.instance_guids {
132 let (status, guid_option) = partition_proxy
133 .get_instance_guid()
134 .await
135 .context("transport error on get_instance_guid")?;
136 zx::Status::ok(status).context("get_instance_guid failed")?;
137 let guid = guid_option.ok_or_else(|| anyhow!("Expected instance guid"))?;
138 if !matcher_instance_guids.into_iter().any(|x| x == &guid.value) {
139 return Ok(false);
140 }
141 }
142
143 if let Some(matcher_labels) = &matcher.labels {
144 let (status, name) =
145 partition_proxy.get_name().await.context("transport error on get_name")?;
146 zx::Status::ok(status).context("get_name failed")?;
147 let name = name.ok_or_else(|| anyhow!("Expected name"))?;
148 if name.is_empty() {
149 return Ok(false);
150 }
151 let mut matches_label = false;
152 for label in matcher_labels {
153 if name == *label {
154 matches_label = true;
155 break;
156 }
157 }
158 if !matches_label {
159 return Ok(false);
160 }
161 }
162
163 let topological_path = controller_proxy
164 .get_topological_path()
165 .await
166 .context("get_topological_path failed")?
167 .map_err(zx::Status::from_raw)?;
168
169 if let Some(matcher_parent_device) = &matcher.parent_device {
170 if !topological_path.starts_with(matcher_parent_device) {
171 return Ok(false);
172 }
173 }
174
175 if let Some(matcher_ignore_prefix) = &matcher.ignore_prefix {
176 if topological_path.starts_with(matcher_ignore_prefix) {
177 return Ok(false);
178 }
179 }
180
181 if let Some(matcher_ignore_if_path_contains) = &matcher.ignore_if_path_contains {
182 if topological_path.find(matcher_ignore_if_path_contains) != None {
183 return Ok(false);
184 }
185 }
186
187 if let Some(matcher_detected_disk_formats) = &matcher.detected_disk_formats {
188 let detected_format = detect_disk_format(&partition_proxy).await;
189 if !matcher_detected_disk_formats.into_iter().any(|x| x == &detected_format) {
190 return Ok(false);
191 }
192 }
193 Ok(true)
194}
195
196pub async fn fvm_allocate_partition(
197 fvm_proxy: &VolumeManagerProxy,
198 type_guid: [u8; 16],
199 instance_guid: [u8; 16],
200 name: &str,
201 flags: u32,
202 slice_count: u64,
203) -> Result<ControllerProxy, Error> {
204 let status = fvm_proxy
205 .allocate_partition(
206 slice_count,
207 &Guid { value: type_guid },
208 &Guid { value: instance_guid },
209 name,
210 flags,
211 )
212 .await?;
213 zx::Status::ok(status)?;
214
215 let matcher = PartitionMatcher {
216 type_guids: Some(vec![type_guid]),
217 instance_guids: Some(vec![instance_guid]),
218 ..Default::default()
219 };
220
221 find_partition(matcher, MonotonicDuration::from_seconds(40)).await
222}
223
224#[cfg(test)]
225mod tests {
226 use super::{partition_matches_with_proxy, PartitionMatcher};
227 use crate::format::{constants, DiskFormat};
228 use block_server::{DeviceInfo, PartitionInfo};
229 use fidl::endpoints::{create_proxy_and_stream, RequestStream as _};
230 use fidl_fuchsia_device::{ControllerMarker, ControllerRequest};
231 use fidl_fuchsia_hardware_block_volume::VolumeRequestStream;
232 use fuchsia_async as fasync;
233 use futures::{pin_mut, select, FutureExt, StreamExt};
234 use std::sync::Arc;
235 use vmo_backed_block_server::{InitialContents, VmoBackedServerOptions};
236
237 const VALID_TYPE_GUID: [u8; 16] = [1; 16];
238 const VALID_INSTANCE_GUID: [u8; 16] = [2; 16];
239 const VALID_LABEL: &str = "fake-server";
240
241 const INVALID_GUID_1: [u8; 16] = [
242 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
243 0x2f,
244 ];
245
246 const INVALID_GUID_2: [u8; 16] = [
247 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e,
248 0x3f,
249 ];
250
251 const INVALID_LABEL_1: &str = "TheWrongLabel";
252 const INVALID_LABEL_2: &str = "StillTheWrongLabel";
253 const PARENT_DEVICE_PATH: &str = "/fake/block/device/1";
254 const NOT_PARENT_DEVICE_PATH: &str = "/fake/block/device/2";
255 const DEFAULT_PATH: &str = "/fake/block/device/1/partition/001";
256
257 async fn check_partition_matches(matcher: &PartitionMatcher) -> bool {
258 let (proxy, mut stream) = create_proxy_and_stream::<ControllerMarker>();
259
260 let fake_block_server = Arc::new(
261 VmoBackedServerOptions {
262 block_size: 512,
263 info: DeviceInfo::Partition(PartitionInfo {
264 type_guid: VALID_TYPE_GUID,
265 instance_guid: VALID_INSTANCE_GUID,
266 name: VALID_LABEL.to_string(),
267 ..Default::default()
268 }),
269 initial_contents: InitialContents::FromBufferAndCapactity(
270 1000,
271 &constants::FVM_MAGIC,
272 ),
273 ..Default::default()
274 }
275 .build()
276 .unwrap(),
277 );
278
279 let mock_controller = async {
280 while let Some(request) = stream.next().await {
281 match request {
282 Ok(ControllerRequest::GetTopologicalPath { responder }) => {
283 responder.send(Ok(DEFAULT_PATH)).unwrap();
284 }
285 Ok(ControllerRequest::ConnectToDeviceFidl { server, .. }) => {
286 let fake_block_server = fake_block_server.clone();
287 fasync::Task::spawn(async move {
288 if let Err(e) = fake_block_server
289 .serve(VolumeRequestStream::from_channel(
290 fasync::Channel::from_channel(server),
291 ))
292 .await
293 {
294 println!("VmoBackedServer::serve failed: {e:?}");
295 }
296 })
297 .detach();
298 }
299 _ => {
300 println!("Unexpected request: {:?}", request);
301 unreachable!()
302 }
303 }
304 }
305 }
306 .fuse();
307
308 pin_mut!(mock_controller);
309
310 select! {
311 _ = mock_controller => unreachable!(),
312 matches = partition_matches_with_proxy(&proxy, &matcher).fuse() => matches,
313 }
314 .unwrap_or(false)
315 }
316
317 #[fuchsia::test]
318 async fn test_type_guid_match() {
319 let matcher = PartitionMatcher {
320 type_guids: Some(vec![VALID_TYPE_GUID, INVALID_GUID_1]),
321 ..Default::default()
322 };
323 assert_eq!(check_partition_matches(&matcher).await, true);
324 }
325
326 #[fuchsia::test]
327 async fn test_instance_guid_match() {
328 let matcher = PartitionMatcher {
329 instance_guids: Some(vec![VALID_INSTANCE_GUID, INVALID_GUID_1]),
330 ..Default::default()
331 };
332 assert_eq!(check_partition_matches(&matcher).await, true);
333 }
334
335 #[fuchsia::test]
336 async fn test_type_and_instance_guid_match() {
337 let matcher = PartitionMatcher {
338 type_guids: Some(vec![VALID_TYPE_GUID, INVALID_GUID_1]),
339 instance_guids: Some(vec![VALID_INSTANCE_GUID, INVALID_GUID_2]),
340 ..Default::default()
341 };
342 assert_eq!(check_partition_matches(&matcher).await, true);
343 }
344
345 #[fuchsia::test]
346 async fn test_parent_match() {
347 let matcher = PartitionMatcher {
348 parent_device: Some(PARENT_DEVICE_PATH.to_string()),
349 ..Default::default()
350 };
351 assert_eq!(check_partition_matches(&matcher).await, true);
352
353 let matcher2 = PartitionMatcher {
354 parent_device: Some(NOT_PARENT_DEVICE_PATH.to_string()),
355 ..Default::default()
356 };
357 assert_eq!(check_partition_matches(&matcher2).await, false);
358 }
359
360 #[fuchsia::test]
361 async fn test_single_label_match() {
362 let the_labels = vec![VALID_LABEL.to_string()];
363 let matcher = PartitionMatcher { labels: Some(the_labels), ..Default::default() };
364 assert_eq!(check_partition_matches(&matcher).await, true);
365 }
366
367 #[fuchsia::test]
368 async fn test_multi_label_match() {
369 let mut the_labels = vec![VALID_LABEL.to_string()];
370 the_labels.push(INVALID_LABEL_1.to_string());
371 the_labels.push(INVALID_LABEL_2.to_string());
372 let matcher = PartitionMatcher { labels: Some(the_labels), ..Default::default() };
373 assert_eq!(check_partition_matches(&matcher).await, true);
374 }
375
376 #[fuchsia::test]
377 async fn test_ignore_prefix_mismatch() {
378 let matcher = PartitionMatcher {
379 type_guids: Some(vec![VALID_TYPE_GUID]),
380 ignore_prefix: Some("/fake/block/device".to_string()),
381 ..Default::default()
382 };
383 assert_eq!(check_partition_matches(&matcher).await, false);
384 }
385
386 #[fuchsia::test]
387 async fn test_ignore_prefix_match() {
388 let matcher = PartitionMatcher {
389 type_guids: Some(vec![VALID_TYPE_GUID]),
390 ignore_prefix: Some("/real/block/device".to_string()),
391 ..Default::default()
392 };
393 assert_eq!(check_partition_matches(&matcher).await, true);
394 }
395
396 #[fuchsia::test]
397 async fn test_ignore_if_path_contains_mismatch() {
398 let matcher = PartitionMatcher {
399 type_guids: Some(vec![VALID_TYPE_GUID]),
400 ignore_if_path_contains: Some("/device/1".to_string()),
401 ..Default::default()
402 };
403 assert_eq!(check_partition_matches(&matcher).await, false);
404 }
405
406 #[fuchsia::test]
407 async fn test_ignore_if_path_contains_match() {
408 let matcher = PartitionMatcher {
409 type_guids: Some(vec![VALID_TYPE_GUID]),
410 ignore_if_path_contains: Some("/device/0".to_string()),
411 ..Default::default()
412 };
413 assert_eq!(check_partition_matches(&matcher).await, true);
414 }
415
416 #[fuchsia::test]
417 async fn test_type_and_label_match() {
418 let the_labels = vec![VALID_LABEL.to_string()];
419 let matcher = PartitionMatcher {
420 type_guids: Some(vec![VALID_TYPE_GUID]),
421 labels: Some(the_labels),
422 ..Default::default()
423 };
424 assert_eq!(check_partition_matches(&matcher).await, true);
425 }
426
427 #[fuchsia::test]
428 async fn test_type_guid_mismatch() {
429 let matcher = PartitionMatcher {
430 type_guids: Some(vec![INVALID_GUID_1, INVALID_GUID_2]),
431 ..Default::default()
432 };
433 assert_eq!(check_partition_matches(&matcher).await, false);
434 }
435
436 #[fuchsia::test]
437 async fn test_instance_guid_mismatch() {
438 let matcher = PartitionMatcher {
439 instance_guids: Some(vec![INVALID_GUID_1, INVALID_GUID_2]),
440 ..Default::default()
441 };
442 assert_eq!(check_partition_matches(&matcher).await, false);
443 }
444
445 #[fuchsia::test]
446 async fn test_label_mismatch() {
447 let mut the_labels = vec![INVALID_LABEL_1.to_string()];
448 the_labels.push(INVALID_LABEL_2.to_string());
449 let matcher = PartitionMatcher { labels: Some(the_labels), ..Default::default() };
450 assert_eq!(check_partition_matches(&matcher).await, false);
451 }
452
453 #[fuchsia::test]
454 async fn test_detected_disk_format_match() {
455 let matcher = PartitionMatcher {
456 detected_disk_formats: Some(vec![DiskFormat::Fvm, DiskFormat::Minfs]),
457 ..Default::default()
458 };
459 assert_eq!(check_partition_matches(&matcher).await, true);
460 }
461
462 #[fuchsia::test]
463 async fn test_detected_disk_format_mismatch() {
464 let matcher = PartitionMatcher {
465 detected_disk_formats: Some(vec![DiskFormat::Fxfs, DiskFormat::Minfs]),
466 ..Default::default()
467 };
468 assert_eq!(check_partition_matches(&matcher).await, false);
469 }
470}