1// Copyright 2023 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.
45use std::ffi::CStr;
6use std::future::Future;
7use std::os::raw::c_char;
8use std::pin::Pin;
910use anyhow::Error;
1112use fuchsia_async::LocalExecutor;
13use installer::{BootloaderType, InstallationPaths};
14use recovery_util_block::BlockDevice;
15use zx::Status;
1617// Converts a raw c-string into a Rust String, or None if the c-string was NULL.
18//
19// Making a String copies the data, but we do our work in a separate thread so we need a copy
20// anyway to be able to move the string over.
21fn to_string(c_str: *const c_char) -> Option<String> {
22if c_str.is_null() {
23return None;
24 }
2526// Safety: we've checked that the pointer is non-NULL and the C caller is required to meet the
27 // remaining safety requirements given by `install_from_usb()`.
28unsafe { CStr::from_ptr(c_str) }.to_str().map(String::from).ok()
29}
3031// Converts a Rust error to Zircon Status.
32// We don't own anyhow::Error or Status so can't directly implement From<> or Into<>.
33fn to_status(error: anyhow::Error) -> Status {
34// We can't easily convert an arbitrary string into a meaningful Zircon error code, so
35 // we log the string for debugging and just report an internal error.
36log::warn!("{error}");
37 Status::INTERNAL
38}
3940// We can't currently auto-generate with `cbindgen` during build, so add lint checks as a reminder
41// to re-generate the C bindings if this API changes. See README for details.
42// LINT.IfChange
43/// Installs images from a source disk to a destination disk.
44///
45/// This function can auto-detect the install source and destination if there is exactly one viable
46/// candidate for each, otherwise they must be supplied by the caller.
47///
48/// # Arguments
49/// `source`: UTF-8 source block device topological path, or NULL to auto-detect a removable disk.
50/// `destination`: UTF-8 destination block device topological path, or NULL to auto-detect internal
51/// storage.
52///
53/// # Returns
54/// A zx_status code.
55///
56/// # Safety
57/// The string arguments must either be NULL or meet all the conditions given at
58/// https://doc.rust-lang.org/std/ffi/struct.CStr.html, primarily:
59/// 1. The string must be null-terminated
60/// 2. The contents must not be modified until this function returns
61#[no_mangle]
62pub extern "C" fn install_from_usb(source: *const c_char, destination: *const c_char) -> i32 {
63// Include the function signature in the lint check, but not implementation, which can change
64 // without affecting the C bindings.
65 // LINT.ThenChange(../ffi_c/bindings.h)
6667 // This function just handles C/Rust conversion and async execution so the internals can be pure
68 // async Rust.
69log::trace!("Starting install_from_usb()");
7071// To handle async, this code spins up a separate thread with a new LocalExecutor. There may be
72 // a better way to do this, but these other methods failed:
73 // 1. New LocalExecutor on this thread - runtime panic, LocalExecutors are per-thread
74 // singletons and some components (in particular fastboot-tcp) may have already created
75 // one on this thread.
76 // 2. futures::executor::block_on() - runtime deadlock, this appears to be able to handle
77 // a single async call but the installer library runs concurrent async via
78 // futures::future::try_join_all() which deadlocks.
79let source = to_string(source);
80let destination = to_string(destination);
81let func = move || {
82 LocalExecutor::new().run_singlethreaded(install_from_usb_internal(
83 source,
84 destination,
85&Dependencies::default(),
86 ))
87 };
88let thread_result = std::thread::spawn(func).join();
89log::trace!("install_from_usb() result = {thread_result:?}");
9091match thread_result {
92Ok(result) => Status::from(result).into_raw(),
93Err(thread_panic) => {
94log::error!("install_from_usb thread panic: {thread_panic:?}");
95 Status::INTERNAL.into_raw()
96 }
97 }
98}
99100// Dependency injection for testing.
101//
102// Unfortunately there doesn't seem to be a super easy way to do this:
103// * mockall doesn't work well for free functions and has lifetime complications
104// * traits can't define async functions so we can't have a trait wrapper
105// So we just pass around a struct of function pointers, which due to `sync` requires some ugly
106// pin/box boilerplate, lifetime management, and indirection.
107//
108// TODO: I think we can greatly simplify this by wrapping each function in a sync -> async
109// wrapper individually. Maybe less efficient but we don't need the async functionality here
110// and it would allow us to mock out sync functions instead which is far easier.
111type BoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
112113struct Dependencies {
114 do_install: Box<dyn Fn(InstallationPaths) -> BoxedFuture<'static, Result<(), Error>>>,
115 find_install_source: Box<
116dyn for<'a> Fn(
117&'a Vec<BlockDevice>,
118 BootloaderType,
119 ) -> BoxedFuture<'a, Result<&'a BlockDevice, Error>>,
120 >,
121 get_block_devices: Box<dyn Fn() -> BoxedFuture<'static, Result<Vec<BlockDevice>, Error>>>,
122}
123124impl Dependencies {
125// Returns the actual implementations.
126fn default() -> Self {
127Self {
128// How do we pass the callback to `do_install()`? For now we don't need it so just pass
129 // a no-op closure, but I can't get the compiler to pass a real closure.
130do_install: Box::new(move |a| Box::pin(installer::do_install(a, &|_| {}))),
131 find_install_source: Box::new(move |a, b| {
132 Box::pin(installer::find_install_source(a, b))
133 }),
134 get_block_devices: Box::new(move || Box::pin(recovery_util_block::get_block_devices())),
135 }
136 }
137}
138139// Internal Rust entry point.
140async fn install_from_usb_internal(
141 source: Option<String>,
142 destination: Option<String>,
143 dependencies: &Dependencies,
144) -> Result<(), Status> {
145log::trace!(
146"Starting install_from_usb_internal(), source = {source:?}, dest = {destination:?}"
147);
148149let installation_paths =
150 get_installation_paths(source.as_deref(), destination.as_deref(), dependencies).await?;
151log::trace!("Installation paths: {installation_paths:?}");
152153 (dependencies.do_install)(installation_paths).await.map_err(to_status)
154}
155156// Finds the source and target to install.
157async fn get_installation_paths(
158 requested_source: Option<&str>,
159 requested_destination: Option<&str>,
160 dependencies: &Dependencies,
161) -> Result<InstallationPaths, Status> {
162// The installer library hardcodes some rules about what partitions are expected on a disk
163 // depending on the bootloader (coreboot vs EFI). This is a bit brittle, we may want to look
164 // into removing this dependency in the future.
165 // For now we don't care about coreboot, just hardcode EFI.
166let bootloader_type = BootloaderType::Efi;
167168log::trace!("Looking for block devices");
169let block_devices = (dependencies.get_block_devices)().await.map_err(to_status)?;
170log::trace!("Got block devices {block_devices:?}");
171172let install_source = match requested_source {
173// If a particular block device was requested, use it (or error out if not found).
174Some(device_path) => block_devices
175 .iter()
176 .find(|d| d.is_disk() && d.topo_path == device_path)
177 .ok_or(Err(Status::NOT_FOUND))?,
178// Otherwise, try to auto-detect the removable disk (e.g. USB).
179None => (dependencies.find_install_source)(&block_devices, bootloader_type)
180 .await
181.map_err(to_status)?,
182 };
183184let install_filter = |d: &&BlockDevice| {
185 d.is_disk()
186 && match requested_destination {
187// If a particular block device was requested, use it.
188Some(device_path) => d.topo_path == device_path,
189// Otherwise use the disk that isn't our source.
190None => *d != install_source,
191 }
192 };
193let mut install_iter = block_devices.iter().filter(install_filter);
194let install_target = install_iter.next().ok_or(Err(Status::NOT_FOUND))?;
195// Don't install if there could have been multiple targets, since it's ambiguous which one
196 // the caller wants. They must provide a `requested_destination` in this case.
197if install_iter.next().is_some() {
198return Err(Status::INVALID_ARGS);
199 }
200201let paths = InstallationPaths {
202 install_source: Some(install_source.clone()),
203 install_target: Some(install_target.clone()),
204 bootloader_type: Some(bootloader_type),
205// I don't think this is currently used - see if we can delete it.
206install_destinations: Vec::new(),
207 available_disks: block_devices,
208 };
209log::trace!("Found installation paths: {paths:?}");
210211Ok(paths)
212}
213214#[cfg(test)]
215mod tests {
216use super::*;
217218use anyhow::anyhow;
219use std::ffi::CString;
220use std::ptr::null;
221222impl Dependencies {
223// Returns the dependency test implementations.
224fn test() -> Self {
225Self {
226 do_install: Box::new(move |_| Box::pin(async { Ok(()) })),
227 find_install_source: Box::new(move |a, b| Box::pin(fake_find_install_source(a, b))),
228 get_block_devices: Box::new(move || Box::pin(fake_get_block_devices())),
229 }
230 }
231232// Returns the dependency test implementations that shows 3 disks.
233fn test_3_disks() -> Self {
234let mut deps = Self::test();
235 deps.get_block_devices = Box::new(move || Box::pin(fake_get_block_devices_3_disks()));
236 deps
237 }
238 }
239240// A fake set of block devices to test against.
241async fn fake_get_block_devices() -> Result<Vec<BlockDevice>, Error> {
242Ok(vec![
243// 2 disks (disks do not contain "/block/part-" in topo_path).
244BlockDevice {
245 topo_path: String::from("/dev/sys/platform/foo/block"),
246 class_path: String::from(""),
247 size: 0,
248 },
249 BlockDevice {
250 topo_path: String::from("/dev/sys/platform/bar/block"),
251 class_path: String::from(""),
252 size: 0,
253 },
254// A handful of partitions on the disks.
255BlockDevice {
256 topo_path: String::from("/dev/sys/platform/foo/block/part-000"),
257 class_path: String::from(""),
258 size: 0,
259 },
260 BlockDevice {
261 topo_path: String::from("/dev/sys/platform/foo/block/part-001"),
262 class_path: String::from(""),
263 size: 0,
264 },
265 BlockDevice {
266 topo_path: String::from("/dev/sys/platform/bar/block/part-000"),
267 class_path: String::from(""),
268 size: 0,
269 },
270 BlockDevice {
271 topo_path: String::from("/dev/sys/platform/bar/block/part-001"),
272 class_path: String::from(""),
273 size: 0,
274 },
275 ])
276 }
277278// A fake set of block devices that contains more than 3 so we can't auto-detect the install
279 // target.
280async fn fake_get_block_devices_3_disks() -> Result<Vec<BlockDevice>, Error> {
281let mut devices = fake_get_block_devices().await.unwrap();
282 devices.append(&mut vec![
283 BlockDevice {
284 topo_path: String::from("/dev/sys/platform/baz/block"),
285 class_path: String::from(""),
286 size: 0,
287 },
288 BlockDevice {
289 topo_path: String::from("/dev/sys/platform/baz/block/part-000"),
290 class_path: String::from(""),
291 size: 0,
292 },
293 BlockDevice {
294 topo_path: String::from("/dev/sys/platform/baz/block/part-001"),
295 class_path: String::from(""),
296 size: 0,
297 },
298 ]);
299Ok(devices)
300 }
301302// Returns the first found block device. The real implementation checks to see if the disk has
303 // partitions with the expected installer GUID, but there's no point replicating that here.
304async fn fake_find_install_source(
305 block_devices: &Vec<BlockDevice>,
306_: BootloaderType,
307 ) -> Result<&BlockDevice, Error> {
308Ok(&block_devices[0])
309 }
310311#[fuchsia::test]
312fn test_to_string() {
313let c_string = CString::new("test string").unwrap();
314assert_eq!(to_string(c_string.as_ptr()).unwrap(), "test string");
315 }
316317#[fuchsia::test]
318fn test_to_string_null() {
319assert!(to_string(null()).is_none());
320 }
321322#[fuchsia::test]
323fn test_to_status() {
324assert_eq!(to_status(anyhow!("expected test error")), Status::INTERNAL);
325 }
326327#[fuchsia_async::run_singlethreaded(test)]
328async fn test_get_installation_paths() {
329let deps = Dependencies::test();
330let fake_devices = (deps.get_block_devices)().await.unwrap();
331332let paths = get_installation_paths(None, None, &deps).await.unwrap();
333assert_eq!(
334 paths,
335 InstallationPaths {
336 install_source: Some(fake_devices[0].clone()),
337// Target should be the non-source disk.
338install_target: Some(fake_devices[1].clone()),
339 bootloader_type: Some(BootloaderType::Efi),
340 install_destinations: Vec::new(),
341 available_disks: fake_devices,
342 }
343 );
344 }
345346#[fuchsia_async::run_singlethreaded(test)]
347async fn test_get_installation_paths_request_source() {
348let deps = Dependencies::test();
349let fake_devices = (deps.get_block_devices)().await.unwrap();
350351// Request the 2nd disk as the install source.
352let paths =
353 get_installation_paths(Some(&fake_devices[1].topo_path), None, &deps).await.unwrap();
354assert_eq!(
355 paths,
356 InstallationPaths {
357 install_source: Some(fake_devices[1].clone()),
358 install_target: Some(fake_devices[0].clone()),
359 bootloader_type: Some(BootloaderType::Efi),
360 install_destinations: Vec::new(),
361 available_disks: fake_devices,
362 }
363 );
364 }
365366#[fuchsia_async::run_singlethreaded(test)]
367async fn test_get_installation_paths_request_target() {
368let deps = Dependencies::test();
369let fake_devices = (deps.get_block_devices)().await.unwrap();
370371// Request the 2nd disk as the install target.
372let paths =
373 get_installation_paths(None, Some(&fake_devices[1].topo_path), &deps).await.unwrap();
374assert_eq!(
375 paths,
376 InstallationPaths {
377 install_source: Some(fake_devices[0].clone()),
378 install_target: Some(fake_devices[1].clone()),
379 bootloader_type: Some(BootloaderType::Efi),
380 install_destinations: Vec::new(),
381 available_disks: fake_devices,
382 }
383 );
384 }
385386#[fuchsia_async::run_singlethreaded(test)]
387async fn test_get_installation_paths_request_both() {
388let deps = Dependencies::test_3_disks();
389let fake_devices = (deps.get_block_devices)().await.unwrap();
390391let paths = get_installation_paths(
392Some(&fake_devices[1].topo_path),
393// device[6] is the 3rd disk "baz" in our fake disks list.
394Some(&fake_devices[6].topo_path),
395&deps,
396 )
397 .await
398.unwrap();
399400assert_eq!(
401 paths,
402 InstallationPaths {
403 install_source: Some(fake_devices[1].clone()),
404 install_target: Some(fake_devices[6].clone()),
405 bootloader_type: Some(BootloaderType::Efi),
406 install_destinations: Vec::new(),
407 available_disks: fake_devices,
408 }
409 );
410 }
411412#[fuchsia_async::run_singlethreaded(test)]
413async fn test_get_installation_paths_ambiguous_target() {
414let deps = Dependencies::test_3_disks();
415let fake_devices = (deps.get_block_devices)().await.unwrap();
416417// With 3 disks we should error out because we can't determine which target to use.
418assert_eq!(
419 get_installation_paths(Some(&fake_devices[0].topo_path), None, &deps,).await,
420Err(Status::INVALID_ARGS)
421 );
422 }
423424#[fuchsia_async::run_singlethreaded(test)]
425async fn test_install_from_usb() {
426let deps = Dependencies::test();
427428assert!(install_from_usb_internal(None, None, &deps).await.is_ok());
429 }
430431#[fuchsia::test]
432fn test_install_from_usb_fail_sync() {
433// Test the top-level API in a sync context.
434 // We expect it to fail, this is primarily to ensure our async calls work.
435let source = CString::new("foo").unwrap();
436let dest = CString::new("bar").unwrap();
437assert!(install_from_usb(source.as_ptr(), dest.as_ptr()) != Status::OK.into_raw());
438 }
439440#[fuchsia::test]
441async fn test_install_from_usb_fail_async() {
442// Test the top-level API in an async context.
443 // We expect it to fail, this is primarily to ensure our async calls work.
444let source = CString::new("foo").unwrap();
445let dest = CString::new("bar").unwrap();
446assert!(install_from_usb(source.as_ptr(), dest.as_ptr()) != Status::OK.into_raw());
447 }
448}