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