/*
 * Copyright (C) 2022 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 "checkpoint.h"
#include "block_allocator.h"
#include "block_cache.h"
#include "block_mac.h"
#include "debug.h"
#include "transaction.h"

#define CHECKPOINT_MAGIC (0x0063797473757274) /* trustyc\0 */

/**
 * struct checkpoint - On-disk block containing the checkpoint metadata
 * @iv:             Initial value used for encrypt/decrypt
 * @magic:          CHECKPOINT_MAGIC
 * @files:          Block and mac of checkpointed files tree root node
 * @free:           Block and mac of checkpointed free set root node. When a
 *                  checkpoint is active blocks may only be allocated if they
 *                  are marked as free in both the filesystem free set and this
 *                  checkpointed free set.
 */
struct checkpoint {
    struct iv iv;
    uint64_t magic;
    struct block_mac files;
    struct block_mac free;
};

/**
 * checkpoint_get_new_block - Get a new, writable copy of the checkpoint block
 * metadata
 * @tr:                 Transaction object.
 * @new_checkpoint_ref: Output pointer to hold the block reference for the new
 *                      block
 * @checkpoint_mac:     Pointer to the current checkpoint block mac.
 * Updated with the block number of the new checkpoint block on success.
 *
 * Returns a new, writable copy of the checkpoint metadata block, or %NULL on
 * failure (tr->failed will be set). The returned pointer should then be passed
 * to checkpoint_update_roots() after the file tree and free set are finalized.
 * We have to split this operation in two so that the newly allocated block will
 * be removed from the free set.
 *
 * Caller takes ownership of the returned new, dirty block and is responsible
 * for releasing @new_checkpoint_ref.
 */
struct checkpoint* checkpoint_get_new_block(struct transaction* tr,
                                            struct obj_ref* new_checkpoint_ref,
                                            struct block_mac* checkpoint_mac) {
    data_block_t new_checkpoint_block;
    struct checkpoint* new_checkpoint;

    new_checkpoint_block = block_allocate(tr);
    if (tr->failed) {
        pr_warn("transaction failed, abort\n");
        return NULL;
    }
    assert(new_checkpoint_block);

    if (block_mac_valid(tr, checkpoint_mac)) {
        block_free(tr, block_mac_to_block(tr, checkpoint_mac));
    }
    new_checkpoint = block_get_cleared(tr, new_checkpoint_block, false,
                                       new_checkpoint_ref);

    block_mac_set_block(tr, checkpoint_mac, new_checkpoint_block);

    new_checkpoint->magic = CHECKPOINT_MAGIC;

    return new_checkpoint;
}

/**
 * checkpoint_update_roots - Update the files and free blocks of a checkpoint
 * @tr:             Transaction object.
 * @new_checkpoint: Pointer to a checkpoint metadata block returned by
 *                  checkpoint_get_new_block()
 * @files:          New checkpoint files tree root node.
 * @free:           New checkpoint free set root node.
 */
void checkpoint_update_roots(struct transaction* tr,
                             struct checkpoint* new_checkpoint,
                             const struct block_mac* files,
                             const struct block_mac* free) {
    new_checkpoint->files = *files;
    new_checkpoint->free = *free;
}

/**
 * checkpoint_read - Initialize root blocks from a checkpoint page
 * @fs:             File-system to initialize checkpoint state in.
 * @checkpoint:     Checkpoint root page block and mac. Must be a valid block.
 * @files:          New checkpoint file tree. May be %NULL.
 * @free:           New checkpoint free set. May be %NULL.
 *
 * Returns %true if the @files and @free nodes were properly populated from the
 * fields in @checkpoint. Either @files or @free may be %NULL; %NULL out params
 * will not be set. Returns %false and does not change @files or @free if the
 * @checkpoint metadata page exists but could not be read.
 *
 * Example: checkpoint_read(tr, &tr->fs->checkpoint, &files,
 *                          &tr->fs->checkpoint_free)
 */
bool checkpoint_read(struct transaction* tr,
                     const struct block_mac* checkpoint,
                     struct block_tree* files,
                     struct block_set* free) {
    const struct checkpoint* checkpoint_ro;
    struct obj_ref checkpoint_ro_ref = OBJ_REF_INITIAL_VALUE(checkpoint_ro_ref);

    assert(block_mac_valid(tr, checkpoint));

    checkpoint_ro = block_get(tr, checkpoint, NULL, &checkpoint_ro_ref);
    if (tr->failed) {
        goto err_block_get;
    }

    if (checkpoint_ro->magic != CHECKPOINT_MAGIC) {
        pr_err("Checkpoint magic mismatch!\n");
        transaction_fail(tr);
        goto err_magic_mismatch;
    }

    if (files) {
        files->root = checkpoint_ro->files;
    }
    if (free) {
        free->block_tree.root = checkpoint_ro->free;
        block_range_clear(&free->initial_range);
    }

err_magic_mismatch:
    block_put(checkpoint_ro, &checkpoint_ro_ref);
err_block_get:
    return !tr->failed;
}

/**
 * checkpoint_commit - Save the current file-system state as a checkpoint
 * @fs:             File-system to checkpoint.
 *
 * Create and commit a checkpoint of the current state of @fs.
 *
 * Returns %true if the checkpoint was created and committed successfully,
 * %false otherwise.
 */
bool checkpoint_commit(struct fs* fs) {
    struct transaction tr;
    bool success;

    assert(fs);
    transaction_init(&tr, fs, true);
    transaction_complete_etc(&tr, true);
    success = !tr.failed;
    if (success) {
        pr_init("Automatically created a checkpoint for filesystem %s\n",
                fs->name);
    } else {
        pr_err("Failed to commit checkpoint for filesystem %s\n", fs->name);
    }
    transaction_free(&tr);
    return success;
}