Add slot support to android_main

Bug: 383620444
Change-Id: Ic16c840bde03c8a0838e79a9e5ec78f3fb5f6323
diff --git a/gbl/efi/src/ops.rs b/gbl/efi/src/ops.rs
index 2ce0432..7eda2fb 100644
--- a/gbl/efi/src/ops.rs
+++ b/gbl/efi/src/ops.rs
@@ -608,6 +608,39 @@
         })
     }
 
+    #[cfg(not(test))]
+    fn get_current_slot(&mut self) -> Result<Slot> {
+        // TODO(b/383620444): GBL EFI slot protocol is currently implemented on a few platforms such
+        // as Cuttlefish but is out of sync. Wait until protocol is more stable and all platforms
+        // pick up the latest before enabling.
+        Err(Error::Unsupported)
+    }
+
+    #[cfg(not(test))]
+    fn get_next_slot(&mut self, _: bool) -> Result<Slot> {
+        // TODO(b/383620444): See `get_current_slot()`.
+        Err(Error::Unsupported)
+    }
+
+    #[cfg(not(test))]
+    fn set_active_slot(&mut self, _: u8) -> Result<()> {
+        // TODO(b/383620444): See `get_current_slot()`.
+        Err(Error::Unsupported)
+    }
+
+    #[cfg(not(test))]
+    fn set_reboot_reason(&mut self, _: RebootReason) -> Result<()> {
+        // TODO(b/383620444): See `get_current_slot()`.
+        Err(Error::Unsupported)
+    }
+
+    #[cfg(not(test))]
+    fn get_reboot_reason(&mut self) -> Result<RebootReason> {
+        // TODO(b/383620444): See `get_current_slot()`.
+        Err(Error::Unsupported)
+    }
+
+    #[cfg(test)]
     fn get_current_slot(&mut self) -> Result<Slot> {
         // TODO(b/363075013): Refactors the opening of slot protocol into a common helper once
         // `MockBootServices::find_first_and_open` is updated to return Protocol<'_, T>.
@@ -619,6 +652,7 @@
             .try_into()
     }
 
+    #[cfg(test)]
     fn get_next_slot(&mut self, mark_boot_attempt: bool) -> Result<Slot> {
         self.efi_entry
             .system_table()
@@ -628,6 +662,7 @@
             .try_into()
     }
 
+    #[cfg(test)]
     fn set_active_slot(&mut self, slot: u8) -> Result<()> {
         self.efi_entry
             .system_table()
@@ -636,6 +671,7 @@
             .set_active_slot(slot)
     }
 
+    #[cfg(test)]
     fn set_reboot_reason(&mut self, reason: RebootReason) -> Result<()> {
         self.efi_entry
             .system_table()
@@ -644,6 +680,7 @@
             .set_boot_reason(gbl_to_efi_boot_reason(reason), b"")
     }
 
+    #[cfg(test)]
     fn get_reboot_reason(&mut self) -> Result<RebootReason> {
         let mut subreason = [0u8; 128];
         self.efi_entry
@@ -656,6 +693,8 @@
 }
 
 /// Converts a [GblEfiBootReason] to [RebootReason].
+// TODO(b/383620444): Remove the attribute once all boards picks up the stable Slot protocol.
+#[allow(dead_code)]
 fn efi_to_gbl_boot_reason(reason: GblEfiBootReason) -> RebootReason {
     match reason {
         GBL_EFI_BOOT_REASON_RECOVERY => RebootReason::Recovery,
@@ -666,6 +705,8 @@
 }
 
 /// Converts a [RebootReason] to [GblEfiBootReason].
+// TODO(b/383620444): Remove the attribute once all boards picks up the stable Slot protocol.
+#[allow(dead_code)]
 fn gbl_to_efi_boot_reason(reason: RebootReason) -> GblEfiBootReason {
     match reason {
         RebootReason::Recovery => GBL_EFI_BOOT_REASON_RECOVERY,
diff --git a/gbl/libgbl/src/android_boot/mod.rs b/gbl/libgbl/src/android_boot/mod.rs
index 1b6b0db..690a6c7 100644
--- a/gbl/libgbl/src/android_boot/mod.rs
+++ b/gbl/libgbl/src/android_boot/mod.rs
@@ -17,7 +17,7 @@
 use crate::{
     constants::{FDT_ALIGNMENT, KERNEL_ALIGNMENT, PAGE_SIZE},
     device_tree::{DeviceTreeComponentSource, DeviceTreeComponentsRegistry},
-    gbl_print, gbl_println, GblOps, Result,
+    gbl_print, gbl_println, GblOps, Result, SuffixBytes,
 };
 use bootimg::{BootImage, VendorImageHeader};
 use bootparams::{bootconfig::BootConfigBuilder, commandline::CommandlineBuilder};
@@ -622,9 +622,34 @@
         .unwrap_or(AndroidBootMode::Normal);
     gbl_println!(ops, "Boot mode from BCB: {}", boot_mode);
 
+    let slot_idx = match ops.get_boot_slot(true) {
+        Ok(slot) => {
+            // TODO(b/383620444): Checks whether fastboot has set a different active slot and resets
+            // if it does.
+
+            // Currently we assume slot suffix only takes value within 'a' to 'z'. Revisit if this
+            // is not the case.
+            //
+            // It's a little awkward to convert suffix char to integer which will then be converted
+            // back to char by the API. Consider passing in the char bytes directly.
+            let suffix_bytes = SuffixBytes::from(slot.suffix);
+            suffix_bytes[0] - b'a'
+        }
+        Err(Error::Unsupported) => {
+            // Default to slot A if slotting is not supported.
+            // Slotless partition name is currently not supported. Revisit if this causes problems.
+            gbl_println!(ops, "Slotting is not supported. Choose A slot by default");
+            0
+        }
+        Err(e) => {
+            gbl_println!(ops, "Failed to get boot slot: {e}");
+            return Err(e.into());
+        }
+    };
+
     // TODO(b/383620444): Add slot and fastboot support.
     let is_recovery = matches!(boot_mode, AndroidBootMode::Recovery);
-    android_load_verify_fixup(ops, 0, is_recovery, load)
+    android_load_verify_fixup(ops, slot_idx, is_recovery, load)
 }
 
 #[cfg(test)]
@@ -632,7 +657,7 @@
     use super::*;
     use crate::{
         gbl_avb::state::{BootStateColor, KeyValidationStatus},
-        ops::test::{FakeGblOps, FakeGblOpsStorage},
+        ops::test::{slot, FakeGblOps, FakeGblOpsStorage},
         tests::AlignedBuffer,
     };
     use load::tests::{
@@ -1268,6 +1293,7 @@
         ops.avb_ops.unlock_state = Ok(false);
         ops.avb_ops.rollbacks = HashMap::from([(TEST_ROLLBACK_INDEX_LOCATION, Ok(0))]);
         ops.avb_key_validation_status = Some(Ok(KeyValidationStatus::Valid));
+        ops.current_slot = Some(Ok(slot('a')));
         ops
     }
 
@@ -1313,4 +1339,95 @@
         let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
         checks_loaded_v2_slot_a_recovery_mode(ramdisk, kernel)
     }
+
+    /// Helper for checking V2 image loaded from slot B and in normal mode.
+    fn checks_loaded_v2_slot_b_normal_mode(ramdisk: &[u8], kernel: &[u8]) {
+        let expected_bootconfig = AvbResultBootconfigBuilder::new()
+            .vbmeta_size(read_test_data("vbmeta_v2_b.img").len())
+            .digest(read_test_data_as_str("vbmeta_v2_b.digest.txt").strip_suffix("\n").unwrap())
+            .public_key_digest(TEST_PUBLIC_KEY_DIGEST)
+            .extra(FakeGblOps::GBL_TEST_BOOTCONFIG)
+            .extra("androidboot.force_normal_boot=1\n")
+            .extra(format!("androidboot.slot_suffix=_b\n"))
+            .build();
+        check_ramdisk(ramdisk, &read_test_data("generic_ramdisk_b.img"), &expected_bootconfig);
+        assert_eq!(kernel, read_test_data("kernel_b.img"));
+    }
+
+    #[test]
+    fn test_android_main_slotted_gbl_slot_a() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"boot_a", read_test_data("boot_v2_a.img"));
+        storage.add_raw_device(c"vbmeta_a", read_test_data("vbmeta_v2_a.img"));
+        storage.add_raw_device(c"misc", vec![0u8; 4 * 1024 * 1024]);
+
+        let mut ops = default_test_gbl_ops(&storage);
+        let mut load_buffer = AlignedBuffer::new(8 * 1024 * 1024, KERNEL_ALIGNMENT);
+        let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
+        assert_eq!(ops.mark_boot_attempt_called, 0);
+        checks_loaded_v2_slot_a_normal_mode(ramdisk, kernel)
+    }
+
+    #[test]
+    fn test_android_main_slotless_gbl_slot_a() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"boot_a", read_test_data("boot_v2_a.img"));
+        storage.add_raw_device(c"vbmeta_a", read_test_data("vbmeta_v2_a.img"));
+        storage.add_raw_device(c"misc", vec![0u8; 4 * 1024 * 1024]);
+
+        let mut ops = default_test_gbl_ops(&storage);
+        ops.current_slot = Some(Err(Error::Unsupported));
+        ops.next_slot = Some(Ok(slot('a')));
+        let mut load_buffer = AlignedBuffer::new(8 * 1024 * 1024, KERNEL_ALIGNMENT);
+        let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
+        assert_eq!(ops.mark_boot_attempt_called, 1);
+        checks_loaded_v2_slot_a_normal_mode(ramdisk, kernel)
+    }
+
+    #[test]
+    fn test_android_main_slotted_gbl_slot_b() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"boot_b", read_test_data("boot_v2_b.img"));
+        storage.add_raw_device(c"vbmeta_b", read_test_data("vbmeta_v2_b.img"));
+        storage.add_raw_device(c"misc", vec![0u8; 4 * 1024 * 1024]);
+
+        let mut ops = default_test_gbl_ops(&storage);
+        ops.current_slot = Some(Ok(slot('b')));
+
+        let mut load_buffer = AlignedBuffer::new(8 * 1024 * 1024, KERNEL_ALIGNMENT);
+        let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
+        assert_eq!(ops.mark_boot_attempt_called, 0);
+        checks_loaded_v2_slot_b_normal_mode(ramdisk, kernel)
+    }
+
+    #[test]
+    fn test_android_main_slotless_gbl_slot_b() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"boot_b", read_test_data("boot_v2_b.img"));
+        storage.add_raw_device(c"vbmeta_b", read_test_data("vbmeta_v2_b.img"));
+        storage.add_raw_device(c"misc", vec![0u8; 4 * 1024 * 1024]);
+
+        let mut ops = default_test_gbl_ops(&storage);
+        ops.current_slot = Some(Err(Error::Unsupported));
+        ops.next_slot = Some(Ok(slot('b')));
+        let mut load_buffer = AlignedBuffer::new(8 * 1024 * 1024, KERNEL_ALIGNMENT);
+        let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
+        assert_eq!(ops.mark_boot_attempt_called, 1);
+        checks_loaded_v2_slot_b_normal_mode(ramdisk, kernel);
+    }
+
+    #[test]
+    fn test_android_main_unsupported_slot_default_to_a() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"boot_a", read_test_data("boot_v2_a.img"));
+        storage.add_raw_device(c"vbmeta_a", read_test_data("vbmeta_v2_a.img"));
+        storage.add_raw_device(c"misc", vec![0u8; 4 * 1024 * 1024]);
+
+        let mut ops = default_test_gbl_ops(&storage);
+        ops.current_slot = Some(Err(Error::Unsupported));
+        ops.next_slot = Some(Err(Error::Unsupported));
+        let mut load_buffer = AlignedBuffer::new(8 * 1024 * 1024, KERNEL_ALIGNMENT);
+        let (ramdisk, _, kernel, _) = android_main(&mut ops, &mut load_buffer).unwrap();
+        checks_loaded_v2_slot_a_normal_mode(ramdisk, kernel)
+    }
 }
diff --git a/gbl/libgbl/src/ops.rs b/gbl/libgbl/src/ops.rs
index 1405f91..ec86b06 100644
--- a/gbl/libgbl/src/ops.rs
+++ b/gbl/libgbl/src/ops.rs
@@ -410,6 +410,20 @@
     ///   any state change.
     fn get_next_slot(&mut self, _mark_boot_attempt: bool) -> Result<Slot, Error>;
 
+    /// Gets the target slot to boot.
+    ///
+    /// * If GBL is slotless (`Self::get_current_slot()` returns `Error::Unsupported`), the API
+    ///   behaves the same as `Self::get_next_slot()`.
+    /// * If GBL is slotted, the API behaves the same as `Self::get_current_slot()` and
+    ///   `mark_boot_attempt` is ignored.
+    fn get_boot_slot(&mut self, mark_boot_attempt: bool) -> Result<Slot, Error> {
+        match self.get_current_slot() {
+            // Slotless bootloader
+            Err(Error::Unsupported) => self.get_next_slot(mark_boot_attempt),
+            v => v,
+        }
+    }
+
     /// Sets the active slot for the next A/B decision.
     ///
     /// # Args
@@ -566,6 +580,19 @@
                 Option<&[u8]>,
             ) -> AvbIoResult<()>,
         >,
+
+        /// For returned by `get_current_slot`
+        //
+        // We wrap it in an `Option` so that if a test exercises code paths that use it but did not
+        // set it, it can panic with "unwrap()" which will give a clearer error and location
+        // message than a vague error such as `Error::Unimplemented`.
+        pub current_slot: Option<Result<Slot, Error>>,
+
+        /// For returned by `get_next_slot`
+        pub next_slot: Option<Result<Slot, Error>>,
+
+        /// Number of times `get_next_slot()` is called with `mark_boot_attempt` set to true.
+        pub mark_boot_attempt_called: usize,
     }
 
     /// Print `console_out` output, which can be useful for debugging.
@@ -839,11 +866,12 @@
         }
 
         fn get_current_slot(&mut self) -> Result<Slot, Error> {
-            unimplemented!()
+            self.current_slot.unwrap()
         }
 
-        fn get_next_slot(&mut self, _: bool) -> Result<Slot, Error> {
-            unimplemented!()
+        fn get_next_slot(&mut self, mark_boot_attempt: bool) -> Result<Slot, Error> {
+            self.mark_boot_attempt_called += usize::from(mark_boot_attempt);
+            self.next_slot.unwrap()
         }
 
         fn set_active_slot(&mut self, _: u8) -> Result<(), Error> {
@@ -903,4 +931,29 @@
         // One shot recovery is not set.
         assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::A, false));
     }
+
+    /// Helper for creating a slot object.
+    pub(crate) fn slot(suffix: char) -> Slot {
+        Slot { suffix: suffix.into(), ..Default::default() }
+    }
+
+    #[test]
+    fn test_get_boot_slot_slotted_gbl() {
+        let storage = FakeGblOpsStorage::default();
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.current_slot = Some(Ok(slot('a')));
+        assert_eq!(gbl_ops.get_current_slot().unwrap().suffix, 'a'.into());
+    }
+
+    #[test]
+    fn test_get_boot_slot_slotless_gbl() {
+        let storage = FakeGblOpsStorage::default();
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.next_slot = Some(Ok(slot('a')));
+        gbl_ops.current_slot = Some(Err(Error::Unsupported));
+        assert_eq!(gbl_ops.get_next_slot(false).unwrap().suffix, 'a'.into());
+        assert_eq!(gbl_ops.mark_boot_attempt_called, 0);
+        assert_eq!(gbl_ops.get_next_slot(true).unwrap().suffix, 'a'.into());
+        assert_eq!(gbl_ops.mark_boot_attempt_called, 1);
+    }
 }