/* SPDX-License-Identifier: LGPL-2.1+ */

#include <zlib.h>

#include "alloc-util.h"
#include "btrfs-util.h"
#include "qcow2-util.h"
#include "sparse-endian.h"
#include "util.h"

#define QCOW2_MAGIC 0x514649fb

#define QCOW2_COPIED (1ULL << 63)
#define QCOW2_COMPRESSED (1ULL << 62)
#define QCOW2_ZERO (1ULL << 0)

typedef struct _packed_ Header {
      be32_t magic;
      be32_t version;

      be64_t backing_file_offset;
      be32_t backing_file_size;

      be32_t cluster_bits;
      be64_t size;
      be32_t crypt_method;

      be32_t l1_size;
      be64_t l1_table_offset;

      be64_t refcount_table_offset;
      be32_t refcount_table_clusters;

      be32_t nb_snapshots;
      be64_t snapshots_offset;

      /* The remainder is only present on QCOW3 */
      be64_t incompatible_features;
      be64_t compatible_features;
      be64_t autoclear_features;

      be32_t refcount_order;
      be32_t header_length;
} Header;

#define HEADER_MAGIC(header) be32toh((header)->magic)
#define HEADER_VERSION(header) be32toh((header)->version)
#define HEADER_CLUSTER_BITS(header) be32toh((header)->cluster_bits)
#define HEADER_CLUSTER_SIZE(header) (1ULL << HEADER_CLUSTER_BITS(header))
#define HEADER_L2_BITS(header) (HEADER_CLUSTER_BITS(header) - 3)
#define HEADER_SIZE(header) be64toh((header)->size)
#define HEADER_CRYPT_METHOD(header) be32toh((header)->crypt_method)
#define HEADER_L1_SIZE(header) be32toh((header)->l1_size)
#define HEADER_L2_SIZE(header) (HEADER_CLUSTER_SIZE(header)/sizeof(uint64_t))
#define HEADER_L1_TABLE_OFFSET(header) be64toh((header)->l1_table_offset)

static uint32_t HEADER_HEADER_LENGTH(const Header *h) {
        if (HEADER_VERSION(h) < 3)
                return offsetof(Header, incompatible_features);

        return be32toh(h->header_length);
}

static int copy_cluster(
                int sfd, uint64_t soffset,
                int dfd, uint64_t doffset,
                uint64_t cluster_size,
                void *buffer) {

        ssize_t l;
        int r;

        r = btrfs_clone_range(sfd, soffset, dfd, doffset, cluster_size);
        if (r >= 0)
                return r;

        l = pread(sfd, buffer, cluster_size, soffset);
        if (l < 0)
                return -errno;
        if ((uint64_t) l != cluster_size)
                return -EIO;

        l = pwrite(dfd, buffer, cluster_size, doffset);
        if (l < 0)
                return -errno;
        if ((uint64_t) l != cluster_size)
                return -EIO;

        return 0;
}

static int decompress_cluster(
                int sfd, uint64_t soffset,
                int dfd, uint64_t doffset,
                uint64_t compressed_size,
                uint64_t cluster_size,
                void *buffer1,
                void *buffer2) {

        _cleanup_free_ void *large_buffer = NULL;
        z_stream s = {};
        uint64_t sz;
        ssize_t l;
        int r;

        if (compressed_size > cluster_size) {
                /* The usual cluster buffer doesn't suffice, let's
                 * allocate a larger one, temporarily */

                large_buffer = malloc(compressed_size);
                if (!large_buffer)
                        return -ENOMEM;

                buffer1 = large_buffer;
        }

        l = pread(sfd, buffer1, compressed_size, soffset);
        if (l < 0)
                return -errno;
        if ((uint64_t) l != compressed_size)
                return -EIO;

        s.next_in = buffer1;
        s.avail_in = compressed_size;
        s.next_out = buffer2;
        s.avail_out = cluster_size;

        r = inflateInit2(&s, -12);
        if (r != Z_OK)
                return -EIO;

        r = inflate(&s, Z_FINISH);
        sz = (uint8_t*) s.next_out - (uint8_t*) buffer2;
        inflateEnd(&s);
        if (r != Z_STREAM_END || sz != cluster_size)
                return -EIO;

        l = pwrite(dfd, buffer2, cluster_size, doffset);
        if (l < 0)
                return -errno;
        if ((uint64_t) l != cluster_size)
                return -EIO;

        return 0;
}

static int normalize_offset(
                const Header *header,
                uint64_t p,
                uint64_t *ret,
                bool *compressed,
                uint64_t *compressed_size) {

        uint64_t q;

        q = be64toh(p);

        if (q & QCOW2_COMPRESSED) {
                uint64_t sz, csize_shift, csize_mask;

                if (!compressed)
                        return -EOPNOTSUPP;

                csize_shift = 64 - 2 - (HEADER_CLUSTER_BITS(header) - 8);
                csize_mask = (1ULL << (HEADER_CLUSTER_BITS(header) - 8)) - 1;
                sz = (((q >> csize_shift) & csize_mask) + 1) * 512 - (q & 511);
                q &= ((1ULL << csize_shift) - 1);

                if (compressed_size)
                        *compressed_size = sz;

                *compressed = true;

        } else {
                if (compressed)  {
                        *compressed = false;
                        *compressed_size = 0;
                }

                if (q & QCOW2_ZERO) {
                        /* We make no distinction between zero blocks and holes */
                        *ret = 0;
                        return 0;
                }

                q &= ~QCOW2_COPIED;
        }

        *ret = q;
        return q > 0;  /* returns positive if not a hole */
}

static int verify_header(const Header *header) {
        assert(header);

        if (HEADER_MAGIC(header) != QCOW2_MAGIC)
                return -EBADMSG;

        if (!IN_SET(HEADER_VERSION(header), 2, 3))
                return -EOPNOTSUPP;

        if (HEADER_CRYPT_METHOD(header) != 0)
                return -EOPNOTSUPP;

        if (HEADER_CLUSTER_BITS(header) < 9) /* 512K */
                return -EBADMSG;

        if (HEADER_CLUSTER_BITS(header) > 21) /* 2MB */
                return -EBADMSG;

        if (HEADER_SIZE(header) % HEADER_CLUSTER_SIZE(header) != 0)
                return -EBADMSG;

        if (HEADER_L1_SIZE(header) > 32*1024*1024) /* 32MB */
                return -EBADMSG;

        if (HEADER_VERSION(header) == 3) {

                if (header->incompatible_features != 0)
                        return -EOPNOTSUPP;

                if (HEADER_HEADER_LENGTH(header) < sizeof(Header))
                        return -EBADMSG;
        }

        return 0;
}

int qcow2_convert(int qcow2_fd, int raw_fd) {
        _cleanup_free_ void *buffer1 = NULL, *buffer2 = NULL;
        _cleanup_free_ be64_t *l1_table = NULL, *l2_table = NULL;
        uint64_t sz, i;
        Header header;
        ssize_t l;
        int r;

        l = pread(qcow2_fd, &header, sizeof(header), 0);
        if (l < 0)
                return -errno;
        if (l != sizeof(header))
                return -EIO;

        r = verify_header(&header);
        if (r < 0)
                return r;

        l1_table = new(be64_t, HEADER_L1_SIZE(&header));
        if (!l1_table)
                return -ENOMEM;

        l2_table = malloc(HEADER_CLUSTER_SIZE(&header));
        if (!l2_table)
                return -ENOMEM;

        buffer1 = malloc(HEADER_CLUSTER_SIZE(&header));
        if (!buffer1)
                return -ENOMEM;

        buffer2 = malloc(HEADER_CLUSTER_SIZE(&header));
        if (!buffer2)
                return -ENOMEM;

        /* Empty the file if it exists, we rely on zero bits */
        if (ftruncate(raw_fd, 0) < 0)
                return -errno;

        if (ftruncate(raw_fd, HEADER_SIZE(&header)) < 0)
                return -errno;

        sz = sizeof(uint64_t) * HEADER_L1_SIZE(&header);
        l = pread(qcow2_fd, l1_table, sz, HEADER_L1_TABLE_OFFSET(&header));
        if (l < 0)
                return -errno;
        if ((uint64_t) l != sz)
                return -EIO;

        for (i = 0; i < HEADER_L1_SIZE(&header); i ++) {
                uint64_t l2_begin, j;

                r = normalize_offset(&header, l1_table[i], &l2_begin, NULL, NULL);
                if (r < 0)
                        return r;
                if (r == 0)
                        continue;

                l = pread(qcow2_fd, l2_table, HEADER_CLUSTER_SIZE(&header), l2_begin);
                if (l < 0)
                        return -errno;
                if ((uint64_t) l != HEADER_CLUSTER_SIZE(&header))
                        return -EIO;

                for (j = 0; j < HEADER_L2_SIZE(&header); j++) {
                        uint64_t data_begin, p, compressed_size;
                        bool compressed;

                        p = ((i << HEADER_L2_BITS(&header)) + j) << HEADER_CLUSTER_BITS(&header);

                        r = normalize_offset(&header, l2_table[j], &data_begin, &compressed, &compressed_size);
                        if (r < 0)
                                return r;
                        if (r == 0)
                                continue;

                        if (compressed)
                                r = decompress_cluster(
                                                qcow2_fd, data_begin,
                                                raw_fd, p,
                                                compressed_size, HEADER_CLUSTER_SIZE(&header),
                                                buffer1, buffer2);
                        else
                                r = copy_cluster(
                                                qcow2_fd, data_begin,
                                                raw_fd, p,
                                                HEADER_CLUSTER_SIZE(&header), buffer1);
                        if (r < 0)
                                return r;
                }
        }

        return 0;
}

int qcow2_detect(int fd) {
        be32_t id;
        ssize_t l;

        l = pread(fd, &id, sizeof(id), 0);
        if (l < 0)
                return -errno;
        if (l != sizeof(id))
                return -EIO;

        return htobe32(QCOW2_MAGIC) == id;
}
