boot_serial: Add image state set/get

Adds optional image state set/get functionality to serial recovery
mode which allows for listing image states and marking images to
be tested or as confirmed.

Signed-off-by: Jamie McCrae <jamie.mccrae@nordicsemi.no>
diff --git a/boot/boot_serial/src/boot_serial.c b/boot/boot_serial/src/boot_serial.c
index 5b90fa5..b993751 100644
--- a/boot/boot_serial/src/boot_serial.c
+++ b/boot/boot_serial/src/boot_serial.c
@@ -87,7 +87,20 @@
 #define MCUBOOT_SERIAL_MAX_RECEIVE_SIZE 512
 #endif
 
-#define BOOT_SERIAL_OUT_MAX     (160 * BOOT_IMAGE_NUMBER)
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+#define BOOT_SERIAL_IMAGE_STATE_SIZE_MAX 48
+#else
+#define BOOT_SERIAL_IMAGE_STATE_SIZE_MAX 0
+#endif
+#ifdef MCUBOOT_SERIAL_IMG_GRP_HASH
+#define BOOT_SERIAL_HASH_SIZE_MAX 36
+#else
+#define BOOT_SERIAL_HASH_SIZE_MAX 0
+#endif
+
+#define BOOT_SERIAL_OUT_MAX     ((128 + BOOT_SERIAL_IMAGE_STATE_SIZE_MAX + \
+                                  BOOT_SERIAL_HASH_SIZE_MAX) * BOOT_IMAGE_NUMBER)
+
 #define BOOT_SERIAL_FRAME_MTU   124 /* 127 - pkt start (2 bytes) and stop (1 byte) */
 
 #ifdef __ZEPHYR__
@@ -232,7 +245,6 @@
 bs_list(char *buf, int len)
 {
     struct image_header hdr;
-    uint8_t tmpbuf[64];
     uint32_t slot, area_id;
     const struct flash_area *fap;
     uint8_t image_index;
@@ -245,7 +257,20 @@
     zcbor_list_start_encode(cbor_state, 5);
     image_index = 0;
     IMAGES_ITER(image_index) {
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+        int swap_status = boot_swap_type_multi(image_index);
+#endif
+
         for (slot = 0; slot < 2; slot++) {
+            uint8_t tmpbuf[64];
+
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+            bool active = false;
+            bool confirmed = false;
+            bool pending = false;
+            bool permanent = false;
+#endif
+
             area_id = flash_area_id_from_multi_image_slot(image_index, slot);
             if (flash_area_open(area_id, &fap)) {
                 continue;
@@ -258,10 +283,10 @@
                 flash_area_read(fap, 0, &hdr, sizeof(hdr));
             }
 
-            FIH_DECLARE(fih_rc, FIH_FAILURE);
-
             if (hdr.ih_magic == IMAGE_MAGIC)
             {
+                FIH_DECLARE(fih_rc, FIH_FAILURE);
+
                 BOOT_HOOK_CALL_FIH(boot_image_check_hook,
                                    FIH_BOOT_HOOK_REGULAR,
                                    fih_rc, image_index, slot);
@@ -281,6 +306,10 @@
                     FIH_CALL(bootutil_img_validate, fih_rc, NULL, 0, &hdr, fap, tmpbuf, sizeof(tmpbuf),
                                     NULL, 0, NULL);
                 }
+
+                if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) {
+                    continue;
+                }
             }
 
 #ifdef MCUBOOT_SERIAL_IMG_GRP_HASH
@@ -289,11 +318,6 @@
 #endif
 
             flash_area_close(fap);
-
-            if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) {
-                continue;
-            }
-
             zcbor_map_start_encode(cbor_state, 20);
 
 #if (BOOT_IMAGE_NUMBER > 1)
@@ -301,6 +325,59 @@
             zcbor_uint32_put(cbor_state, image_index);
 #endif
 
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+            if (swap_status == BOOT_SWAP_TYPE_NONE) {
+                if (slot == BOOT_PRIMARY_SLOT) {
+                    confirmed = true;
+                    active = true;
+                }
+            } else if (swap_status == BOOT_SWAP_TYPE_TEST) {
+                if (slot == BOOT_PRIMARY_SLOT) {
+                    confirmed = true;
+                } else {
+                    pending = true;
+                }
+            } else if (swap_status == BOOT_SWAP_TYPE_PERM) {
+                if (slot == BOOT_PRIMARY_SLOT) {
+                    confirmed = true;
+                } else {
+                    pending = true;
+                    permanent = true;
+                }
+            } else if (swap_status == BOOT_SWAP_TYPE_REVERT) {
+                if (slot == BOOT_PRIMARY_SLOT) {
+                    active = true;
+                } else {
+                    confirmed = true;
+                }
+            }
+
+            if (!(hdr.ih_flags & IMAGE_F_NON_BOOTABLE)) {
+                zcbor_tstr_put_lit_cast(cbor_state, "bootable");
+                zcbor_bool_put(cbor_state, 1);
+            }
+
+            if (confirmed) {
+                zcbor_tstr_put_lit_cast(cbor_state, "confirmed");
+                zcbor_bool_put(cbor_state, true);
+            }
+
+            if (active) {
+                zcbor_tstr_put_lit_cast(cbor_state, "active");
+                zcbor_bool_put(cbor_state, true);
+            }
+
+            if (pending) {
+                zcbor_tstr_put_lit_cast(cbor_state, "pending");
+                zcbor_bool_put(cbor_state, true);
+            }
+
+            if (permanent) {
+                zcbor_tstr_put_lit_cast(cbor_state, "permanent");
+                zcbor_bool_put(cbor_state, true);
+            }
+#endif
+
             zcbor_tstr_put_lit_cast(cbor_state, "slot");
             zcbor_uint32_put(cbor_state, slot);
 
@@ -324,8 +401,162 @@
     boot_serial_output();
 }
 
-#ifdef MCUBOOT_ERASE_PROGRESSIVELY
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+/*
+ * Set image state.
+ */
+static void
+bs_set(char *buf, int len)
+{
+    /*
+     * Expected data format.
+     * {
+     *   "confirm":<true for confirm, false for test>
+     *   "hash":<hash of image (OPTIONAL for single image only)>
+     * }
+     */
+    uint8_t image_index = 0;
+    size_t decoded = 0;
+    uint8_t hash[32];
+    bool confirm;
+    struct zcbor_string img_hash;
+    bool ok;
+    int rc;
 
+#ifdef MCUBOOT_SERIAL_IMG_GRP_HASH
+    bool found = false;
+#endif
+
+    zcbor_state_t zsd[4];
+    zcbor_new_state(zsd, sizeof(zsd) / sizeof(zcbor_state_t), (uint8_t *)buf, len, 1);
+
+    struct zcbor_map_decode_key_val image_set_state_decode[] = {
+        ZCBOR_MAP_DECODE_KEY_DECODER("confirm", zcbor_uint32_decode, &confirm),
+#ifdef MCUBOOT_SERIAL_IMG_GRP_HASH
+        ZCBOR_MAP_DECODE_KEY_DECODER("hash", zcbor_bstr_decode, &img_hash),
+#endif
+    };
+
+    ok = zcbor_map_decode_bulk(zsd, image_set_state_decode, ARRAY_SIZE(image_set_state_decode),
+                               &decoded) == 0;
+
+    if (!ok || len != decoded) {
+        rc = MGMT_ERR_EINVAL;
+        goto out;
+    }
+
+#ifdef MCUBOOT_SERIAL_IMG_GRP_HASH
+    if ((img_hash.len != sizeof(hash) && img_hash.len != 0) ||
+        (img_hash.len == 0 && BOOT_IMAGE_NUMBER > 1)) {
+        /* Hash is required and was not provided or is invalid size */
+        rc = MGMT_ERR_EINVAL;
+        goto out;
+    }
+
+    if (img_hash.len != 0) {
+        for (image_index = 0; image_index < BOOT_IMAGE_NUMBER; ++image_index) {
+            struct image_header hdr;
+            uint32_t area_id;
+            const struct flash_area *fap;
+            uint8_t tmpbuf[64];
+
+            area_id = flash_area_id_from_multi_image_slot(image_index, 1);
+            if (flash_area_open(area_id, &fap)) {
+                BOOT_LOG_ERR("Failed to open flash area ID %d", area_id);
+                continue;
+            }
+
+            rc = BOOT_HOOK_CALL(boot_read_image_header_hook,
+                                BOOT_HOOK_REGULAR, image_index, 1, &hdr);
+            if (rc == BOOT_HOOK_REGULAR)
+            {
+                flash_area_read(fap, 0, &hdr, sizeof(hdr));
+            }
+
+            if (hdr.ih_magic == IMAGE_MAGIC)
+            {
+                FIH_DECLARE(fih_rc, FIH_FAILURE);
+
+                BOOT_HOOK_CALL_FIH(boot_image_check_hook,
+                                   FIH_BOOT_HOOK_REGULAR,
+                                   fih_rc, image_index, 1);
+                if (FIH_EQ(fih_rc, FIH_BOOT_HOOK_REGULAR))
+                {
+                    FIH_CALL(bootutil_img_validate, fih_rc, NULL, 0, &hdr, fap,
+                             tmpbuf, sizeof(tmpbuf), NULL, 0, NULL);
+                }
+
+                if (FIH_NOT_EQ(fih_rc, FIH_SUCCESS)) {
+                    continue;
+                }
+            }
+
+            /* Retrieve SHA256 hash of image for identification */
+            rc = boot_serial_get_hash(&hdr, fap, hash);
+            flash_area_close(fap);
+
+            if (rc == 0 && memcmp(hash, img_hash.value, sizeof(hash)) == 0) {
+                /* Hash matches, set this slot for test or confirmation */
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            /* Image was not found with specified hash */
+            BOOT_LOG_ERR("Did not find image with specified hash");
+            rc = MGMT_ERR_ENOENT;
+            goto out;
+        }
+    }
+#endif
+
+    rc = boot_set_pending_multi(image_index, confirm);
+
+out:
+    if (rc == 0) {
+        /* Success - return updated list of images */
+        bs_list(buf, len);
+    } else {
+        /* Error code, only return the error */
+        zcbor_map_start_encode(cbor_state, 10);
+        zcbor_tstr_put_lit_cast(cbor_state, "rc");
+        zcbor_int32_put(cbor_state, rc);
+        zcbor_map_end_encode(cbor_state, 10);
+
+        boot_serial_output();
+    }
+}
+#endif
+
+/*
+ * Send rc code only.
+ */
+static void
+bs_rc_rsp(int rc_code)
+{
+    zcbor_map_start_encode(cbor_state, 10);
+    zcbor_tstr_put_lit_cast(cbor_state, "rc");
+    zcbor_int32_put(cbor_state, rc_code);
+    zcbor_map_end_encode(cbor_state, 10);
+    boot_serial_output();
+}
+
+static void
+bs_list_set(uint8_t op, char *buf, int len)
+{
+    if (op == NMGR_OP_READ) {
+        bs_list(buf, len);
+    } else {
+#ifdef MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+        bs_set(buf, len);
+#else
+        bs_rc_rsp(MGMT_ERR_ENOTSUP);
+#endif
+    }
+}
+
+#ifdef MCUBOOT_ERASE_PROGRESSIVELY
 /** Erases range of flash, aligned to sector size
  *
  * Function will erase all sectors withing [start, end] range; it does not check
@@ -630,20 +861,6 @@
 #endif //#ifdef MCUBOOT_ENC_IMAGES
 }
 
-/*
- * Send rc code only.
- */
-static void
-bs_rc_rsp(int rc_code)
-{
-    zcbor_map_start_encode(cbor_state, 10);
-    zcbor_tstr_put_lit_cast(cbor_state, "rc");
-    zcbor_int32_put(cbor_state, rc_code);
-    zcbor_map_end_encode(cbor_state, 10);
-    boot_serial_output();
-}
-
-
 #ifdef MCUBOOT_BOOT_MGMT_ECHO
 static void
 bs_echo(char *buf, int len)
@@ -758,7 +975,7 @@
     if (hdr->nh_group == MGMT_GROUP_ID_IMAGE) {
         switch (hdr->nh_id) {
         case IMGMGR_NMGR_ID_STATE:
-            bs_list(buf, len);
+            bs_list_set(hdr->nh_op, buf, len);
             break;
         case IMGMGR_NMGR_ID_UPLOAD:
             bs_upload(buf, len);
diff --git a/boot/boot_serial/src/boot_serial_priv.h b/boot/boot_serial/src/boot_serial_priv.h
index 1714cfd..a9ff1ed 100644
--- a/boot/boot_serial/src/boot_serial_priv.h
+++ b/boot/boot_serial/src/boot_serial_priv.h
@@ -40,6 +40,7 @@
 #define MGMT_ERR_EUNKNOWN       1
 #define MGMT_ERR_ENOMEM         2
 #define MGMT_ERR_EINVAL         3
+#define MGMT_ERR_ENOENT         5
 #define MGMT_ERR_ENOTSUP        8
 #define MGMT_ERR_EBUSY		10
 
diff --git a/boot/zephyr/Kconfig.serial_recovery b/boot/zephyr/Kconfig.serial_recovery
index c73573b..4d8f92f 100644
--- a/boot/zephyr/Kconfig.serial_recovery
+++ b/boot/zephyr/Kconfig.serial_recovery
@@ -207,4 +207,12 @@
 	  If y, image list responses will include the image hash (adds ~100
 	  bytes of flash).
 
+config BOOT_SERIAL_IMG_GRP_IMAGE_STATE
+	bool "Image state support"
+	depends on !SINGLE_APPLICATION_SLOT
+	select BOOT_SERIAL_IMG_GRP_HASH if UPDATEABLE_IMAGE_NUMBER > 1
+	help
+	  If y, image states will be included with image lists and the set
+	  state command can be used to mark an image as test/confirmed.
+
 endif # MCUBOOT_SERIAL
diff --git a/boot/zephyr/include/mcuboot_config/mcuboot_config.h b/boot/zephyr/include/mcuboot_config/mcuboot_config.h
index f4cd969..d69cf5d 100644
--- a/boot/zephyr/include/mcuboot_config/mcuboot_config.h
+++ b/boot/zephyr/include/mcuboot_config/mcuboot_config.h
@@ -213,6 +213,10 @@
 #define MCUBOOT_SERIAL_IMG_GRP_HASH
 #endif
 
+#ifdef CONFIG_BOOT_SERIAL_IMG_GRP_IMAGE_STATE
+#define MCUBOOT_SERIAL_IMG_GRP_IMAGE_STATE
+#endif
+
 /*
  * The option enables code, currently in boot_serial, that attempts
  * to erase flash progressively, as update fragments are received,