[recovery] Move block device resolution to recovery-util

The functionality to list devices in dev/class/block and resolve their
topological paths is useful in both installer and recovery OTA. This
code can live in a common location for reusability.

Cq-Include-Trybots: luci.fuchsia.try:core.x64-asan-slow
Bug: b/235401382
Change-Id: I0ff6ffe2dfc5ce641b8648940082df6f808bfa6f
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/690390
Commit-Queue: Eric Stone <ecstone@google.com>
Reviewed-by: Simon Shields <simonshields@google.com>
Reviewed-by: Mike "mc" Comfoltey <comfoltey@google.com>
diff --git a/src/recovery/lib/recovery-util/BUILD.gn b/src/recovery/lib/recovery-util/BUILD.gn
index b74f969..2a34723 100644
--- a/src/recovery/lib/recovery-util/BUILD.gn
+++ b/src/recovery/lib/recovery-util/BUILD.gn
@@ -9,14 +9,20 @@
   edition = "2021"
   with_unit_tests = true
   deps = [
+    "//sdk/fidl/fuchsia.device:fuchsia.device-rustc",
+    "//sdk/fidl/fuchsia.hardware.block:fuchsia.hardware.block-rustc",
+    "//sdk/fidl/fuchsia.hardware.block.partition:fuchsia.hardware.block.partition-rustc",
+    "//sdk/fidl/fuchsia.hardware.block.volume:fuchsia.hardware.block.volume-rustc",
     "//sdk/fidl/fuchsia.wlan.common:fuchsia.wlan.common-rustc",
     "//sdk/fidl/fuchsia.wlan.policy:fuchsia.wlan.policy-rustc",
+    "//src/lib/fdio/rust:fdio",
     "//src/lib/fidl/rust/fidl",
     "//src/lib/fuchsia",
     "//src/lib/fuchsia-async",
     "//src/lib/fuchsia-component",
     "//src/lib/ui/carnelian",
     "//src/lib/zircon/rust:fuchsia-zircon",
+    "//src/lib/zircon/rust:fuchsia-zircon-status",
     "//third_party/rust_crates:anyhow",
     "//third_party/rust_crates:async-trait",
     "//third_party/rust_crates:euclid",
@@ -28,6 +34,7 @@
     "//third_party/rust_crates:pin-utils",
   ]
   sources = [
+    "src/block.rs",
     "src/lib.rs",
     "src/testing/mod.rs",
     "src/ui/mod.rs",
diff --git a/src/recovery/lib/recovery-util/src/block.rs b/src/recovery/lib/recovery-util/src/block.rs
new file mode 100644
index 0000000..17eb774
--- /dev/null
+++ b/src/recovery/lib/recovery-util/src/block.rs
@@ -0,0 +1,95 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+    anyhow::{anyhow, Context, Error},
+    fdio,
+    fidl::endpoints::Proxy,
+    fidl_fuchsia_device::ControllerProxy,
+    fidl_fuchsia_hardware_block::BlockProxy,
+    fuchsia_zircon as zx, fuchsia_zircon_status as zx_status,
+    std::{fs, path::Path},
+};
+
+async fn connect_to_service(path: &str) -> Result<fidl::AsyncChannel, Error> {
+    let (local, remote) = zx::Channel::create().context("Creating channel")?;
+    fdio::service_connect(path, remote).context("Connecting to service")?;
+    let local = fidl::AsyncChannel::from_channel(local).context("Creating AsyncChannel")?;
+    Ok(local)
+}
+
+async fn block_device_get_info(
+    block_channel: fidl::AsyncChannel,
+) -> Result<Option<(String, u64)>, Error> {
+    // Figure out topological path of the block device, so we can guess if it's a disk or a
+    // partition.
+    let (maybe_path, block_channel) = get_topological_path(block_channel).await?;
+    let topo_path = maybe_path.ok_or(anyhow!("Failed to get topo path for device"))?;
+
+    if topo_path.contains("/ramdisk-") {
+        // This is probably ram, skip it
+        return Ok(None);
+    }
+
+    let block = BlockProxy::from_channel(block_channel);
+    let (status, maybe_info) = block.get_info().await?;
+    if let Some(info) = maybe_info {
+        let blocks = info.block_count;
+        let block_size = info.block_size as u64;
+        return Ok(Some((topo_path, blocks * block_size)));
+    }
+
+    return Err(Error::new(zx_status::Status::from_raw(status)));
+}
+
+// There's no nice way to use a service without losing the channel,
+// so this function returns the controller.
+async fn get_topological_path(
+    channel: fidl::AsyncChannel,
+) -> Result<(Option<String>, fidl::AsyncChannel), Error> {
+    let controller = ControllerProxy::from_channel(channel);
+    let topo_resp = controller.get_topological_path().await.context("Getting topological path")?;
+    Ok((topo_resp.ok(), controller.into_channel().unwrap()))
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct BlockDevice {
+    /// Topological path of the block device.
+    pub topo_path: String,
+    /// Path to the block device under /dev/class/block.
+    pub class_path: String,
+    /// Size of the block device, in bytes.
+    pub size: u64,
+}
+
+impl BlockDevice {
+    /// Returns true if this block device is a disk.
+    pub fn is_disk(&self) -> bool {
+        // partitions have paths like this:
+        // /dev/sys/platform/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block/part-000/block
+        // while disks are like this:
+        // /dev/sys/platform/pci/00:17.0/ahci/sata2/block
+        !self.topo_path.contains("/block/part-")
+    }
+}
+
+pub async fn get_block_device(class_path: String) -> Result<Option<BlockDevice>, Error> {
+    let block_channel = connect_to_service(&class_path).await?;
+    let result = block_device_get_info(block_channel).await.context("Getting block device info")?;
+    Ok(result.map(|(topo_path, size)| BlockDevice { topo_path, class_path, size }))
+}
+
+pub async fn get_block_devices() -> Result<Vec<BlockDevice>, Error> {
+    let block_dir = Path::new("/dev/class/block");
+    let mut devices = Vec::new();
+    for entry in fs::read_dir(block_dir)? {
+        let name = entry?.path().to_str().unwrap().to_owned();
+        if let Some(bd) = get_block_device(name.clone()).await? {
+            devices.push(bd);
+        } else {
+            println!("Bad disk: {:?}", name);
+        }
+    }
+    Ok(devices)
+}
diff --git a/src/recovery/lib/recovery-util/src/lib.rs b/src/recovery/lib/recovery-util/src/lib.rs
index 4b03276..cd547b5 100644
--- a/src/recovery/lib/recovery-util/src/lib.rs
+++ b/src/recovery/lib/recovery-util/src/lib.rs
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+pub mod block;
 pub mod ui;
 pub mod wlan;
 
diff --git a/src/recovery/system/BUILD.gn b/src/recovery/system/BUILD.gn
index 986e527..b49c40c 100644
--- a/src/recovery/system/BUILD.gn
+++ b/src/recovery/system/BUILD.gn
@@ -257,6 +257,7 @@
     "//src/lib/ui/fuchsia-framebuffer",
     "//src/lib/zircon/rust:fuchsia-zircon",
     "//src/lib/zircon/rust:fuchsia-zircon-status",
+    "//src/recovery/lib/recovery-util",
     "//src/sys/lib/payload_streamer",
     "//src/sys/pkg/lib/isolated-ota",
     "//third_party/rust_crates:anyhow",
diff --git a/src/recovery/system/installer/installer.rs b/src/recovery/system/installer/installer.rs
index c83b4d7..320f75e 100644
--- a/src/recovery/system/installer/installer.rs
+++ b/src/recovery/system/installer/installer.rs
@@ -7,15 +7,13 @@
     anyhow::{anyhow, Context, Error},
     fdio,
     fidl::endpoints::{ClientEnd, Proxy, ServerEnd},
-    fidl_fuchsia_device::ControllerProxy,
-    fidl_fuchsia_hardware_block::BlockProxy,
     fidl_fuchsia_paver::{
         BootManagerMarker, Configuration, DynamicDataSinkProxy, PaverMarker, PaverProxy,
     },
     fidl_fuchsia_sysinfo as fsysinfo,
     fuchsia_component::client,
-    fuchsia_zircon as zx, fuchsia_zircon_status as zx_status,
-    std::{fs, path::Path},
+    fuchsia_zircon as zx,
+    recovery_util::block::BlockDevice,
 };
 
 #[derive(Clone, Copy, Debug, PartialEq)]
@@ -24,88 +22,6 @@
     Coreboot,
 }
 
-async fn connect_to_service(path: &str) -> Result<fidl::AsyncChannel, Error> {
-    let (local, remote) = zx::Channel::create().context("Creating channel")?;
-    fdio::service_connect(path, remote).context("Connecting to service")?;
-    let local = fidl::AsyncChannel::from_channel(local).context("Creating AsyncChannel")?;
-    Ok(local)
-}
-
-async fn block_device_get_info(
-    block_channel: fidl::AsyncChannel,
-) -> Result<Option<(String, u64)>, Error> {
-    // Figure out topological path of the block device, so we can guess if it's a disk or a
-    // partition.
-    let (maybe_path, block_channel) = get_topological_path(block_channel).await?;
-    let topo_path = maybe_path.ok_or(anyhow!("Failed to get topo path for device"))?;
-
-    if topo_path.contains("/ramdisk-") {
-        // This is probably ram, skip it
-        return Ok(None);
-    }
-
-    let block = BlockProxy::from_channel(block_channel);
-    let (status, maybe_info) = block.get_info().await?;
-    if let Some(info) = maybe_info {
-        let blocks = info.block_count;
-        let block_size = info.block_size as u64;
-        return Ok(Some((topo_path, blocks * block_size)));
-    }
-
-    return Err(Error::new(zx_status::Status::from_raw(status)));
-}
-
-// There's no nice way to use a service without losing the channel,
-// so this function returns the controller.
-async fn get_topological_path(
-    channel: fidl::AsyncChannel,
-) -> Result<(Option<String>, fidl::AsyncChannel), Error> {
-    let controller = ControllerProxy::from_channel(channel);
-    let topo_resp = controller.get_topological_path().await.context("Getting topological path")?;
-    Ok((topo_resp.ok(), controller.into_channel().unwrap()))
-}
-
-#[derive(Debug, PartialEq, Clone)]
-pub struct BlockDevice {
-    /// Topological path of the block device.
-    pub topo_path: String,
-    /// Path to the block device under /dev/class/block.
-    pub class_path: String,
-    /// Size of the block device, in bytes.
-    pub size: u64,
-}
-
-impl BlockDevice {
-    /// Returns true if this block device is a disk.
-    pub fn is_disk(&self) -> bool {
-        // partitions have paths like this:
-        // /dev/sys/platform/pci/00:14.0/xhci/usb-bus/001/001/ifc-000/ums/lun-000/block/part-000/block
-        // while disks are like this:
-        // /dev/sys/platform/pci/00:17.0/ahci/sata2/block
-        !self.topo_path.contains("/block/part-")
-    }
-}
-
-pub async fn get_block_device(class_path: String) -> Result<Option<BlockDevice>, Error> {
-    let block_channel = connect_to_service(&class_path).await?;
-    let result = block_device_get_info(block_channel).await.context("Getting block device info")?;
-    Ok(result.map(|(topo_path, size)| BlockDevice { topo_path, class_path, size }))
-}
-
-pub async fn get_block_devices() -> Result<Vec<BlockDevice>, Error> {
-    let block_dir = Path::new("/dev/class/block");
-    let mut devices = Vec::new();
-    for entry in fs::read_dir(block_dir)? {
-        let name = entry?.path().to_str().unwrap().to_owned();
-        if let Some(bd) = get_block_device(name.clone()).await? {
-            devices.push(bd);
-        } else {
-            println!("Bad disk: {:?}", name);
-        }
-    }
-    Ok(devices)
-}
-
 pub async fn find_install_source(
     block_devices: &Vec<BlockDevice>,
     bootloader: BootloaderType,
diff --git a/src/recovery/system/installer/main.rs b/src/recovery/system/installer/main.rs
index 8aee51d..ed24746 100644
--- a/src/recovery/system/installer/main.rs
+++ b/src/recovery/system/installer/main.rs
@@ -35,9 +35,10 @@
 
 pub mod installer;
 use installer::{
-    find_install_source, get_block_device, get_block_devices, get_bootloader_type, paver_connect,
-    set_active_configuration, BlockDevice, BootloaderType,
+    find_install_source, get_bootloader_type, paver_connect, set_active_configuration,
+    BootloaderType,
 };
+use recovery_util::block::{get_block_device, get_block_devices, BlockDevice};
 
 pub mod partition;
 use partition::Partition;
@@ -662,7 +663,13 @@
     let bootloader_type = installation_paths.bootloader_type.unwrap();
 
     // TODO(fxbug.dev/100712): Remove this once flake is resolved.
-    println!("Installing to {} ({}), source {} ({})", install_target.topo_path, install_target.class_path, install_source.topo_path, install_source.class_path);
+    println!(
+        "Installing to {} ({}), source {} ({})",
+        install_target.topo_path,
+        install_target.class_path,
+        install_source.topo_path,
+        install_source.class_path
+    );
 
     let (paver, data_sink) =
         paver_connect(&install_target.class_path).context("Could not contact paver")?;
diff --git a/src/recovery/system/installer/menu.rs b/src/recovery/system/installer/menu.rs
index 102c60a6..fb655ae 100644
--- a/src/recovery/system/installer/menu.rs
+++ b/src/recovery/system/installer/menu.rs
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use crate::installer::BlockDevice;
+use recovery_util::block::BlockDevice;
 
 const CONST_SELECT_INSTALL_HEADLINE: &'static str = "Select Installation Method";
 const CONST_SELECT_DISK_HEADLINE: &'static str = "Select Disk you would like to install Fuchsia to";
diff --git a/src/recovery/system/installer/partition.rs b/src/recovery/system/installer/partition.rs
index 1c522ea3..76f01da 100644
--- a/src/recovery/system/installer/partition.rs
+++ b/src/recovery/system/installer/partition.rs
@@ -3,7 +3,7 @@
 // found in the LICENSE file.
 
 use {
-    crate::installer::{BlockDevice, BootloaderType},
+    crate::installer::BootloaderType,
     anyhow::{Context, Error},
     fidl::endpoints::Proxy,
     fidl_fuchsia_fshost::{BlockWatcherMarker, BlockWatcherProxy},
@@ -13,6 +13,7 @@
     fuchsia_async as fasync, fuchsia_zircon as zx, fuchsia_zircon_status as zx_status,
     futures::prelude::*,
     payload_streamer::PayloadStreamer,
+    recovery_util::block::BlockDevice,
     regex,
     std::{fmt, fs, io::Read, path::Path, sync::Mutex},
 };