/*
 * Shared file cache protocol.
 * Copyright (c) 2026 Niklas Haas
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * FFmpeg is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 * Based on cache.c by Michael Niedermayer
 */

#include "libavutil/attributes.h"
#include "libavutil/avassert.h"
#include "libavutil/avstring.h"
#include "libavutil/crc.h"
#include "libavutil/hash.h"
#include "libavutil/file_open.h"
#include "libavutil/mem.h"
#include "libavutil/opt.h"
#include "libavutil/time.h"

#include "url.h"

#include <errno.h>
#include <fcntl.h>
#include <stdatomic.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

/**
 * This hash should be resistant against collision attacks, so that an
 * attacker could not generate e.g. two different URIs that map to the same
 * cache file. This requires at least 64 bits of collision resistance in
 * practice (i.e. 128 bits = 16 bytes of hash size). However, we can be
 * conservative by computing e.g. a 256 bit hash and storing it inside the
 * file header for verification.
 *
 * Note that due to the way we use atomics, we should avoid zero bytes in
 * the resulting hash; hence we tweak the input slightly to avoid this.
 * The resulting loss in hash strength is negligible, since 32 bytes is
 * already much more than needed.
 */
#define HASH_METHOD    "SHA512/256"
#define HASH_SIZE      32

static int hash_uri(uint8_t hash[HASH_SIZE], const char *uri)
{
    struct AVHashContext *ctx = NULL;
    int ret = av_hash_alloc(&ctx, HASH_METHOD);
    if (ret < 0)
        return ret;

    av_assert0(av_hash_get_size(ctx) == HASH_SIZE);
    av_hash_init(ctx);
    av_hash_update(ctx, (const uint8_t *) uri, strlen(uri));
    av_hash_final(ctx, hash);
    av_hash_freep(&ctx);

    for (int i = 0; i < HASH_SIZE; i++)
        hash[i] = hash[i] ? hash[i] : ~hash[i]; /* prevent zero bytes */
    return 0;
}

#define HEADER_MAGIC   MKTAG(u'\xFF', 'S', 'h', '$')
#define HEADER_VERSION 2

enum BlockState {
    /* Reserved block state values */
    BLOCK_NONE = 0, ///< block is not cached
    BLOCK_PENDING,  ///< a thread is currently trying to write this block
    BLOCK_FAILED,   ///< the underlying I/O source failed to read this block

    /**
     * All other block states represent valid cached blocks, with the value
     * being the CRC of the block data.
     */
};

static uint16_t get_block_crc(const uint8_t *block, size_t block_size)
{
    uint16_t crc = av_crc(av_crc_get_table(AV_CRC_16_ANSI), 0, block, block_size);
    switch (crc) {
    case BLOCK_NONE:
    case BLOCK_FAILED:
    case BLOCK_PENDING:
        return ~crc; /* avoid reserved block states */
    default:
        return crc;
    }
}

typedef struct Block {
    atomic_ushort state; /* enum BlockState */
} Block;

typedef struct Spacemap {
    atomic_uint header_magic;
    atomic_ushort version;
    atomic_ushort block_shift;
    atomic_ullong filesize; /* byte offset of true EOF, or 0 if unknown */
    atomic_uchar hash[HASH_SIZE]; /* hash of resource URI / filename */
    char reserved[80];

    Block blocks[];
} Spacemap;

/* Set to value iff the current value is unset (zero) */
#define DEF_SET_ONCE(ctype, atype)                                              \
    static int set_once_##atype(atomic_##atype *const ptr, const ctype value)   \
    {                                                                           \
        ctype prev = 0;                                                         \
        av_assert1(value != 0);                                                 \
        if (atomic_compare_exchange_strong_explicit(                            \
                ptr, &prev, value, memory_order_release, memory_order_relaxed)) \
            return 1;                                                           \
        else if (prev == value)                                                 \
            return 0;                                                           \
        else                                                                    \
            return AVERROR(EINVAL);                                             \
    }

DEF_SET_ONCE(unsigned char,      uchar)
DEF_SET_ONCE(unsigned int,       uint)
DEF_SET_ONCE(unsigned short,     ushort)
DEF_SET_ONCE(unsigned long long, ullong)

typedef struct SharedContext {
    AVClass *class;
    URLContext *inner;
    int64_t inner_pos;

    /* options */
    char *cache_dir;
    int block_shift; ///< requested shift; may disagree with actual
    int read_only;
    int64_t timeout;
    int retry_errors;
    int verify;

    /* misc state */
    int64_t pos; ///< current logical position
    uint8_t *tmp_buf;
    int block_size;
    int write_err; ///< write error occurred

    /* cache file */
    uint8_t *cache_data; ///< optional mmap of the cache file
    char *cache_path;
    off_t cache_size; ///< size of mapped memory region (for munmap)
    int fd;

    /* space map */
    Spacemap *spacemap;
    char *map_path;
    off_t map_size;
    int mapfd;

    /* statistics */
    int64_t nb_hit;
    int64_t nb_miss;
} SharedContext;

static int shared_close(URLContext *h)
{
    SharedContext *s = h->priv_data;

    ffurl_close(s->inner);
    if (s->cache_data)
        munmap(s->cache_data, s->cache_size);
    if (s->spacemap)
        munmap(s->spacemap, s->map_size);
    if (s->fd != -1)
        close(s->fd);
    if (s->mapfd != -1)
        close(s->mapfd);
    av_freep(&s->cache_path);
    av_freep(&s->map_path);
    av_freep(&s->tmp_buf);

    av_log(h, AV_LOG_DEBUG, "Cache statistics: %"PRId64" hits, %"PRId64" misses\n",
           s->nb_hit, s->nb_miss);
    return 0;
}

static int cache_map(URLContext *h, int64_t filesize);
static int spacemap_init(URLContext *h, const uint8_t hash[HASH_SIZE]);
static int spacemap_grow(URLContext *h, int64_t block);

static int64_t get_filesize(URLContext *h)
{
    SharedContext *s = h->priv_data;
    return atomic_load_explicit(&s->spacemap->filesize, memory_order_relaxed);
}

static int set_filesize(URLContext *h, int64_t new_size)
{
    SharedContext *s = h->priv_data;
    int ret;

    if (!new_size)
        return 0;

    ret = set_once_ullong(&s->spacemap->filesize, new_size);
    if (ret < 0) {
        av_log(h, AV_LOG_ERROR, "Cached file size mismatch, expected: "
                "%"PRId64", got: %"PRIu64"!\n", new_size,
                (uint64_t) atomic_load(&s->spacemap->filesize));
        return ret;
    } else if (ret) {
        /* Opportunistically map the file; this also sets the correct filesize.
         * Ignore errors as this is not critical to the cache logic. */
        cache_map(h, new_size);
    }

    return ret;
}

static int shared_open(URLContext *h, const char *arg, int flags, AVDictionary **options)
{
    SharedContext *s = h->priv_data;
    int ret;

    if (!s->cache_dir || !s->cache_dir[0]) {
        av_log(h, AV_LOG_ERROR, "Missing path for shared cache! Specify a "
               "directory using the -cache_dir option.\n");
        return AVERROR(EINVAL);
    }

    s->fd = s->mapfd = -1; /* Set these early for shared_close() failure path */

    /* Open underlying protocol */
    av_strstart(arg, "shared:", &arg);
    ret = ffurl_open_whitelist(&s->inner, arg, flags, &h->interrupt_callback,
                               options, h->protocol_whitelist, h->protocol_blacklist, h);

    if (ret < 0)
        goto fail;

    uint8_t hash[HASH_SIZE];
    ret = hash_uri(hash, arg);
    if (ret < 0)
        goto fail;

    /* 128 bits is enough for collision resistance; we already store the full
     * hash inside the header for verification */
    char filename[2 * 16 + 1];
    for (int i = 0; i < FF_ARRAY_ELEMS(filename) / 2; i++)
        sprintf(&filename[i * 2], "%02X", hash[i]);
    s->cache_path = av_asprintf("%s/%s.cache",    s->cache_dir, filename);
    s->map_path   = av_asprintf("%s/%s.spacemap", s->cache_dir, filename);
    if (!s->cache_path || !s->map_path) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    av_log(h, AV_LOG_VERBOSE, "Opening cache file '%s' for URI: '%s'\n",
           s->cache_path, s->inner->filename);

    s->fd    = avpriv_open(s->cache_path, O_RDWR | O_CREAT, 0660);
    s->mapfd = avpriv_open(s->map_path,   O_RDWR | O_CREAT, 0660);
    if (s->fd < 0 || s->mapfd < 0) {
        ret = AVERROR(errno);
        av_log(h, AV_LOG_ERROR, "Failed to open '%s': %s\n",
               s->fd < 0 ? s->cache_path : s->map_path, av_err2str(ret));
        goto fail;
    }

    ret = spacemap_init(h, hash);
    if (ret < 0)
        goto fail;

    s->block_size = 1 << atomic_load(&s->spacemap->block_shift);

    int64_t filesize = get_filesize(h);
    if (!filesize) {
        /* Filesize is not yet known, try to get it from the underlying URL */
        filesize = ffurl_size(s->inner);
        if (filesize < 0 && filesize != AVERROR(ENOSYS)) {
            ret = (int) filesize;
            goto fail;
        } else if (filesize > 0)
            set_filesize(h, filesize);
    }

    if (filesize > 0) {
        int64_t last_pos = filesize - 1;
        int64_t last_block = last_pos >> atomic_load(&s->spacemap->block_shift);
        ret = spacemap_grow(h, last_block);
        if (ret < 0)
            goto fail;

        /* If filesize is known, we can directly mmap() the cache file */
        ret = cache_map(h, filesize);
        if (ret < 0) {
            av_log(h, AV_LOG_WARNING, "Failed to map cache file: %s. Falling "
                   "back to normal read/write\n", av_err2str(ret));
            ret = 0;
        }
    }

    if (!s->cache_data) {
        /* Temporary buffer needed for pread/pwrite() fallback */
        s->tmp_buf = av_malloc(s->block_size);
        if (!s->tmp_buf) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
    }

    h->max_packet_size = s->block_size;
    h->min_packet_size = s->block_size;

fail:
    if (ret < 0)
        shared_close(h);
    return ret;
}

static int cache_map(URLContext *h, int64_t filesize)
{
    SharedContext *s = h->priv_data;
    if (s->cache_size >= filesize || filesize > SIZE_MAX)
        return 0;

    if (s->cache_data) {
        munmap(s->cache_data, s->cache_size);
        s->cache_data = NULL;
        s->cache_size = 0;
    }

    struct stat st;
    int ret = fstat(s->fd, &st);
    if (ret < 0)
        return AVERROR(errno);

    if (st.st_size != filesize) {
        /* Ensure the file size is correct before mapping; this can happen if
         * another process wrote the correct filesize to the header but
         * crashed right before actually successfully resizing the file. */
        ret = ftruncate(s->fd, filesize);
        if (ret < 0)
            return AVERROR(errno);
    }

    s->cache_data = mmap(NULL, filesize, PROT_READ | PROT_WRITE, MAP_SHARED, s->fd, 0);
    if (s->cache_data == MAP_FAILED) {
        s->cache_data = NULL;
        return AVERROR(errno);
    }

    s->cache_size = filesize;
    return 0;
}

static int spacemap_remap(URLContext *h, size_t map_size)
{
    SharedContext *s = h->priv_data;
    struct flock fl = { .l_type  = F_WRLCK };
    int ret, did_grow = 0;
    if (map_size <= s->map_size)
        return 0;

    /* Opportunistically get current filesize before attempting to lock */
    struct stat st;
    ret = fstat(s->mapfd, &st);
    if (ret < 0) {
        ret = AVERROR(errno);
        goto fail;
    }

    if (st.st_size >= map_size)
        goto skip_resize;

    /* Lock the spacemap to ensure nobody else is currently resizing it */
    ret = fcntl(s->mapfd, F_SETLKW, &fl);
    if (ret < 0) {
        ret = AVERROR(errno);
        goto fail;
    }
    fl.l_type = F_UNLCK;

    /* Refresh filesize after acquiring the lock */
    ret = fstat(s->mapfd, &st);
    if (ret < 0) {
        ret = AVERROR(errno);
        goto fail;
    }

    if (st.st_size >= map_size)
        goto skip_resize;

    ret = ftruncate(s->mapfd, map_size);
    if (ret < 0) {
        ret = AVERROR(errno);
        goto fail;
    }
    st.st_size = map_size;
    did_grow = 1;

skip_resize:
    if (s->spacemap)
        munmap(s->spacemap, s->map_size);
    s->map_size = st.st_size;
    s->spacemap = mmap(NULL, s->map_size, PROT_READ | PROT_WRITE, MAP_SHARED, s->mapfd, 0);
    if (s->spacemap == MAP_FAILED) {
        s->spacemap = NULL; /* for munmap check */
        s->map_size = 0;
        ret = AVERROR(errno);
        goto fail;
    }

    /* fl.l_type is set to F_UNLCK only after successful lock */
    if (fl.l_type == F_UNLCK)
        fcntl(s->mapfd, F_SETLK, &fl);

    return did_grow;

fail:
    if (fl.l_type == F_UNLCK)
        fcntl(s->mapfd, F_SETLK, &fl);
    av_log(h, AV_LOG_ERROR, "Failed to resize space map: %s\n", av_err2str(ret));
    return ret;
}

static int spacemap_grow(URLContext *h, int64_t block)
{
    SharedContext *s = h->priv_data;
    int64_t num_blocks = block + 1;
    size_t map_bytes = sizeof(Spacemap) + num_blocks * sizeof(Block);

    /* When streaming files without known size, round up the number of blocks
     * to the nearest multiple of the block size to reduce the rate of resizes */
    if (!get_filesize(h)) {
        av_assert0(s->block_size > 0);
        map_bytes = FFALIGN(map_bytes, (int64_t) s->block_size);
    }

    if (map_bytes < num_blocks)
        return AVERROR(EINVAL); /* overflow */

    const off_t old_size = s->map_size;
    int ret = spacemap_remap(h, map_bytes);
    if (ret < 0)
        return ret;

    /* Report new size after successful grow */
    if (s->map_size > old_size) {
        num_blocks = (s->map_size - sizeof(Spacemap)) / sizeof(Block);
        av_log(h, AV_LOG_DEBUG,
               "%s %zu bytes, capacity: %"PRId64" blocks = %zu MB\n",
               ret ? "Resized spacemap to" : "Mapped spacemap with",
               (size_t) s->map_size, num_blocks,
               (num_blocks * (int64_t) s->block_size) >> 20);
    }
    return 0;
}

static int spacemap_init(URLContext *h, const uint8_t hash[HASH_SIZE])
{
    SharedContext *s = h->priv_data;
    int ret;

    ret = spacemap_remap(h, sizeof(Spacemap));
    if (ret < 0)
        return ret;

    if ((ret = set_once_uint(&s->spacemap->header_magic, HEADER_MAGIC)) < 0 ||
        (ret = set_once_ushort(&s->spacemap->version, HEADER_VERSION)) < 0)
    {
        av_log(h, AV_LOG_ERROR, "Shared cache spacemap header mismatch!\n");
        av_log(h, AV_LOG_ERROR, "  Expected magic: 0x%X, version: %d\n",
               HEADER_MAGIC, HEADER_VERSION);
        av_log(h, AV_LOG_ERROR, "  Got      magic: 0x%X, version: %d\n",
               atomic_load(&s->spacemap->header_magic),
               atomic_load(&s->spacemap->version));
        return ret;
    }

    ret = set_once_ushort(&s->spacemap->block_shift, s->block_shift);
    if (ret < 0) {
        const int shift = atomic_load(&s->spacemap->block_shift);
        av_log(h, AV_LOG_WARNING, "Shared cache uses block shift %d, "
               "but requested block shift is %d.\n", shift, s->block_shift);
        if (shift < 9 || shift > 30) {
            av_log(h, AV_LOG_ERROR, "Invalid block shift %d in cache file!\n", shift);
            return AVERROR(EINVAL);
        }
    }

    for (int i = 0; i < HASH_SIZE; i++) {
        ret = set_once_uchar(&s->spacemap->hash[i], hash[i]);
        if (ret < 0) {
            av_log(h, AV_LOG_ERROR, "Shared cache spacemap hash mismatch!\n");
            av_log(h, AV_LOG_ERROR, "  Expected hash: ");
            for (int j = 0; j < 32; j++)
                av_log(h, AV_LOG_ERROR, "%02X", hash[j]);
            av_log(h, AV_LOG_ERROR, "\n  Got      hash: ");
            for (int j = 0; j < 32; j++)
                av_log(h, AV_LOG_ERROR, "%02X", atomic_load(&s->spacemap->hash[j]));
            av_log(h, AV_LOG_ERROR, "\n");
            return ret;
        }
    }

    if (ret) /* set_once() return 1 if this is the first time setting the value */
        av_log(h, AV_LOG_DEBUG, "Initialized new cache spacemap.\n");

    return ret;
}

static int read_cache(SharedContext *s, uint8_t *buf, size_t size, off_t offset)
{
    if (s->cache_data) {
        av_assert1(offset + size <= s->cache_size);
        memcpy(buf, s->cache_data + offset, size);
        return 0;
    }

    while (size) {
        ssize_t ret = pread(s->fd, buf, size, offset);
        if (ret <= 0)
            return ret ? AVERROR(errno) : AVERROR(EIO);
        buf    += ret;
        offset += ret;
        size   -= ret;
    }

    return 0;
}

static int write_cache(SharedContext *s, const uint8_t *buf, size_t size, off_t offset)
{
    if (s->cache_data) {
        av_assert1(offset + size <= s->cache_size);
        memcpy(s->cache_data + offset, buf, size);
        return 0;
    }

    while (size) {
        ssize_t ret = pwrite(s->fd, buf, size, offset);
        if (ret <= 0)
            return ret ? AVERROR(errno) : AVERROR(EIO);
        buf    += ret;
        offset += ret;
        size   -= ret;
    }

    return 0;
}

static size_t clamp_size(URLContext *h, size_t size, int64_t pos)
{
    const int64_t filesize = get_filesize(h);
    if (!filesize)
        return size;
    else if (pos > filesize)
        return 0;
    else
        return FFMIN(filesize - pos, size);
}

static int shared_read(URLContext *h, unsigned char *buf, int size)
{
    SharedContext *s = h->priv_data;
    uint8_t *tmp;
    int ret;

    if (size <= 0)
        return 0;

    size = clamp_size(h, size, s->pos);
    if (size <= 0)
        return AVERROR_EOF;

    const int shift = atomic_load_explicit(&s->spacemap->block_shift, memory_order_relaxed);
    const int64_t block_id = s->pos >> shift;
    const int64_t offset = s->pos & (s->block_size - 1);
    const int64_t block_pos = block_id * s->block_size;
    int block_size = clamp_size(h, s->block_size, block_pos);
    ret = spacemap_grow(h, block_id);
    if (ret < 0)
        return ret;

    Block *const block = &s->spacemap->blocks[block_id];
    unsigned short state = atomic_load_explicit(&block->state, memory_order_acquire);
    int64_t pending_since = 0;
    int verify_read = 0;

retry:
    switch (state) {
    default:
        /* We always need to read the entire block to verify integrity */
        block_size = clamp_size(h, block_size, block_pos); /* filesize may have changed */
        if (s->cache_data) {
            av_assert1(block_pos + block_size <= s->cache_size);
            tmp = s->cache_data + block_pos;
        } else {
            tmp = s->tmp_buf;
            ret = read_cache(s, tmp, block_size, block_pos);
            if (ret < 0) {
                av_log(h, AV_LOG_ERROR, "Failed to read from cache file: %s\n", av_err2str(ret));
                return ret;
            }
        }

        uint16_t crc = get_block_crc(tmp, block_size);
        if (crc != state) {
            av_log(h, AV_LOG_ERROR, "Cache corruption detected for block 0x%"PRIx64" at "
                   "offset 0x%"PRIx64": expected CRC: 0x%04X, got: 0x%04X\n",
                   block_id, block_pos, state, crc);
            return AVERROR(EIO);
        }

        tmp += (ptrdiff_t) offset;
        size = FFMIN(size, block_size - offset);
        if (s->verify) {
            verify_read = 1;
            break; /* fall through to the cache miss logic */
        }

        memcpy(buf, tmp, size);
        s->nb_hit++;
        s->pos += size;
        return size;

    case BLOCK_FAILED:
        if (!s->retry_errors)
            return AVERROR(EIO);
        av_fallthrough;
    case BLOCK_NONE:
        if (s->read_only)
            break; /* don't mark block as pending */
        if (atomic_compare_exchange_weak_explicit(&block->state, &state,
                                                  BLOCK_PENDING,
                                                  memory_order_acquire,
                                                  memory_order_acquire))
        {
            /* Acquired pending state, proceed to fetch the block */
            state = BLOCK_PENDING;
            break;
        }
        /* CAS failed, another thread changed the state; reload it */
        goto retry;

    case BLOCK_PENDING:
        /* Another thread is busy fetching this block, wait for it to finish */
        if (!s->timeout) {
            break; /* no timeout requested, immediately race to fetch block */
        } else if (pending_since) {
            int64_t new = av_gettime_relative();
            if (new - pending_since >= s->timeout)
                break; /* timeout expired, try to fetch the block ourselves */
        } else {
            pending_since = av_gettime_relative();
        }

        /* Make sure we try a few times before giving up */
        av_usleep(s->timeout >> 4);
        state = atomic_load_explicit(&block->state, memory_order_acquire);
        goto retry;
    }

    /* Cache miss, fetch this block from underlying protocol */
    s->nb_miss++;

    const int read_only = s->read_only || s->write_err || verify_read;
    int64_t inner_pos = read_only ? s->pos : block_pos;
    if (s->inner_pos != inner_pos) {
        inner_pos = ffurl_seek(s->inner, inner_pos, SEEK_SET);
        if (inner_pos < 0) {
            av_log(h, AV_LOG_ERROR, "Failed to seek underlying protocol: %s\n",
                   av_err2str(inner_pos));
            if (!read_only) {
                /* Release pending state to avoid stalling other threads. Don't
                 * mark this as failed, since the seek error may be unrelated to
                 * the block and should probably be tried again. */
                atomic_compare_exchange_strong_explicit(&block->state, &state,
                                                        BLOCK_NONE,
                                                        memory_order_relaxed,
                                                        memory_order_relaxed);
            }
            return inner_pos;
        }

        av_log(h, AV_LOG_DEBUG, "Inner seek to 0x%"PRIx64"\n", inner_pos);
        s->inner_pos = inner_pos;
    }

    if (read_only) {
        /* Directly defer to the underlying protocol */
        ret = ffurl_read(s->inner, buf, size);
        if (ret < 0)
            return ret;

        /* Verify the read data against the cached data if requested */
        if (verify_read && memcmp(buf, tmp, ret)) {
            av_log(h, AV_LOG_ERROR, "Cache verification failed for %d bytes "
                   "in block 0x%"PRIx64" at offset 0x%"PRIx64" + %"PRId64"!\n",
                   ret, block_id, block_pos, offset);
        }

        s->pos = s->inner_pos = inner_pos + ret;
        return ret;
    }

    int write_back = 1;
    if (s->cache_data) {
        /* Read directly into memory mapped cache file */
        tmp = s->cache_data + block_pos;
        write_back = 0;
    } else if (size >= block_size && !offset) {
        /* Read directly into output buffer if aligned and large enough */
        tmp = buf;
    } else {
        /* Read into temporary buffer and copy later */
        tmp = s->tmp_buf;
    }

    /* Try and fetch the entire block */
    av_assert0(inner_pos == block_pos);
    int bytes_read = 0;
    while (bytes_read < block_size) {
        ret = ffurl_read(s->inner, &tmp[bytes_read], block_size - bytes_read);
        if (!ret || ret == AVERROR_EOF)
            break;
        else if (ret < 0) {
            av_log(h, AV_LOG_ERROR, "Failed to read block 0x%"PRIx64": %s\n",
                   block_id, av_err2str(ret));
            /* Try to mark block as failed; ignore errors - any mismatch
             * here will mean that either another thread already marked it
             * as failed, or successfully cached it in the meantime */
            atomic_compare_exchange_strong_explicit(&block->state, &state,
                                                    BLOCK_FAILED,
                                                    memory_order_relaxed,
                                                    memory_order_relaxed);
            return ret;
        }

        bytes_read += ret;
        s->inner_pos += ret;
    }

    if (bytes_read < block_size) {
        /* Learned location of true EOF, update filesize */
        ret = set_filesize(h, inner_pos + bytes_read);
        if (ret < 0)
            return ret;
    }

    if (bytes_read > 0) {
        ret = write_back ? write_cache(s, tmp, bytes_read, block_pos) : 0;
        if (ret < 0) {
            av_log(h, AV_LOG_ERROR, "Failed to write to cache file: %s\n",
                   av_err2str(ret));
            s->write_err = 1;
            /* Mark as NONE, not FAILED, since the block itself is fine -
             * just absent from the cache. */
            atomic_compare_exchange_strong_explicit(&block->state, &state,
                                                    BLOCK_NONE,
                                                    memory_order_relaxed,
                                                    memory_order_relaxed);
        } else {
            uint16_t crc = get_block_crc(tmp, bytes_read);
            av_log(h, AV_LOG_TRACE, "Cached %d bytes to block 0x%"PRIx64" at "
                   "offset 0x%"PRIx64", CRC 0x%04X\n", bytes_read, block_id,
                   block_pos, crc);
            atomic_store_explicit(&block->state, crc, memory_order_release);
        }
    } else {
        return AVERROR_EOF;
    }

    size = FFMIN(bytes_read - offset, size);
    av_assert0(size > 0);
    if (tmp != buf)
        memcpy(buf, &tmp[offset], size);
    s->pos += size;
    return size;
}

static int64_t shared_seek(URLContext *h, int64_t pos, int whence)
{
    SharedContext *s = h->priv_data;
    const int64_t filesize = get_filesize(h);
    int64_t res;

    switch (whence) {
    case AVSEEK_SIZE:
        if (filesize)
            return filesize;
        res = ffurl_seek(s->inner, pos, whence);
        if (res > 0)
            set_filesize(h, res);
        return res;
    case SEEK_SET:
        break;
    case SEEK_CUR:
        pos += s->pos;
        break;
    case SEEK_END:
        if (filesize) {
            pos += filesize;
            break;
        }
        /* Defer to underlying protocol if filesize is unknown */
        res = ffurl_seek(s->inner, pos, whence);
        if (res < 0)
            return res;
        set_filesize(h, res - pos); /* Opportunistically update known filesize */
        av_log(h, AV_LOG_DEBUG, "Inner seek to 0x%"PRIx64"\n", res);
        return s->pos = s->inner_pos = res;
    default:
        return AVERROR(EINVAL);
    }

    if (pos < 0)
        return AVERROR(EINVAL);

    av_log(h, AV_LOG_DEBUG, "Virtual seek to 0x%"PRIx64"\n", pos);
    return s->pos = pos;
}

static int shared_get_file_handle(URLContext *h)
{
    SharedContext *s = h->priv_data;
    return ffurl_get_file_handle(s->inner);
}

static int shared_get_short_seek(URLContext *h)
{
    SharedContext *s = h->priv_data;
    int ret = ffurl_get_short_seek(s->inner);
    if (ret < 0)
        return ret;
    return FFMAX(ret, s->block_size);
}

#define OFFSET(x) offsetof(SharedContext, x)
#define D AV_OPT_FLAG_DECODING_PARAM

static const AVOption options[] = {
    { "cache_dir",      "Directory path for shared file cache",             OFFSET(cache_dir),      AV_OPT_TYPE_STRING, {.str = NULL}, .flags = D },
    { "block_shift",    "Set the base 2 logarithm of the block size",       OFFSET(block_shift),    AV_OPT_TYPE_INT, {.i64 = 15}, 9, 30, .flags = D },
    { "read_only",      "Don't write data to the cache, only read from it", OFFSET(read_only),      AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, .flags = D },
    { "cache_verify",   "Verify correctness of the cache against the source",   OFFSET(verify),     AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, .flags = D },
    { "cache_timeout",  "Time in us to wait before re-fetching pending blocks", OFFSET(timeout),    AV_OPT_TYPE_INT64, {.i64 = 0}, 0, INT64_MAX, .flags = D },
    { "retry_errors",   "Re-request blocks even if they previously failed", OFFSET(retry_errors),   AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, .flags = D },
    {0},
};

static const AVClass shared_context_class = {
    .class_name = "shared",
    .item_name  = av_default_item_name,
    .option     = options,
    .version    = LIBAVUTIL_VERSION_INT,
};

const URLProtocol ff_shared_protocol = {
    .name                = "shared",
    .url_open2           = shared_open,
    .url_read            = shared_read,
    .url_seek            = shared_seek,
    .url_close           = shared_close,
    .url_get_file_handle = shared_get_file_handle,
    .url_get_short_seek  = shared_get_short_seek,
    .priv_data_size      = sizeof(SharedContext),
    .priv_data_class     = &shared_context_class,
};
