/*
 * 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 "client.h"

#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>

#include <interface/storage/storage.h>
#include <lib/system_state/system_state.h>
#include <lk/list.h>

#include "client_session.h"
#include "file.h"
#include "storage_limits.h"

// macros to help manage debug output
#define SS_ERR(args...) fprintf(stderr, "ss: " args)

#if 0
// this can generate a lot of spew on debug builds
#define SS_INFO(args...) fprintf(stderr, "ss: " args)
#else
#define SS_INFO(args...) \
    do {                 \
    } while (0)
#endif

/**
 * checkpoint_update_allowed - Is checkpoint modification currently allowed?
 * @transaction:    Transaction object.
 *
 * Check if the given transaction is allowed to create a new or update the
 * existing checkpoint for its file system. Checkpoint updates may only be
 * requested by client while the device is in provisioning mode.
 *
 * Returns %true if a transaction may update the current checkpoint state,
 * %false otherwise.
 */
static bool checkpoint_update_allowed(struct transaction* tr) {
    return system_state_provisioning_allowed();
}

/*
 * Legal secure storage directory and file names contain only
 * characters from the following set: [a-z][A-Z][0-9][.-_]
 *
 * It is not null terminated.
 */
static int is_valid_name(const char* name, size_t name_len) {
    size_t i;

    if (!name_len)
        return 0;

    for (i = 0; i < name_len; i++) {
        if ((name[i] >= 'a') && (name[i] <= 'z'))
            continue;
        if ((name[i] >= 'A') && (name[i] <= 'Z'))
            continue;
        if ((name[i] >= '0') && (name[i] <= '9'))
            continue;
        if ((name[i] == '.') || (name[i] == '-') || (name[i] == '_'))
            continue;

        // not a legal character so reject this name
        return 0;
    }

    return 1;
}

static int get_path(char* path_out,
                    size_t path_out_size,
                    const uuid_t* uuid,
                    const char* file_name,
                    size_t file_name_len) {
    unsigned int rc;

    rc = snprintf(path_out, path_out_size,
                  "%08x%04x%04x%02x%02x%02x%02x%02x%02x%02x%02x/",
                  uuid->time_low, uuid->time_mid, uuid->time_hi_and_version,
                  uuid->clock_seq_and_node[0], uuid->clock_seq_and_node[1],
                  uuid->clock_seq_and_node[2], uuid->clock_seq_and_node[3],
                  uuid->clock_seq_and_node[4], uuid->clock_seq_and_node[5],
                  uuid->clock_seq_and_node[6], uuid->clock_seq_and_node[7]);

    if (rc + file_name_len >= path_out_size) {
        return STORAGE_ERR_NOT_VALID;
    }

    memcpy(path_out + rc, file_name, file_name_len);
    path_out[rc + file_name_len] = '\0';

    return STORAGE_NO_ERROR;
}

static enum storage_err file_op_result_to_storage_err(
        enum file_op_result result) {
    switch (result) {
    case FILE_OP_SUCCESS:
        return STORAGE_NO_ERROR;
    case FILE_OP_ERR_FAILED:
        // TODO: Consider returning STORAGE_ERR_TRANSACT consistently
        return STORAGE_ERR_GENERIC;
    case FILE_OP_ERR_EXIST:
        return STORAGE_ERR_EXIST;
    case FILE_OP_ERR_ALREADY_OPEN:
        return STORAGE_ERR_NOT_ALLOWED;
    case FILE_OP_ERR_NOT_FOUND:
        return STORAGE_ERR_NOT_FOUND;
    case FILE_OP_ERR_FS_REPAIRED:
        return STORAGE_ERR_FS_REPAIRED;
    }

    SS_ERR("%s: Unknown file_op_result: %d\n", __func__, result);
    return STORAGE_ERR_GENERIC;
}

static enum storage_err session_set_files_count(
        struct storage_client_session* session,
        size_t files_count) {
    struct storage_file_handle** files;

    if (files_count > STORAGE_MAX_OPEN_FILES) {
        SS_ERR("%s: too many open files\n", __func__);
        return STORAGE_ERR_NOT_VALID;
    }

    if (!files_count) {
        free(session->files);
        session->files = NULL;
    } else {
        files = realloc(session->files, sizeof(files[0]) * files_count);
        if (!files) {
            SS_ERR("%s: out of memory\n", __func__);
            return STORAGE_ERR_GENERIC;
        }
        if (files_count > session->files_count)
            memset(files + session->files_count, 0,
                   sizeof(files[0]) * (files_count - session->files_count));

        session->files = files;
    }
    session->files_count = files_count;

    SS_INFO("%s: new file table size, 0x%zx\n", __func__, files_count);

    return STORAGE_NO_ERROR;
}

static void session_shrink_files(struct storage_client_session* session) {
    uint32_t handle;

    handle = session->files_count;
    while (handle > 0 && !session->files[handle - 1])
        handle--;

    if (handle < session->files_count)
        session_set_files_count(session, handle);
}

static void session_close_all_files(struct storage_client_session* session) {
    uint32_t f_handle;
    struct storage_file_handle* file;

    for (f_handle = 0; f_handle < session->files_count; f_handle++) {
        file = session->files[f_handle];
        if (file) {
            file_close(file);
            free(file);
        }
    }
    if (session->files) {
        free(session->files);
    }
    session->files_count = 0;
}

static enum storage_err create_file_handle(
        struct storage_client_session* session,
        uint32_t* handlep,
        struct storage_file_handle** file_p) {
    enum storage_err result;
    uint32_t handle;
    struct storage_file_handle* file;

    for (handle = 0; handle < session->files_count; handle++)
        if (!session->files[handle])
            break;

    if (handle >= session->files_count) {
        result = session_set_files_count(session, handle + 1);
        if (result != STORAGE_NO_ERROR)
            return result;
    }

    file = calloc(1, sizeof(*file));
    if (!file) {
        SS_ERR("%s: out of memory\n", __func__);
        return STORAGE_ERR_GENERIC;
    }

    session->files[handle] = file;

    SS_INFO("%s: created file handle 0x%" PRIx32 "\n", __func__, handle);
    *handlep = handle;
    *file_p = file;
    return STORAGE_NO_ERROR;
}

static void free_file_handle(struct storage_client_session* session,
                             uint32_t handle) {
    if (handle >= session->files_count) {
        SS_ERR("%s: invalid handle, 0x%" PRIx32 "\n", __func__, handle);
        return;
    }
    if (session->files[handle] == NULL) {
        SS_ERR("%s: closed handle, 0x%" PRIx32 "\n", __func__, handle);
        return;
    }
    free(session->files[handle]);
    session->files[handle] = NULL;

    SS_INFO("%s: deleted file handle 0x%" PRIx32 "\n", __func__, handle);

    session_shrink_files(session);
}

static struct storage_file_handle* get_file_handle(
        struct storage_client_session* session,
        uint32_t handle) {
    struct storage_file_handle* file;
    if (handle >= session->files_count) {
        SS_ERR("%s: invalid handle, 0x%" PRIx32 "\n", __func__, handle);
        return NULL;
    }
    file = session->files[handle];
    if (!file) {
        SS_ERR("%s: closed handle, 0x%" PRIx32 "\n", __func__, handle);
        return NULL;
    }
    return file;
}

static enum storage_err assert_checkpoint_flag_valid(
        struct storage_client_session* session,
        struct storage_op_flags flags,
        const char* func_name) {
    if (flags.update_checkpoint) {
        if (!(flags.complete_transaction)) {
            SS_ERR("%s: STORAGE_MSG_FLAG_TRANSACT_CHECKPOINT cannot "
                   "be used without STORAGE_MSG_FLAG_TRANSACT_COMPLETE\n",
                   func_name);
            return STORAGE_ERR_NOT_VALID;
        }

        if (!checkpoint_update_allowed(&session->tr)) {
            SS_ERR("%s: Checkpoint requested but not currently allowed.\n",
                   func_name);
            return STORAGE_ERR_NOT_ALLOWED;
        }
    }
    return STORAGE_NO_ERROR;
}

static enum storage_err ensure_active_transaction(
        struct storage_client_session* session,
        struct storage_op_flags flags) {
    if (session->tr.failed) {
        if (flags.complete_transaction) {
            /* last command in current transaction:
             * reset failed state and return error */
            session->tr.failed = false;
        }
        return STORAGE_ERR_TRANSACT;
    }

    if (!transaction_is_active(&session->tr)) {
        /* previous transaction complete */
        transaction_activate(&session->tr);
    }
    return STORAGE_NO_ERROR;
}

void storage_client_session_init(struct storage_client_session* session,
                                 struct fs* fs,
                                 const uuid_t* peer) {
    session->magic = STORAGE_CLIENT_SESSION_MAGIC;
    session->files = NULL;
    session->files_count = 0;

    transaction_init(&session->tr, fs, false);

    /* cache identity information */
    memcpy(&session->uuid, peer, sizeof(*peer));
}

void storage_client_session_destroy(struct storage_client_session* session) {
    if (list_in_list(&session->tr.allocated.node) && !session->tr.failed) {
        /* discard partial transaction */
        transaction_fail(&session->tr);
    }
    session_close_all_files(session);
    transaction_free(&session->tr);
}

/* abort transaction and clear sticky transaction error */
enum storage_err storage_transaction_end(struct storage_client_session* session,
                                         struct storage_op_flags flags) {
    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    if (flags.complete_transaction) {
        /* Allow checkpoint creation without an active transaction */
        if (flags.update_checkpoint && !transaction_is_active(&session->tr)) {
            transaction_activate(&session->tr);
        }
        /* try to complete current transaction */
        if (transaction_is_active(&session->tr)) {
            transaction_complete_etc(&session->tr, flags.update_checkpoint);
        }
        if (session->tr.failed) {
            SS_ERR("%s: failed to complete transaction\n", __func__);
            /* clear transaction failed state */
            session->tr.failed = false;
            return STORAGE_ERR_TRANSACT;
        }
        return STORAGE_NO_ERROR;
    }

    /* discard current transaction */
    if (transaction_is_active(&session->tr)) {
        transaction_fail(&session->tr);
    }
    /* clear transaction failed state */
    session->tr.failed = false;
    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_delete(struct storage_client_session* session,
                                     const char* fname,
                                     size_t fname_len,
                                     struct storage_op_flags flags) {
    enum file_op_result delete_res;
    char path_buf[FS_PATH_MAX];

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    /* make sure filename is legal */
    if (!is_valid_name(fname, fname_len)) {
        SS_ERR("%s: invalid filename\n", __func__);
        return STORAGE_ERR_NOT_VALID;
    }

    result = get_path(path_buf, sizeof(path_buf), &session->uuid, fname,
                      fname_len);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    SS_INFO("%s: path %s\n", __func__, path_buf);

    delete_res = file_delete(&session->tr, path_buf, flags.allow_repaired);

    if (delete_res != FILE_OP_SUCCESS) {
        return file_op_result_to_storage_err(delete_res);
    }

    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
        if (session->tr.failed) {
            SS_ERR("%s: transaction commit failed\n", __func__);
            return STORAGE_ERR_GENERIC;
        }
    }

    return STORAGE_NO_ERROR;
}

/**
 * storage_file_check_name - Check if file handle matches path
 * @tr:         Transaction object.
 * @file:       File handle object.
 * @path:       Path to check.
 *
 * Return: %true if @file matches @path, %false otherwise.
 */
static bool storage_file_check_name(struct transaction* tr,
                                    const struct storage_file_handle* file,
                                    const char* path) {
    bool ret;
    const struct file_info* file_info;
    struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref);

    file_info = file_get_info(tr, &file->block_mac, &ref);
    if (!file_info) {
        printf("can't read file entry at %" PRIu64 "\n",
               block_mac_to_block(tr, &file->block_mac));
        return false;
    }
    assert(file_info);
    ret = strcmp(file_info->path, path) == 0;
    file_info_put(file_info, &ref);

    return ret;
}

enum storage_err storage_file_move(struct storage_client_session* session,
                                   uint32_t handle,
                                   bool src_already_opened,
                                   const char* src_name,
                                   size_t src_name_len,
                                   const char* dst_name,
                                   size_t dst_name_len,
                                   enum file_create_mode dst_file_create_mode,
                                   struct storage_op_flags flags) {
    enum file_op_result open_result;
    enum file_op_result move_result;
    struct storage_file_handle* file = NULL;
    char path_buf[FS_PATH_MAX];
    struct storage_file_handle tmp_file;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    /* make sure filenames are legal */
    if (src_name && !is_valid_name(src_name, src_name_len)) {
        SS_ERR("%s: invalid src filename\n", __func__);
        return STORAGE_ERR_NOT_VALID;
    }
    if (!is_valid_name(dst_name, dst_name_len)) {
        SS_ERR("%s: invalid dst filename\n", __func__);
        return STORAGE_ERR_NOT_VALID;
    }

    if (!src_name && !src_already_opened) {
        SS_ERR("%s: src needs to be opened, but no src name provided\n",
               __func__);
    }

    if (src_already_opened) {
        file = get_file_handle(session, handle);
        if (!file)
            return STORAGE_ERR_NOT_VALID;
    }

    if (src_name) {
        result = get_path(path_buf, sizeof(path_buf), &session->uuid, src_name,
                          src_name_len);
        if (result != STORAGE_NO_ERROR) {
            return result;
        }
    }

    SS_INFO("%s: src path %s\n", __func__, path_buf);

    if (file) {
        if (src_name &&
            !storage_file_check_name(&session->tr, file, path_buf)) {
            return STORAGE_ERR_NOT_VALID;
        }
    } else {
        open_result = file_open(&session->tr, path_buf, &tmp_file,
                                FILE_OPEN_NO_CREATE, flags.allow_repaired);
        if (open_result != FILE_OP_SUCCESS) {
            return file_op_result_to_storage_err(open_result);
        }
        file = &tmp_file;
    }

    result = get_path(path_buf, sizeof(path_buf), &session->uuid, dst_name,
                      dst_name_len);
    if (result != STORAGE_NO_ERROR) {
        if (file == &tmp_file) {
            file_close(&tmp_file);
        }
        return result;
    }
    SS_INFO("%s: dst path %s\n", __func__, path_buf);

    move_result = file_move(&session->tr, file, path_buf, dst_file_create_mode,
                            flags.allow_repaired);
    if (file == &tmp_file) {
        file_close(&tmp_file);
    }

    if (move_result != FILE_OP_SUCCESS) {
        return file_op_result_to_storage_err(move_result);
    }

    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
        if (session->tr.failed) {
            SS_ERR("%s: transaction commit failed\n", __func__);
            return STORAGE_ERR_GENERIC;
        }
    }

    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_open(struct storage_client_session* session,
                                   const char* fname,
                                   size_t fname_len,
                                   enum file_create_mode file_create_mode,
                                   bool truncate,
                                   struct storage_op_flags flags,
                                   uint32_t* handle) {
    enum file_op_result open_result;
    struct storage_file_handle* file = NULL;
    uint32_t f_handle;
    char path_buf[FS_PATH_MAX];

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    /* make sure filename is legal */
    if (!is_valid_name(fname, fname_len)) {
        SS_ERR("%s: invalid filename\n", __func__);
        return STORAGE_ERR_NOT_VALID;
    }

    result = get_path(path_buf, sizeof(path_buf), &session->uuid, fname,
                      fname_len);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    SS_INFO("%s: path %s (create_mode: %d, truncate: %d)\n", __func__, path_buf,
            file_create_mode, truncate);

    SS_INFO("%s: call create_file_handle\n", __func__);
    /* alloc file info struct */
    result = create_file_handle(session, &f_handle, &file);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    open_result = file_open(&session->tr, path_buf, file, file_create_mode,
                            flags.allow_repaired);
    if (open_result != FILE_OP_SUCCESS) {
        result = file_op_result_to_storage_err(open_result);
        goto err_open_file;
    }

    if (truncate && file->size) {
        file_set_size(&session->tr, file, 0);
    }

    if (session->tr.failed) {
        SS_ERR("%s: transaction failed\n", __func__);
        result = STORAGE_ERR_GENERIC;
        goto err_transaction_failed;
    }

    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
        if (session->tr.failed) {
            SS_ERR("%s: transaction commit failed\n", __func__);
            result = STORAGE_ERR_GENERIC;
            goto err_transaction_failed;
        }
    }

    *handle = f_handle;
    return STORAGE_NO_ERROR;

err_transaction_failed:
    file_close(file);
err_open_file:
    free_file_handle(session, f_handle);
    return result;
}

enum storage_err storage_file_close(struct storage_client_session* session,
                                    uint32_t handle,
                                    struct storage_op_flags flags) {
    struct storage_file_handle* file;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    file = get_file_handle(session, handle);
    if (!file) {
        return STORAGE_ERR_NOT_VALID;
    }

    file_close(file);
    free_file_handle(session, handle);

    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
        if (session->tr.failed) {
            SS_ERR("%s: transaction commit failed\n", __func__);
            return STORAGE_ERR_GENERIC;
        }
    }

    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_read(struct storage_client_session* session,
                                   uint32_t handle,
                                   uint32_t size,
                                   uint64_t offset,
                                   struct storage_op_flags flags,
                                   uint8_t* resp,
                                   size_t* resp_len) {
    void* bufp = resp;
    size_t buflen;
    size_t bytes_left, len;
    struct storage_file_handle* file;
    size_t block_size = get_file_block_size(session->tr.fs);
    data_block_t block_num;
    const uint8_t* block_data;
    struct obj_ref block_data_ref = OBJ_REF_INITIAL_VALUE(block_data_ref);
    size_t block_offset;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    file = get_file_handle(session, handle);
    if (!file) {
        SS_ERR("%s: invalid file handle (%" PRIx32 ")\n", __func__, handle);
        return STORAGE_ERR_NOT_VALID;
    }

    buflen = size;
    if (buflen > *resp_len) {
        SS_ERR("can't read more than %zu bytes, requested %zu\n", *resp_len,
               buflen);
        return STORAGE_ERR_NOT_VALID;
    }

    if (offset > file->size) {
        SS_ERR("can't read past end of file (%" PRIu64 " > %" PRIu64 ")\n",
               offset, file->size);
        return STORAGE_ERR_NOT_VALID;
    }

    /* calc number of bytes to read */
    if ((offset + buflen) > file->size) {
        bytes_left = (size_t)(file->size - offset);
    } else {
        bytes_left = buflen;
    }
    buflen = bytes_left; /* save to return it to caller */

    SS_INFO("%s: start 0x%" PRIx64 " cnt %zu\n", __func__, offset, bytes_left);

    while (bytes_left) {
        block_num = offset / block_size;
        block_data =
                file_get_block(&session->tr, file, block_num, &block_data_ref);
        block_offset = offset % block_size;
        len = (block_offset + bytes_left > block_size)
                      ? block_size - block_offset
                      : bytes_left;
        if (!block_data) {
            if (session->tr.failed) {
                SS_ERR("error reading block %" PRIu64 "\n", block_num);
                return STORAGE_ERR_GENERIC;
            }
            memset(bufp, 0, len);
        } else {
            memcpy(bufp, block_data + block_offset, len);
            file_block_put(block_data, &block_data_ref);
        }

        bytes_left -= len;
        offset += len;
        bufp += len;
    }

    *resp_len = buflen;
    return STORAGE_NO_ERROR;
}

static enum storage_err storage_create_gap(
        struct storage_client_session* session,
        struct storage_file_handle* file) {
    size_t block_size = get_file_block_size(session->tr.fs);
    data_block_t block_num;
    size_t block_offset;
    uint8_t* block_data;
    struct obj_ref block_data_ref = OBJ_REF_INITIAL_VALUE(block_data_ref);

    block_num = file->size / block_size;
    block_offset = file->size % block_size;

    if (block_offset) {
        /*
         * The file does not currently end on a block boundary.
         * We don't clear data in partial blocks when truncating
         * a file, so the last block could contain data that
         * should not be readable. We unconditionally clear the
         * exposed data when creating gaps in the file, as we
         * don't know if that data is already clear.
         */
        block_data = file_get_block_write(&session->tr, file, block_num, true,
                                          &block_data_ref);
        if (!block_data) {
            SS_ERR("error getting block %" PRIu64 "\n", block_num);
            return STORAGE_ERR_GENERIC;
        }

        memset(block_data + block_offset, 0, block_size - block_offset);
        file_block_put_dirty(&session->tr, file, block_num, block_data,
                             &block_data_ref);

        SS_INFO("%s: clear block at old size 0x%" PRIx64
                ", block_offset 0x%zx\n",
                __func__, file->size, block_offset);
    }
    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_write(struct storage_client_session* session,
                                    uint32_t handle,
                                    uint64_t offset,
                                    const uint8_t* data,
                                    size_t data_len,
                                    struct storage_op_flags flags) {
    size_t len;
    struct storage_file_handle* file;
    size_t block_size = get_file_block_size(session->tr.fs);
    data_block_t block_num;
    uint8_t* block_data;
    struct obj_ref block_data_ref = OBJ_REF_INITIAL_VALUE(block_data_ref);
    size_t block_offset;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    file = get_file_handle(session, handle);
    if (!file) {
        SS_ERR("%s: invalid file handle (%" PRIx32 ")\n", __func__, handle);
        return STORAGE_ERR_NOT_VALID;
    }

    if (offset > file->size) {
        result = storage_create_gap(session, file);
        if (result != STORAGE_NO_ERROR) {
            goto err_write;
        }
    }

    /* transfer data one ss block at a time */
    while (data_len) {
        block_num = offset / block_size;
        block_offset = offset % block_size;
        len = (block_offset + data_len > block_size) ? block_size - block_offset
                                                     : data_len;

        block_data = file_get_block_write(&session->tr, file, block_num,
                                          len != block_size, &block_data_ref);
        if (!block_data) {
            SS_ERR("error getting block %" PRIu64 "\n", block_num);
            result = STORAGE_ERR_GENERIC;
            goto err_write;
        }

        memcpy(block_data + block_offset, data, len);
        file_block_put_dirty(&session->tr, file, block_num, block_data,
                             &block_data_ref);

#if TLOG_LVL >= TLOG_LVL_DEBUG
        SS_INFO("%s: data %p offset 0x%" PRIx64 " len 0x%zx\n", __func__, data,
                offset, len);
#endif
        offset += len;
        data += len;
        data_len -= len;
    }

    if (offset > file->size) {
        file_set_size(&session->tr, file, offset);
    }

    if (session->tr.failed) {
        SS_ERR("%s: transaction failed\n", __func__);
        return STORAGE_ERR_GENERIC;
    }

    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
        if (session->tr.failed) {
            SS_ERR("%s: transaction commit failed\n", __func__);
            return STORAGE_ERR_GENERIC;
        }
    }

    return STORAGE_NO_ERROR;

err_write:
    if (!session->tr.failed) {
        transaction_fail(&session->tr);
    }
err_transaction_complete:
    return result;
}

struct storage_file_list_state {
    struct file_iterate_state iter;
    char prefix[34];
    size_t prefix_len;
    uint8_t max_count;
    uint8_t count;
    bool (*can_record_path)(void* callback_data, size_t max_path_len);
    void (*record_path)(void* callback_data,
                        enum storage_file_list_flag flags,
                        const char* path,
                        size_t path_len);
    void* callback_data;
};

static bool storage_file_list_buf_full(struct storage_file_list_state* miter) {
    if (miter->max_count && miter->count >= miter->max_count) {
        return true;
    }

    return !miter->can_record_path(miter->callback_data,
                                   FS_PATH_MAX - miter->prefix_len);
}

static void storage_file_list_add(struct storage_file_list_state* miter,
                                  enum storage_file_list_flag flags,
                                  const char* path) {
    assert(!storage_file_list_buf_full(miter));
    size_t path_len = path ? strlen(path) : 0;
    assert(path_len <= FS_PATH_MAX - miter->prefix_len);
    miter->record_path(miter->callback_data, flags, path, path_len);
    miter->count++;
}

static bool storage_file_list_iter(struct file_iterate_state* iter,
                                   struct transaction* tr,
                                   const struct block_mac* block_mac,
                                   bool added,
                                   bool removed) {
    struct storage_file_list_state* miter =
            containerof(iter, struct storage_file_list_state, iter);
    const struct file_info* file_info;
    struct obj_ref ref = OBJ_REF_INITIAL_VALUE(ref);

    file_info = file_get_info(tr, block_mac, &ref);
    if (!file_info) {
        printf("can't read file entry at %" PRIu64 "\n",
               block_mac_to_block(tr, block_mac));
        return true;
    }

    if (strncmp(file_info->path, miter->prefix, miter->prefix_len) == 0) {
        storage_file_list_add(miter,
                              added     ? STORAGE_FILE_LIST_ADDED
                              : removed ? STORAGE_FILE_LIST_REMOVED
                                        : STORAGE_FILE_LIST_COMMITTED,
                              file_info->path + miter->prefix_len);
    }

    file_info_put(file_info, &ref);

    return storage_file_list_buf_full(miter);
}

enum storage_err storage_file_list(
        struct storage_client_session* session,
        uint8_t max_count,
        enum storage_file_list_flag last_state,
        const char* last_fname,
        size_t last_fname_len,
        struct storage_op_flags flags,
        bool (*can_record_path)(void* callback_data, size_t max_path_len),
        void (*record_path)(void* callback_data,
                            enum storage_file_list_flag flags,
                            const char* path,
                            size_t path_len),
        void* callback_data) {
    enum file_op_result iterate_res;
    const char* last_name;
    char path_buf[FS_PATH_MAX];

    struct storage_file_list_state state = {
            .iter.file = storage_file_list_iter,
            .max_count = max_count,
            .record_path = record_path,
            .can_record_path = can_record_path,
            .callback_data = callback_data,
    };

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    result =
            get_path(state.prefix, sizeof(state.prefix), &session->uuid, "", 0);
    if (result != STORAGE_NO_ERROR) {
        SS_ERR("%s: internal error, get_path failed\n", __func__);
        return STORAGE_ERR_GENERIC;
    }
    state.prefix_len = strlen(state.prefix);

    if (last_state == STORAGE_FILE_LIST_END) {
        SS_ERR("%s: invalid request state (%" PRIx8 ")\n", __func__,
               last_state);
        return STORAGE_ERR_NOT_VALID;
    }

    if (last_state == STORAGE_FILE_LIST_START) {
        last_name = NULL;
    } else {
        /* make sure filename is legal */
        if (!is_valid_name(last_fname, last_fname_len)) {
            SS_ERR("%s: invalid filename\n", __func__);
            return STORAGE_ERR_NOT_VALID;
        }

        result = get_path(path_buf, sizeof(path_buf), &session->uuid,
                          last_fname, last_fname_len);
        if (result != STORAGE_NO_ERROR) {
            return result;
        }

        last_name = path_buf;
    }

    if (last_state != STORAGE_FILE_LIST_ADDED) {
        iterate_res = file_iterate(&session->tr, last_name, false, &state.iter,
                                   flags.allow_repaired);
        last_name = NULL;
    } else {
        iterate_res = FILE_OP_SUCCESS;
    }
    if (iterate_res == FILE_OP_SUCCESS && !storage_file_list_buf_full(&state)) {
        iterate_res = file_iterate(&session->tr, last_name, true, &state.iter,
                                   flags.allow_repaired);
    }
    if (iterate_res != FILE_OP_SUCCESS) {
        SS_ERR("%s: file_iterate failed\n", __func__);
        return file_op_result_to_storage_err(iterate_res);
    }

    if (!storage_file_list_buf_full(&state)) {
        storage_file_list_add(&state, STORAGE_FILE_LIST_END, NULL);
    }

    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_get_size(struct storage_client_session* session,
                                       uint32_t handle,
                                       struct storage_op_flags flags,
                                       uint64_t* size) {
    bool valid;
    struct storage_file_handle* file;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    file = get_file_handle(session, handle);
    if (!file) {
        SS_ERR("%s: invalid file handle (%" PRIx32 ")\n", __func__, handle);
        return STORAGE_ERR_NOT_VALID;
    }

    valid = file_get_size(&session->tr, file, size);
    if (!valid) {
        return STORAGE_ERR_NOT_VALID;
    }

    return STORAGE_NO_ERROR;
}

enum storage_err storage_file_set_size(struct storage_client_session* session,
                                       uint32_t handle,
                                       uint64_t new_size,
                                       struct storage_op_flags flags) {
    struct storage_file_handle* file;

    enum storage_err result =
            assert_checkpoint_flag_valid(session, flags, __func__);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }
    result = ensure_active_transaction(session, flags);
    if (result != STORAGE_NO_ERROR) {
        return result;
    }

    file = get_file_handle(session, handle);
    if (!file) {
        SS_ERR("%s: invalid file handle (%" PRIx32 ")\n", __func__, handle);
        return STORAGE_ERR_NOT_VALID;
    }

    SS_INFO("%s: new size 0x%" PRIx64 ", old size 0x%" PRIx64 "\n", __func__,
            new_size, file->size);

    /* for now we only support shrinking the file */
    if (new_size > file->size) {
        result = storage_create_gap(session, file);
        if (result != STORAGE_NO_ERROR) {
            return result;
        }
        storage_create_gap(session, file);
    }

    /* check for nop */
    if (new_size != file->size) {
        /* update size */
        file_set_size(&session->tr, file, new_size);
    }

    /* try to commit */
    if (flags.complete_transaction) {
        transaction_complete_etc(&session->tr, flags.update_checkpoint);
    }

    if (session->tr.failed) {
        SS_ERR("%s: transaction failed\n", __func__);
        return STORAGE_ERR_GENERIC;
    }

    return STORAGE_NO_ERROR;
}
