// Copyright (C) 2020 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 <ftw.h>
#include <inttypes.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sysexits.h>

#include <chrono>
#include <string>

#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <fs_mgr.h>
#include <libsnapshot/auto_device.h>
#include <libsnapshot/snapshot.h>
#include <storage_literals/storage_literals.h>

#include "snapshot_fuzz_utils.h"
#include "utility.h"

// Prepends the errno string, but it is good enough.
#ifndef PCHECK
#define PCHECK(x) CHECK(x) << strerror(errno) << ": "
#endif

using namespace android::storage_literals;
using namespace std::chrono_literals;
using namespace std::string_literals;

using android::base::Basename;
using android::base::ReadFileToString;
using android::base::SetProperty;
using android::base::Split;
using android::base::StartsWith;
using android::base::StringPrintf;
using android::base::unique_fd;
using android::base::WriteStringToFile;
using android::dm::DeviceMapper;
using android::dm::DmTarget;
using android::dm::LoopControl;
using android::fiemap::IImageManager;
using android::fiemap::ImageManager;
using android::fs_mgr::BlockDeviceInfo;
using android::fs_mgr::FstabEntry;
using android::fs_mgr::IPartitionOpener;
using chromeos_update_engine::DynamicPartitionMetadata;

static const char MNT_DIR[] = "/mnt";
static const char BLOCK_SYSFS[] = "/sys/block";

static const char FAKE_ROOT_NAME[] = "snapshot_fuzz";
static const auto SUPER_IMAGE_SIZE = 16_MiB;
static const auto DATA_IMAGE_SIZE = 16_MiB;
static const auto FAKE_ROOT_SIZE = 64_MiB;

namespace android::snapshot {

bool Mkdir(const std::string& path) {
    if (mkdir(path.c_str(), 0750) == -1 && errno != EEXIST) {
        PLOG(ERROR) << "Cannot create " << path;
        return false;
    }
    return true;
}

bool RmdirRecursive(const std::string& path) {
    auto callback = [](const char* child, const struct stat*, int file_type, struct FTW*) -> int {
        switch (file_type) {
            case FTW_D:
            case FTW_DP:
            case FTW_DNR:
                if (rmdir(child) == -1) {
                    PLOG(ERROR) << "rmdir " << child;
                    return -1;
                }
                return 0;
            case FTW_NS:
            default:
                if (rmdir(child) != -1) break;
                [[fallthrough]];
            case FTW_F:
            case FTW_SL:
            case FTW_SLN:
                if (unlink(child) == -1) {
                    PLOG(ERROR) << "unlink " << child;
                    return -1;
                }
                return 0;
        }
        return 0;
    };

    return nftw(path.c_str(), callback, 128, FTW_DEPTH | FTW_MOUNT | FTW_PHYS) == 0;
}

std::string GetLinearBaseDeviceString(const DeviceMapper::TargetInfo& target) {
    if (target.spec.target_type != "linear"s) return {};
    auto tokens = Split(target.data, " ");
    CHECK_EQ(2, tokens.size());
    return tokens[0];
}

std::vector<std::string> GetSnapshotBaseDeviceStrings(const DeviceMapper::TargetInfo& target) {
    if (target.spec.target_type != "snapshot"s && target.spec.target_type != "snapshot-merge"s)
        return {};
    auto tokens = Split(target.data, " ");
    CHECK_EQ(4, tokens.size());
    return {tokens[0], tokens[1]};
}

bool ShouldDeleteLoopDevice(const std::string& node) {
    std::string backing_file;
    if (ReadFileToString(StringPrintf("%s/loop/backing_file", node.data()), &backing_file)) {
        if (StartsWith(backing_file, std::string(MNT_DIR) + "/" + FAKE_ROOT_NAME)) {
            return true;
        }
    }
    return false;
}

std::vector<DeviceMapper::TargetInfo> GetTableInfoIfExists(const std::string& dev_name) {
    auto& dm = DeviceMapper::Instance();
    std::vector<DeviceMapper::TargetInfo> table;
    if (!dm.GetTableInfo(dev_name, &table)) {
        PCHECK(errno == ENODEV || errno == ENXIO);
        return {};
    }
    return table;
}

std::set<std::string> GetAllBaseDeviceStrings(const std::string& child_dev) {
    std::set<std::string> ret;
    for (const auto& child_target : GetTableInfoIfExists(child_dev)) {
        auto snapshot_bases = GetSnapshotBaseDeviceStrings(child_target);
        ret.insert(snapshot_bases.begin(), snapshot_bases.end());

        auto linear_base = GetLinearBaseDeviceString(child_target);
        if (!linear_base.empty()) {
            ret.insert(linear_base);
        }
    }
    return ret;
}

using PropertyList = std::set<std::string>;
void InsertProperty(const char* key, const char* /*name*/, void* cookie) {
    reinterpret_cast<PropertyList*>(cookie)->insert(key);
}

// Attempt to delete all devices that is based on dev_name, including itself.
void CheckDeleteDeviceMapperTree(const std::string& dev_name, bool known_allow_delete = false,
                                 uint64_t depth = 100) {
    CHECK(depth > 0) << "Reaching max depth when deleting " << dev_name
                     << ". There may be devices referencing itself. Check `dmctl list devices -v`.";

    auto& dm = DeviceMapper::Instance();
    auto table = GetTableInfoIfExists(dev_name);
    if (table.empty()) {
        PCHECK(dm.DeleteDeviceIfExists(dev_name)) << dev_name;
        return;
    }

    if (!known_allow_delete) {
        for (const auto& target : table) {
            auto base_device_string = GetLinearBaseDeviceString(target);
            if (base_device_string.empty()) continue;
            if (ShouldDeleteLoopDevice(
                        StringPrintf("/sys/dev/block/%s", base_device_string.data()))) {
                known_allow_delete = true;
                break;
            }
        }
    }
    if (!known_allow_delete) {
        return;
    }

    std::string dev_string;
    PCHECK(dm.GetDeviceString(dev_name, &dev_string));

    std::vector<DeviceMapper::DmBlockDevice> devices;
    PCHECK(dm.GetAvailableDevices(&devices));
    for (const auto& child_dev : devices) {
        auto child_bases = GetAllBaseDeviceStrings(child_dev.name());
        if (child_bases.find(dev_string) != child_bases.end()) {
            CheckDeleteDeviceMapperTree(child_dev.name(), true /* known_allow_delete */, depth - 1);
        }
    }

    PCHECK(dm.DeleteDeviceIfExists(dev_name)) << dev_name;
}

// Attempt to clean up residues from previous runs.
void CheckCleanupDeviceMapperDevices() {
    auto& dm = DeviceMapper::Instance();
    std::vector<DeviceMapper::DmBlockDevice> devices;
    PCHECK(dm.GetAvailableDevices(&devices));

    for (const auto& dev : devices) {
        CheckDeleteDeviceMapperTree(dev.name());
    }
}

void CheckUmount(const std::string& path) {
    PCHECK(TEMP_FAILURE_RETRY(umount(path.data()) == 0) || errno == ENOENT || errno == EINVAL)
            << path;
}

void CheckDetachLoopDevices(const std::set<std::string>& exclude_names = {}) {
    // ~SnapshotFuzzEnv automatically does the following.
    std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(BLOCK_SYSFS), closedir);
    PCHECK(dir != nullptr) << BLOCK_SYSFS;
    LoopControl loop_control;
    dirent* dp;
    while ((dp = readdir(dir.get())) != nullptr) {
        if (exclude_names.find(dp->d_name) != exclude_names.end()) {
            continue;
        }
        if (!ShouldDeleteLoopDevice(StringPrintf("%s/%s", BLOCK_SYSFS, dp->d_name).data())) {
            continue;
        }
        PCHECK(loop_control.Detach(StringPrintf("/dev/block/%s", dp->d_name).data()));
    }
}

void CheckUmountAll() {
    CheckUmount(std::string(MNT_DIR) + "/snapshot_fuzz_data");
    CheckUmount(std::string(MNT_DIR) + "/" + FAKE_ROOT_NAME);
}

class AutoDeleteDir : public AutoDevice {
  public:
    static std::unique_ptr<AutoDeleteDir> New(const std::string& path) {
        if (!Mkdir(path)) {
            return std::unique_ptr<AutoDeleteDir>(new AutoDeleteDir(""));
        }
        return std::unique_ptr<AutoDeleteDir>(new AutoDeleteDir(path));
    }
    ~AutoDeleteDir() {
        if (!HasDevice()) return;
        PCHECK(rmdir(name_.c_str()) == 0 || errno == ENOENT) << name_;
    }

  private:
    AutoDeleteDir(const std::string& path) : AutoDevice(path) {}
};

class AutoUnmount : public AutoDevice {
  public:
    ~AutoUnmount() {
        if (!HasDevice()) return;
        CheckUmount(name_);
    }
    AutoUnmount(const std::string& path) : AutoDevice(path) {}
};

class AutoUnmountTmpfs : public AutoUnmount {
  public:
    static std::unique_ptr<AutoUnmount> New(const std::string& path, uint64_t size) {
        if (mount("tmpfs", path.c_str(), "tmpfs", 0,
                  (void*)StringPrintf("size=%" PRIu64, size).data()) == -1) {
            PLOG(ERROR) << "Cannot mount " << path;
            return std::unique_ptr<AutoUnmount>(new AutoUnmount(""));
        }
        return std::unique_ptr<AutoUnmount>(new AutoUnmount(path));
    }
  private:
    using AutoUnmount::AutoUnmount;
};

// A directory on tmpfs. Upon destruct, it is unmounted and deleted.
class AutoMemBasedDir : public AutoDevice {
  public:
    static std::unique_ptr<AutoMemBasedDir> New(const std::string& name, uint64_t size) {
        auto ret = std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(name));
        ret->auto_delete_mount_dir_ = AutoDeleteDir::New(ret->mount_path());
        if (!ret->auto_delete_mount_dir_->HasDevice()) {
            return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
        }
        ret->auto_umount_mount_point_ = AutoUnmountTmpfs::New(ret->mount_path(), size);
        if (!ret->auto_umount_mount_point_->HasDevice()) {
            return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
        }
        // tmp_path() and persist_path does not need to be deleted upon destruction, hence it is
        // not wrapped with AutoDeleteDir.
        if (!Mkdir(ret->tmp_path())) {
            return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
        }
        if (!Mkdir(ret->persist_path())) {
            return std::unique_ptr<AutoMemBasedDir>(new AutoMemBasedDir(""));
        }
        return ret;
    }
    // Return the temporary scratch directory.
    std::string tmp_path() const {
        CHECK(HasDevice());
        return mount_path() + "/tmp";
    }
    // Return the temporary scratch directory.
    std::string persist_path() const {
        CHECK(HasDevice());
        return mount_path() + "/persist";
    }
    // Delete all contents in tmp_path() and start over. tmp_path() itself is re-created.
    void CheckSoftReset() {
        PCHECK(RmdirRecursive(tmp_path()));
        PCHECK(Mkdir(tmp_path()));
    }

  private:
    AutoMemBasedDir(const std::string& name) : AutoDevice(name) {}
    std::string mount_path() const {
        CHECK(HasDevice());
        return MNT_DIR + "/"s + name_;
    }
    std::unique_ptr<AutoDeleteDir> auto_delete_mount_dir_;
    std::unique_ptr<AutoUnmount> auto_umount_mount_point_;
};

SnapshotFuzzEnv::SnapshotFuzzEnv() {
    CheckCleanupDeviceMapperDevices();
    CheckDetachLoopDevices();
    CheckUmountAll();

    fake_root_ = AutoMemBasedDir::New(FAKE_ROOT_NAME, FAKE_ROOT_SIZE);
    CHECK(fake_root_ != nullptr);
    CHECK(fake_root_->HasDevice());
    loop_control_ = std::make_unique<LoopControl>();

    fake_data_mount_point_ = MNT_DIR + "/snapshot_fuzz_data"s;
    auto_delete_data_mount_point_ = AutoDeleteDir::New(fake_data_mount_point_);
    CHECK(auto_delete_data_mount_point_ != nullptr);
    CHECK(auto_delete_data_mount_point_->HasDevice());

    const auto& fake_persist_path = fake_root_->persist_path();
    mapped_super_ = CheckMapImage(fake_persist_path + "/super.img", SUPER_IMAGE_SIZE,
                                  loop_control_.get(), &fake_super_);
    mapped_data_ = CheckMapImage(fake_persist_path + "/data.img", DATA_IMAGE_SIZE,
                                 loop_control_.get(), &fake_data_block_device_);
    mounted_data_ = CheckMountFormatData(fake_data_block_device_, fake_data_mount_point_);
}

SnapshotFuzzEnv::~SnapshotFuzzEnv() {
    CheckCleanupDeviceMapperDevices();
    mounted_data_ = nullptr;
    auto_delete_data_mount_point_ = nullptr;
    mapped_data_ = nullptr;
    mapped_super_ = nullptr;
    CheckDetachLoopDevices();
    loop_control_ = nullptr;
    fake_root_ = nullptr;
    CheckUmountAll();
}

void CheckZeroFill(const std::string& file, size_t size) {
    std::string zeros(size, '\0');
    PCHECK(WriteStringToFile(zeros, file)) << "Cannot write zeros to " << file;
}

void SnapshotFuzzEnv::CheckSoftReset() {
    fake_root_->CheckSoftReset();
    CheckZeroFill(super(), SUPER_IMAGE_SIZE);
    CheckCleanupDeviceMapperDevices();
    CheckDetachLoopDevices({Basename(fake_super_), Basename(fake_data_block_device_)});
}

std::unique_ptr<IImageManager> SnapshotFuzzEnv::CheckCreateFakeImageManager() {
    auto metadata_dir = fake_root_->tmp_path() + "/images_manager_metadata";
    auto data_dir = fake_data_mount_point_ + "/image_manager_data";
    PCHECK(Mkdir(metadata_dir));
    PCHECK(Mkdir(data_dir));
    return SnapshotFuzzImageManager::Open(metadata_dir, data_dir);
}

// Helper to create a loop device for a file.
static void CheckCreateLoopDevice(LoopControl* control, const std::string& file,
                                  const std::chrono::milliseconds& timeout_ms, std::string* path) {
    static constexpr int kOpenFlags = O_RDWR | O_NOFOLLOW | O_CLOEXEC;
    android::base::unique_fd file_fd(open(file.c_str(), kOpenFlags));
    PCHECK(file_fd >= 0) << "Could not open file: " << file;
    CHECK(control->Attach(file_fd, timeout_ms, path))
            << "Could not create loop device for: " << file;
}

class AutoDetachLoopDevice : public AutoDevice {
  public:
    AutoDetachLoopDevice(LoopControl* control, const std::string& device)
        : AutoDevice(device), control_(control) {}
    ~AutoDetachLoopDevice() { PCHECK(control_->Detach(name_)) << name_; }

  private:
    LoopControl* control_;
};

std::unique_ptr<AutoDevice> SnapshotFuzzEnv::CheckMapImage(const std::string& img_path,
                                                           uint64_t size, LoopControl* control,
                                                           std::string* mapped_path) {
    CheckZeroFill(img_path, size);
    CheckCreateLoopDevice(control, img_path, 1s, mapped_path);

    return std::make_unique<AutoDetachLoopDevice>(control, *mapped_path);
}

SnapshotTestModule SnapshotFuzzEnv::CheckCreateSnapshotManager(const SnapshotFuzzData& data) {
    SnapshotTestModule ret;
    auto partition_opener = std::make_unique<TestPartitionOpener>(super());
    ret.opener = partition_opener.get();
    CheckWriteSuperMetadata(data, *partition_opener);
    auto metadata_dir = fake_root_->tmp_path() + "/snapshot_metadata";
    PCHECK(Mkdir(metadata_dir));
    if (data.has_metadata_snapshots_dir()) {
        PCHECK(Mkdir(metadata_dir + "/snapshots"));
    }

    ret.device_info = new SnapshotFuzzDeviceInfo(this, data.device_info_data(),
                                                 std::move(partition_opener), metadata_dir);
    auto snapshot = SnapshotManager::New(ret.device_info /* takes ownership */);
    ret.snapshot = std::move(snapshot);

    return ret;
}

const std::string& SnapshotFuzzEnv::super() const {
    return fake_super_;
}

void SnapshotFuzzEnv::CheckWriteSuperMetadata(const SnapshotFuzzData& data,
                                              const IPartitionOpener& opener) {
    if (!data.is_super_metadata_valid()) {
        // Leave it zero.
        return;
    }

    BlockDeviceInfo super_device("super", SUPER_IMAGE_SIZE, 0, 0, 4096);
    std::vector<BlockDeviceInfo> devices = {super_device};
    auto builder = MetadataBuilder::New(devices, "super", 65536, 2);
    CHECK(builder != nullptr);

    // Attempt to create a super partition metadata using proto. All errors are ignored.
    for (const auto& group_proto : data.super_data().dynamic_partition_metadata().groups()) {
        (void)builder->AddGroup(group_proto.name(), group_proto.size());
        for (const auto& partition_name : group_proto.partition_names()) {
            (void)builder->AddPartition(partition_name, group_proto.name(),
                                        LP_PARTITION_ATTR_READONLY);
        }
    }

    for (const auto& partition_proto : data.super_data().partitions()) {
        auto p = builder->FindPartition(partition_proto.partition_name());
        if (p == nullptr) continue;
        (void)builder->ResizePartition(p, partition_proto.new_partition_info().size());
    }

    auto metadata = builder->Export();
    // metadata may be nullptr if it is not valid (e.g. partition name too long).
    // In this case, just use empty super partition data.
    if (metadata == nullptr) {
        builder = MetadataBuilder::New(devices, "super", 65536, 2);
        CHECK(builder != nullptr);
        metadata = builder->Export();
        CHECK(metadata != nullptr);
    }
    CHECK(FlashPartitionTable(opener, super(), *metadata.get()));
}

std::unique_ptr<AutoDevice> SnapshotFuzzEnv::CheckMountFormatData(const std::string& blk_device,
                                                                  const std::string& mount_point) {
    FstabEntry entry{
            .blk_device = blk_device,
            .length = static_cast<off64_t>(DATA_IMAGE_SIZE),
            .fs_type = "ext4",
            .mount_point = mount_point,
    };
    CHECK(0 == fs_mgr_do_format(entry));
    CHECK(0 == fs_mgr_do_mount_one(entry));
    return std::make_unique<AutoUnmount>(mount_point);
}

SnapshotFuzzImageManager::~SnapshotFuzzImageManager() {
    // Remove relevant gsid.mapped_images.* props.
    for (const auto& name : mapped_) {
        CHECK(UnmapImageIfExists(name)) << "Cannot unmap " << name;
    }
}

bool SnapshotFuzzImageManager::MapImageDevice(const std::string& name,
                                              const std::chrono::milliseconds& timeout_ms,
                                              std::string* path) {
    if (impl_->MapImageDevice(name, timeout_ms, path)) {
        mapped_.insert(name);
        return true;
    }
    return false;
}

}  // namespace android::snapshot
