Merge changes Ib744d763,I74278bb5,I3d240d6e,I1b41d233

* changes:
  fastboot: Move some helpers into util.h/.cpp.
  fastboot: Allow using LOG().
  fastboot: Use RAII for sparse_file objects.
  liblp: Add a helper class for building sparse-compatible super image layouts.
diff --git a/fastboot/fastboot.cpp b/fastboot/fastboot.cpp
index 9676f87..7a5a782 100644
--- a/fastboot/fastboot.cpp
+++ b/fastboot/fastboot.cpp
@@ -53,6 +53,7 @@
 
 #include <android-base/endian.h>
 #include <android-base/file.h>
+#include <android-base/logging.h>
 #include <android-base/macros.h>
 #include <android-base/parseint.h>
 #include <android-base/parsenetaddress.h>
@@ -113,7 +114,7 @@
 
 struct fastboot_buffer {
     enum fb_buffer_type type;
-    void* data;
+    std::vector<SparsePtr> files;
     int64_t sz;
     unique_fd fd;
     int64_t image_size;
@@ -246,14 +247,6 @@
     fprintf(stderr, "(bootloader) %s\n", info.c_str());
 }
 
-static int64_t get_file_size(borrowed_fd fd) {
-    struct stat sb;
-    if (fstat(fd.get(), &sb) == -1) {
-        die("could not get file size");
-    }
-    return sb.st_size;
-}
-
 bool ReadFileToVector(const std::string& file, std::vector<char>* out) {
     out->clear();
 
@@ -824,27 +817,32 @@
     fprintf(stderr, "--------------------------------------------\n");
 }
 
-static struct sparse_file** load_sparse_files(int fd, int64_t max_size) {
-    struct sparse_file* s = sparse_file_import_auto(fd, false, true);
-    if (!s) die("cannot sparse read file");
-
+static std::vector<SparsePtr> resparse_file(sparse_file* s, int64_t max_size) {
     if (max_size <= 0 || max_size > std::numeric_limits<uint32_t>::max()) {
         die("invalid max size %" PRId64, max_size);
     }
 
-    int files = sparse_file_resparse(s, max_size, nullptr, 0);
+    const int files = sparse_file_resparse(s, max_size, nullptr, 0);
     if (files < 0) die("Failed to resparse");
 
-    sparse_file** out_s =
-            reinterpret_cast<sparse_file**>(calloc(sizeof(struct sparse_file*), files + 1));
-    if (!out_s) die("Failed to allocate sparse file array");
+    auto temp = std::make_unique<sparse_file*[]>(files);
+    const int rv = sparse_file_resparse(s, max_size, temp.get(), files);
+    if (rv < 0) die("Failed to resparse");
 
-    files = sparse_file_resparse(s, max_size, out_s, files);
-    if (files < 0) die("Failed to resparse");
-
+    std::vector<SparsePtr> out_s;
+    for (int i = 0; i < files; i++) {
+        out_s.emplace_back(temp[i], sparse_file_destroy);
+    }
     return out_s;
 }
 
+static std::vector<SparsePtr> load_sparse_files(int fd, int64_t max_size) {
+    SparsePtr s(sparse_file_import_auto(fd, false, true), sparse_file_destroy);
+    if (!s) die("cannot sparse read file");
+
+    return resparse_file(s.get(), max_size);
+}
+
 static uint64_t get_uint_var(const char* var_name) {
     std::string value_str;
     if (fb->GetVar(var_name, &value_str) != fastboot::SUCCESS || value_str.empty()) {
@@ -903,15 +901,13 @@
     int64_t limit = get_sparse_limit(sz);
     buf->fd = std::move(fd);
     if (limit) {
-        sparse_file** s = load_sparse_files(buf->fd.get(), limit);
-        if (s == nullptr) {
+        buf->files = load_sparse_files(buf->fd.get(), limit);
+        if (buf->files.empty()) {
             return false;
         }
         buf->type = FB_BUFFER_SPARSE;
-        buf->data = s;
     } else {
         buf->type = FB_BUFFER_FD;
-        buf->data = nullptr;
         buf->sz = sz;
     }
 
@@ -996,6 +992,8 @@
            fb->GetVar("partition-type:vbmeta_b", &partition_type) == fastboot::SUCCESS;
 }
 
+// Note: this only works in userspace fastboot. In the bootloader, use
+// should_flash_in_userspace().
 static bool is_logical(const std::string& partition) {
     std::string value;
     return fb->GetVar("is-logical:" + partition, &value) == fastboot::SUCCESS && value == "yes";
@@ -1078,9 +1076,16 @@
     lseek(buf->fd.get(), 0, SEEK_SET);
 }
 
-static void flash_buf(const std::string& partition, struct fastboot_buffer* buf) {
-    sparse_file** s;
+static void flash_partition_files(const std::string& partition,
+                                  const std::vector<SparsePtr>& files) {
+    for (size_t i = 0; i < files.size(); i++) {
+        sparse_file* s = files[i].get();
+        int64_t sz = sparse_file_len(s, true, false);
+        fb->FlashPartition(partition, s, sz, i + 1, files.size());
+    }
+}
 
+static void flash_buf(const std::string& partition, struct fastboot_buffer* buf) {
     if (partition == "boot" || partition == "boot_a" || partition == "boot_b" ||
         partition == "init_boot" || partition == "init_boot_a" || partition == "init_boot_b" ||
         partition == "recovery" || partition == "recovery_a" || partition == "recovery_b") {
@@ -1103,18 +1108,7 @@
 
     switch (buf->type) {
         case FB_BUFFER_SPARSE: {
-            std::vector<std::pair<sparse_file*, int64_t>> sparse_files;
-            s = reinterpret_cast<sparse_file**>(buf->data);
-            while (*s) {
-                int64_t sz = sparse_file_len(*s, true, false);
-                sparse_files.emplace_back(*s, sz);
-                ++s;
-            }
-
-            for (size_t i = 0; i < sparse_files.size(); ++i) {
-                const auto& pair = sparse_files[i];
-                fb->FlashPartition(partition, pair.first, pair.second, i + 1, sparse_files.size());
-            }
+            flash_partition_files(partition, buf->files);
             break;
         }
         case FB_BUFFER_FD:
@@ -1402,13 +1396,6 @@
     }
 }
 
-class ImageSource {
-  public:
-    virtual ~ImageSource(){};
-    virtual bool ReadFile(const std::string& name, std::vector<char>* out) const = 0;
-    virtual unique_fd OpenFile(const std::string& name) const = 0;
-};
-
 class FlashAllTool {
   public:
     FlashAllTool(const ImageSource& source, const std::string& slot_override, bool skip_secondary,
@@ -1782,20 +1769,7 @@
     if (!metadata) {
         return false;
     }
-    for (const auto& partition : metadata->partitions) {
-        auto candidate = android::fs_mgr::GetPartitionName(partition);
-        if (partition.attributes & LP_PARTITION_ATTR_SLOT_SUFFIXED) {
-            // On retrofit devices, we don't know if, or whether, the A or B
-            // slot has been flashed for dynamic partitions. Instead we add
-            // both names to the list as a conservative guess.
-            if (candidate + "_a" == partition_name || candidate + "_b" == partition_name) {
-                return true;
-            }
-        } else if (candidate == partition_name) {
-            return true;
-        }
-    }
-    return false;
+    return should_flash_in_userspace(*metadata.get(), partition_name);
 }
 
 static bool wipe_super(const android::fs_mgr::LpMetadata& metadata, const std::string& slot,
@@ -1868,7 +1842,19 @@
     }
 }
 
+static void FastbootLogger(android::base::LogId /* id */, android::base::LogSeverity /* severity */,
+                           const char* /* tag */, const char* /* file */, unsigned int /* line */,
+                           const char* message) {
+    verbose("%s", message);
+}
+
+static void FastbootAborter(const char* message) {
+    die("%s", message);
+}
+
 int FastBootTool::Main(int argc, char* argv[]) {
+    android::base::InitLogging(argv, FastbootLogger, FastbootAborter);
+
     bool wants_wipe = false;
     bool wants_reboot = false;
     bool wants_reboot_bootloader = false;
diff --git a/fastboot/util.cpp b/fastboot/util.cpp
index 900d6ea..ded54a5 100644
--- a/fastboot/util.cpp
+++ b/fastboot/util.cpp
@@ -30,11 +30,13 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-
+#include <sys/stat.h>
 #include <sys/time.h>
 
 #include "util.h"
 
+using android::base::borrowed_fd;
+
 static bool g_verbose = false;
 
 double now() {
@@ -73,3 +75,34 @@
     }
     fprintf(stderr, "\n");
 }
+
+bool should_flash_in_userspace(const android::fs_mgr::LpMetadata& metadata,
+                               const std::string& partition_name) {
+    for (const auto& partition : metadata.partitions) {
+        auto candidate = android::fs_mgr::GetPartitionName(partition);
+        if (partition.attributes & LP_PARTITION_ATTR_SLOT_SUFFIXED) {
+            // On retrofit devices, we don't know if, or whether, the A or B
+            // slot has been flashed for dynamic partitions. Instead we add
+            // both names to the list as a conservative guess.
+            if (candidate + "_a" == partition_name || candidate + "_b" == partition_name) {
+                return true;
+            }
+        } else if (candidate == partition_name) {
+            return true;
+        }
+    }
+    return false;
+}
+
+bool is_sparse_file(borrowed_fd fd) {
+    SparsePtr s(sparse_file_import(fd.get(), false, false), sparse_file_destroy);
+    return !!s;
+}
+
+int64_t get_file_size(borrowed_fd fd) {
+    struct stat sb;
+    if (fstat(fd.get(), &sb) == -1) {
+        die("could not get file size");
+    }
+    return sb.st_size;
+}
diff --git a/fastboot/util.h b/fastboot/util.h
index c719df2..290d0d5 100644
--- a/fastboot/util.h
+++ b/fastboot/util.h
@@ -4,8 +4,14 @@
 #include <stdlib.h>
 
 #include <string>
+#include <vector>
 
+#include <android-base/unique_fd.h>
 #include <bootimg.h>
+#include <liblp/liblp.h>
+#include <sparse/sparse.h>
+
+using SparsePtr = std::unique_ptr<sparse_file, decltype(&sparse_file_destroy)>;
 
 /* util stuff */
 double now();
@@ -19,3 +25,15 @@
 void verbose(const char* fmt, ...) __attribute__((__format__(__printf__, 1, 2)));
 
 void die(const std::string& str) __attribute__((__noreturn__));
+
+bool should_flash_in_userspace(const android::fs_mgr::LpMetadata& metadata,
+                               const std::string& partition_name);
+bool is_sparse_file(android::base::borrowed_fd fd);
+int64_t get_file_size(android::base::borrowed_fd fd);
+
+class ImageSource {
+  public:
+    virtual ~ImageSource(){};
+    virtual bool ReadFile(const std::string& name, std::vector<char>* out) const = 0;
+    virtual android::base::unique_fd OpenFile(const std::string& name) const = 0;
+};
diff --git a/fs_mgr/liblp/Android.bp b/fs_mgr/liblp/Android.bp
index 4b81c2c..996ffd7 100644
--- a/fs_mgr/liblp/Android.bp
+++ b/fs_mgr/liblp/Android.bp
@@ -39,6 +39,7 @@
     ],
     srcs: [
         "builder.cpp",
+        "super_layout_builder.cpp",
         "images.cpp",
         "partition_opener.cpp",
         "property_fetcher.cpp",
@@ -62,17 +63,6 @@
     export_include_dirs: ["include"],
 }
 
-filegroup {
-    name: "liblp_test_srcs",
-    srcs: [
-        "builder_test.cpp",
-        "device_test.cpp",
-        "io_test.cpp",
-        "test_partition_opener.cpp",
-        "utility_test.cpp",
-    ],
-}
-
 cc_defaults {
     name: "liblp_test_defaults",
     defaults: ["fs_mgr_defaults"],
@@ -82,22 +72,39 @@
     static_libs: [
         "libcutils",
         "libgmock",
-        "libfs_mgr",
         "liblp",
         "libcrypto_static",
     ] + liblp_lib_deps,
     header_libs: [
         "libstorage_literals_headers",
     ],
+    target: {
+        android: {
+            srcs: [
+                "device_test.cpp",
+                "io_test.cpp",
+            ],
+            static_libs: [
+                "libfs_mgr",
+            ],
+        }
+    },
     stl: "libc++_static",
-    srcs: [":liblp_test_srcs"],
+    srcs: [
+        "builder_test.cpp",
+        "super_layout_builder_test.cpp",
+        "test_partition_opener.cpp",
+        "utility_test.cpp",
+    ],
 }
 
 cc_test {
     name: "liblp_test",
     defaults: ["liblp_test_defaults"],
-    test_config: "liblp_test.xml",
     test_suites: ["device-tests"],
+    auto_gen_config: true,
+    require_root: true,
+    host_supported: true
 }
 
 cc_test {
diff --git a/fs_mgr/liblp/include/liblp/super_layout_builder.h b/fs_mgr/liblp/include/liblp/super_layout_builder.h
new file mode 100644
index 0000000..d920855
--- /dev/null
+++ b/fs_mgr/liblp/include/liblp/super_layout_builder.h
@@ -0,0 +1,104 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#pragma once
+
+#include <stdint.h>
+
+#include <memory>
+#include <ostream>
+#include <string>
+#include <unordered_map>
+#include <utility>
+
+#include <android-base/unique_fd.h>
+#include <liblp/builder.h>
+
+namespace android {
+namespace fs_mgr {
+
+struct SuperImageExtent {
+    enum class Type { INVALID, DATA, PARTITION, ZERO, DONTCARE };
+
+    SuperImageExtent(const SuperImageExtent& other) = default;
+    SuperImageExtent(SuperImageExtent&& other) = default;
+    SuperImageExtent(uint64_t offset, uint64_t size, Type type)
+        : offset(offset), size(size), type(type) {}
+
+    SuperImageExtent(uint64_t offset, std::shared_ptr<std::string> blob)
+        : SuperImageExtent(offset, blob->size(), Type::DATA) {
+        this->blob = blob;
+    }
+
+    SuperImageExtent(uint64_t offset, uint64_t size, const std::string& image_name,
+                     uint64_t image_offset)
+        : SuperImageExtent(offset, size, Type::PARTITION) {
+        this->image_name = image_name;
+        this->image_offset = image_offset;
+    }
+
+    SuperImageExtent& operator=(const SuperImageExtent& other) = default;
+    SuperImageExtent& operator=(SuperImageExtent&& other) = default;
+
+    bool operator<(const SuperImageExtent& other) const { return offset < other.offset; }
+    bool operator==(const SuperImageExtent& other) const;
+
+    // Location, size, and type of the extent.
+    uint64_t offset = 0;
+    uint64_t size = 0;
+    Type type = Type::INVALID;
+
+    // If type == DATA, this contains the bytes to write.
+    std::shared_ptr<std::string> blob;
+    // If type == PARTITION, this contains the partition image name and
+    // offset within that file.
+    std::string image_name;
+    uint64_t image_offset = 0;
+};
+
+// The SuperLayoutBuilder allows building a sparse view of a super image. This
+// is useful for efficient flashing, eg to bypass fastbootd and directly flash
+// super without physically building and storing the image.
+class SuperLayoutBuilder final {
+  public:
+    // Open a super_empty.img, return false on failure. This must be called to
+    // initialize the tool. If it returns false, either the image failed to
+    // parse, or the tool is not compatible with how the device is configured
+    // (in which case fastbootd should be preferred).
+    [[nodiscard]] bool Open(android::base::borrowed_fd fd);
+    [[nodiscard]] bool Open(const void* data, size_t bytes);
+    [[nodiscard]] bool Open(const LpMetadata& metadata);
+
+    // Add a partition's image and size to the work list. If false is returned,
+    // there was either a duplicate partition or not enough space in super.
+    bool AddPartition(const std::string& partition_name, const std::string& image_name,
+                      uint64_t partition_size);
+
+    // Return the list of extents describing the super image. If this list is
+    // empty, then there was an unrecoverable error in building the list.
+    std::vector<SuperImageExtent> GetImageLayout();
+
+    // Return the current metadata.
+    std::unique_ptr<LpMetadata> Export() const { return builder_->Export(); }
+
+  private:
+    std::unique_ptr<MetadataBuilder> builder_;
+    std::unordered_map<std::string, std::string> image_map_;
+};
+
+std::ostream& operator<<(std::ostream& stream, const SuperImageExtent& extent);
+
+}  // namespace fs_mgr
+}  // namespace android
diff --git a/fs_mgr/liblp/liblp_test.xml b/fs_mgr/liblp/liblp_test.xml
deleted file mode 100644
index 98414b1..0000000
--- a/fs_mgr/liblp/liblp_test.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<configuration description="Config for liblp_test">
-    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
-    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
-        <option name="cleanup" value="true" />
-        <option name="push" value="liblp_test->/data/local/tmp/liblp_test" />
-    </target_preparer>
-    <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp" />
-        <option name="module-name" value="liblp_test" />
-    </test>
-</configuration>
diff --git a/fs_mgr/liblp/super_layout_builder.cpp b/fs_mgr/liblp/super_layout_builder.cpp
new file mode 100644
index 0000000..37f28e1
--- /dev/null
+++ b/fs_mgr/liblp/super_layout_builder.cpp
@@ -0,0 +1,241 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#include <liblp/super_layout_builder.h>
+
+#include <liblp/liblp.h>
+
+#include "images.h"
+#include "utility.h"
+#include "writer.h"
+
+using android::base::borrowed_fd;
+using android::base::unique_fd;
+
+namespace android {
+namespace fs_mgr {
+
+bool SuperLayoutBuilder::Open(borrowed_fd fd) {
+    auto metadata = ReadFromImageFile(fd.get());
+    if (!metadata) {
+        return false;
+    }
+    return Open(*metadata.get());
+}
+
+bool SuperLayoutBuilder::Open(const void* data, size_t size) {
+    auto metadata = ReadFromImageBlob(data, size);
+    if (!metadata) {
+        return false;
+    }
+    return Open(*metadata.get());
+}
+
+bool SuperLayoutBuilder::Open(const LpMetadata& metadata) {
+    for (const auto& partition : metadata.partitions) {
+        if (partition.attributes & LP_PARTITION_ATTR_SLOT_SUFFIXED) {
+            // Retrofit devices are not supported.
+            return false;
+        }
+        if (!(partition.attributes & LP_PARTITION_ATTR_READONLY)) {
+            // Writable partitions are not supported.
+            return false;
+        }
+    }
+    if (!metadata.extents.empty()) {
+        // Partitions that already have extents are not supported (should
+        // never be true of super_empty.img).
+        return false;
+    }
+    if (metadata.block_devices.size() != 1) {
+        // Only one "super" is supported.
+        return false;
+    }
+
+    builder_ = MetadataBuilder::New(metadata);
+    return !!builder_;
+}
+
+bool SuperLayoutBuilder::AddPartition(const std::string& partition_name,
+                                      const std::string& image_name, uint64_t partition_size) {
+    auto p = builder_->FindPartition(partition_name);
+    if (!p) {
+        return false;
+    }
+    if (!builder_->ResizePartition(p, partition_size)) {
+        return false;
+    }
+    image_map_.emplace(partition_name, image_name);
+    return true;
+}
+
+// Fill the space between each extent, if any, with either a fill or dontcare
+// extent. The caller constructs a sample extent to re-use.
+static bool AddGapExtents(std::vector<SuperImageExtent>* extents, SuperImageExtent::Type gap_type) {
+    std::vector<SuperImageExtent> old = std::move(*extents);
+    std::sort(old.begin(), old.end());
+
+    *extents = {};
+
+    uint64_t current_offset = 0;
+    for (const auto& extent : old) {
+        // Check for overlapping extents - this would be a serious error.
+        if (current_offset > extent.offset) {
+            LOG(INFO) << "Overlapping extents detected; cannot layout temporary super image";
+            return false;
+        }
+
+        if (extent.offset != current_offset) {
+            uint64_t gap_size = extent.offset - current_offset;
+            extents->emplace_back(current_offset, gap_size, gap_type);
+            current_offset = extent.offset;
+        }
+
+        extents->emplace_back(extent);
+        current_offset += extent.size;
+    }
+    return true;
+}
+
+std::vector<SuperImageExtent> SuperLayoutBuilder::GetImageLayout() {
+    auto metadata = builder_->Export();
+    if (!metadata) {
+        return {};
+    }
+
+    std::vector<SuperImageExtent> extents;
+
+    // Write the primary and backup copies of geometry.
+    std::string geometry_bytes = SerializeGeometry(metadata->geometry);
+    auto blob = std::make_shared<std::string>(std::move(geometry_bytes));
+
+    extents.emplace_back(0, GetPrimaryGeometryOffset(), SuperImageExtent::Type::ZERO);
+    extents.emplace_back(GetPrimaryGeometryOffset(), blob);
+    extents.emplace_back(GetBackupGeometryOffset(), blob);
+
+    // Write the primary and backup copies of each metadata slot. When flashing,
+    // all metadata copies are the same, even for different slots.
+    std::string metadata_bytes = SerializeMetadata(*metadata.get());
+
+    // Align metadata size to 4KB. This makes the layout easily compatible with
+    // libsparse.
+    static constexpr size_t kSparseAlignment = 4096;
+    size_t metadata_aligned_bytes;
+    if (!AlignTo(metadata_bytes.size(), kSparseAlignment, &metadata_aligned_bytes)) {
+        LOG(ERROR) << "Unable to align metadata size " << metadata_bytes.size() << " to "
+                   << kSparseAlignment;
+        return {};
+    }
+    metadata_bytes.resize(metadata_aligned_bytes, '\0');
+
+    // However, alignment can cause larger-than-supported metadata blocks. Fall
+    // back to fastbootd/update-super.
+    if (metadata_bytes.size() > metadata->geometry.metadata_max_size) {
+        LOG(VERBOSE) << "Aligned metadata size " << metadata_bytes.size()
+                     << " is larger than maximum metadata size "
+                     << metadata->geometry.metadata_max_size;
+        return {};
+    }
+
+    blob = std::make_shared<std::string>(std::move(metadata_bytes));
+    for (uint32_t i = 0; i < metadata->geometry.metadata_slot_count; i++) {
+        int64_t metadata_primary = GetPrimaryMetadataOffset(metadata->geometry, i);
+        int64_t metadata_backup = GetBackupMetadataOffset(metadata->geometry, i);
+        extents.emplace_back(metadata_primary, blob);
+        extents.emplace_back(metadata_backup, blob);
+    }
+
+    // Add extents for each partition.
+    for (const auto& partition : metadata->partitions) {
+        auto partition_name = GetPartitionName(partition);
+        auto image_name_iter = image_map_.find(partition_name);
+        if (image_name_iter == image_map_.end()) {
+            if (partition.num_extents != 0) {
+                LOG(ERROR) << "Partition " << partition_name
+                           << " has extents but no image filename";
+                return {};
+            }
+            continue;
+        }
+        const auto& image_name = image_name_iter->second;
+
+        uint64_t image_offset = 0;
+        for (uint32_t i = 0; i < partition.num_extents; i++) {
+            const auto& e = metadata->extents[partition.first_extent_index + i];
+
+            if (e.target_type != LP_TARGET_TYPE_LINEAR) {
+                // Any type other than LINEAR isn't understood here. We don't even
+                // bother with ZERO, which is never generated.
+                LOG(INFO) << "Unknown extent type from liblp: " << e.target_type;
+                return {};
+            }
+
+            size_t size = e.num_sectors * LP_SECTOR_SIZE;
+            uint64_t super_offset = e.target_data * LP_SECTOR_SIZE;
+            extents.emplace_back(super_offset, size, image_name, image_offset);
+
+            image_offset += size;
+        }
+    }
+
+    if (!AddGapExtents(&extents, SuperImageExtent::Type::DONTCARE)) {
+        return {};
+    }
+    return extents;
+}
+
+bool SuperImageExtent::operator==(const SuperImageExtent& other) const {
+    if (offset != other.offset) {
+        return false;
+    }
+    if (size != other.size) {
+        return false;
+    }
+    if (type != other.type) {
+        return false;
+    }
+    switch (type) {
+        case Type::DATA:
+            return *blob == *other.blob;
+        case Type::PARTITION:
+            return image_name == other.image_name && image_offset == other.image_offset;
+        default:
+            return true;
+    }
+}
+
+std::ostream& operator<<(std::ostream& stream, const SuperImageExtent& extent) {
+    stream << "extent:" << extent.offset << ":" << extent.size << ":";
+    switch (extent.type) {
+        case SuperImageExtent::Type::DATA:
+            stream << "data";
+            break;
+        case SuperImageExtent::Type::PARTITION:
+            stream << "partition:" << extent.image_name << ":" << extent.image_offset;
+            break;
+        case SuperImageExtent::Type::ZERO:
+            stream << "zero";
+            break;
+        case SuperImageExtent::Type::DONTCARE:
+            stream << "dontcare";
+            break;
+        default:
+            stream << "invalid";
+    }
+    return stream;
+}
+
+}  // namespace fs_mgr
+}  // namespace android
diff --git a/fs_mgr/liblp/super_layout_builder_test.cpp b/fs_mgr/liblp/super_layout_builder_test.cpp
new file mode 100644
index 0000000..714b6b4
--- /dev/null
+++ b/fs_mgr/liblp/super_layout_builder_test.cpp
@@ -0,0 +1,141 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <liblp/builder.h>
+#include <liblp/super_layout_builder.h>
+#include <storage_literals/storage_literals.h>
+
+#include "images.h"
+#include "writer.h"
+
+using namespace android::fs_mgr;
+using namespace android::storage_literals;
+
+TEST(SuperImageTool, Layout) {
+    auto builder = MetadataBuilder::New(4_MiB, 8_KiB, 2);
+    ASSERT_NE(builder, nullptr);
+
+    Partition* p = builder->AddPartition("system_a", LP_PARTITION_ATTR_READONLY);
+    ASSERT_NE(p, nullptr);
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    SuperLayoutBuilder tool;
+    ASSERT_TRUE(tool.Open(*metadata.get()));
+    ASSERT_TRUE(tool.AddPartition("system_a", "system.img", 16_KiB));
+
+    // Get a copy of the metadata we'd expect if flashing.
+    ASSERT_TRUE(builder->ResizePartition(p, 16_KiB));
+    metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    auto geometry_blob = std::make_shared<std::string>(SerializeGeometry(metadata->geometry));
+    auto metadata_blob = std::make_shared<std::string>(SerializeMetadata(*metadata.get()));
+    metadata_blob->resize(4_KiB, '\0');
+
+    auto extents = tool.GetImageLayout();
+    ASSERT_EQ(extents.size(), 12);
+    EXPECT_EQ(extents[0], SuperImageExtent(0, 4096, SuperImageExtent::Type::ZERO));
+    EXPECT_EQ(extents[1], SuperImageExtent(4096, geometry_blob));
+    EXPECT_EQ(extents[2], SuperImageExtent(8192, geometry_blob));
+    EXPECT_EQ(extents[3], SuperImageExtent(12288, metadata_blob));
+    EXPECT_EQ(extents[4], SuperImageExtent(16384, 4096, SuperImageExtent::Type::DONTCARE));
+    EXPECT_EQ(extents[5], SuperImageExtent(20480, metadata_blob));
+    EXPECT_EQ(extents[6], SuperImageExtent(24576, 4096, SuperImageExtent::Type::DONTCARE));
+    EXPECT_EQ(extents[7], SuperImageExtent(28672, metadata_blob));
+    EXPECT_EQ(extents[8], SuperImageExtent(32768, 4096, SuperImageExtent::Type::DONTCARE));
+    EXPECT_EQ(extents[9], SuperImageExtent(36864, metadata_blob));
+    EXPECT_EQ(extents[10], SuperImageExtent(40960, 4096, SuperImageExtent::Type::DONTCARE));
+    EXPECT_EQ(extents[11], SuperImageExtent(45056, 16384, "system.img", 0));
+}
+
+TEST(SuperImageTool, NoWritablePartitions) {
+    auto builder = MetadataBuilder::New(4_MiB, 8_KiB, 2);
+    ASSERT_NE(builder, nullptr);
+
+    Partition* p = builder->AddPartition("system_a", 0);
+    ASSERT_NE(p, nullptr);
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    SuperLayoutBuilder tool;
+    ASSERT_FALSE(tool.Open(*metadata.get()));
+}
+
+TEST(SuperImageTool, NoRetrofit) {
+    auto builder = MetadataBuilder::New(4_MiB, 8_KiB, 2);
+    ASSERT_NE(builder, nullptr);
+
+    Partition* p = builder->AddPartition("system_a", LP_PARTITION_ATTR_READONLY);
+    ASSERT_NE(p, nullptr);
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    // Add an extra block device.
+    metadata->block_devices.emplace_back(metadata->block_devices[0]);
+
+    SuperLayoutBuilder tool;
+    ASSERT_FALSE(tool.Open(*metadata.get()));
+}
+
+TEST(SuperImageTool, NoRetrofit2) {
+    auto builder = MetadataBuilder::New(4_MiB, 8_KiB, 2);
+    ASSERT_NE(builder, nullptr);
+
+    Partition* p = builder->AddPartition(
+            "system_a", LP_PARTITION_ATTR_READONLY | LP_PARTITION_ATTR_SLOT_SUFFIXED);
+    ASSERT_NE(p, nullptr);
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    SuperLayoutBuilder tool;
+    ASSERT_FALSE(tool.Open(*metadata.get()));
+}
+
+TEST(SuperImageTool, NoFixedPartitions) {
+    auto builder = MetadataBuilder::New(4_MiB, 8_KiB, 2);
+    ASSERT_NE(builder, nullptr);
+
+    Partition* p = builder->AddPartition("system_a", LP_PARTITION_ATTR_READONLY);
+    ASSERT_NE(p, nullptr);
+    ASSERT_TRUE(builder->ResizePartition(p, 4_KiB));
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    SuperLayoutBuilder tool;
+    ASSERT_FALSE(tool.Open(*metadata.get()));
+}
+
+TEST(SuperImageTool, LargeAlignedMetadata) {
+    auto builder = MetadataBuilder::New(4_MiB, 512, 2);
+    ASSERT_NE(builder, nullptr);
+
+    auto metadata = builder->Export();
+    ASSERT_NE(metadata, nullptr);
+
+    SuperLayoutBuilder tool;
+    ASSERT_TRUE(tool.Open(*metadata.get()));
+
+    auto extents = tool.GetImageLayout();
+    ASSERT_TRUE(extents.empty());
+}