fs_management/
partition.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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/// Set of parameters to use for identifying the correct partition to open via
18/// [`open_partition`]
19///
20/// If multiple matchers are specified, the first partition that satisfies any set
21/// of matchers will be used. At least one of [`type_guids`], [`instance_guids`], [`labels`],
22/// [`detected_formats`], or [`parent_device`] must be specified.
23#[derive(Default, Clone)]
24pub struct PartitionMatcher {
25    /// Set of type GUIDs the partition must match. Ignored if empty.
26    pub type_guids: Option<Vec<[u8; 16]>>,
27    /// Set of instance GUIDs the partition must match. Ignored if empty.
28    pub instance_guids: Option<Vec<[u8; 16]>>,
29    pub labels: Option<Vec<String>>,
30    pub detected_disk_formats: Option<Vec<DiskFormat>>,
31    /// partition must be a child of this device.
32    pub parent_device: Option<String>,
33    /// The topological path must not start with this prefix.
34    pub ignore_prefix: Option<String>,
35    /// The topological path must not contain this substring.
36    pub ignore_if_path_contains: Option<String>,
37}
38
39const BLOCK_DEV_PATH: &str = "/dev/class/block/";
40
41/// Waits for a partition to appear on BLOCK_DEV_PATH that matches the fields in the
42/// PartitionMatcher. Returns the path of the partition if found. Errors after timeout duration.
43// TODO(https://fxbug.dev/42072982): Most users end up wanting the things we open for checking the partition,
44// like the partition proxy and the topological path. We should consider returning all those
45// resources instead of forcing them to retrieve them again.
46pub 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
54/// Waits for a partition to appear in [`dir`] that matches the fields in [`matcher`]. Returns the
55/// topological path of the partition if found. Returns an error after the timeout duration
56/// expires.
57pub 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
99/// Checks if the partition associated with proxy matches the matcher.
100/// An error isn't necessarily an issue - we might be using a matcher that wants a type guid,
101/// but the device we are currently checking doesn't implement get_type_guid. The error message may
102/// help debugging why no partition was matched but should generally be considered recoverable.
103pub 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}