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