/*
 * Copyright 2026 Bloomberg LP
 * Copyright 2026 The Bazel Authors
 *
 * 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 <buildboxcommon_fastcdcutils.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>

#include <algorithm>
#include <array>
#include <bit>
#include <cerrno>
#include <cstring>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>

#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>

namespace buildboxcommon {

namespace {

// Number of entries in the Gear table (one per possible byte value).
constexpr size_t GEAR_TABLE_SIZE = 256;

// The Gear table used for the rolling hash. These are seemingly random numbers
// created by computing the MD5 digest of the values 0 to 255 and taking the
// high 8 bytes of each 16-byte digest. This is the "gear hash" referred to in
// the FastCDC paper; the same table is used by the reference implementations.
// clang-format off
constexpr std::array<uint64_t, GEAR_TABLE_SIZE> GEAR = {
    0x3b5d3c7d207e37dcULL, 0x784d68ba91123086ULL, 0xcd52880f882e7298ULL, 0xeacf8e4e19fdcca7ULL,
    0xc31f385dfbd1632bULL, 0x1d5f27001e25abe6ULL, 0x83130bde3c9ad991ULL, 0xc4b225676e9b7649ULL,
    0xaa329b29e08eb499ULL, 0xb67fcbd21e577d58ULL, 0x0027baaada2acf6bULL, 0xe3ef2d5ac73c2226ULL,
    0x0890f24d6ed312b7ULL, 0xa809e036851d7c7eULL, 0xf0a6fe5e0013d81bULL, 0x1d026304452cec14ULL,
    0x03864632648e248fULL, 0xcdaacf3dcd92b9b4ULL, 0xf5e012e63c187856ULL, 0x8862f9d3821c00b6ULL,
    0xa82f7338750f6f8aULL, 0x1e583dc6c1cb0b6fULL, 0x7a3145b69743a7f1ULL, 0xabb20fee404807ebULL,
    0xb14b3cfe07b83a5dULL, 0xb9dc27898adb9a0fULL, 0x3703f5e91baa62beULL, 0xcf0bb866815f7d98ULL,
    0x3d9867c41ea9dcd3ULL, 0x1be1fa65442bf22cULL, 0x14300da4c55631d9ULL, 0xe698e9cbc6545c99ULL,
    0x4763107ec64e92a5ULL, 0xc65821fc65696a24ULL, 0x76196c064822f0b7ULL, 0x485be841f3525e01ULL,
    0xf652bc9c85974ff5ULL, 0xcad8352face9e3e9ULL, 0x2a6ed1dceb35e98eULL, 0xc6f483badc11680fULL,
    0x3cfd8c17e9cf12f1ULL, 0x89b83c5e2ea56471ULL, 0xae665cfd24e392a9ULL, 0xec33c4e504cb8915ULL,
    0x3fb9b15fc9fe7451ULL, 0xd7fd1fd1945f2195ULL, 0x31ade0853443efd8ULL, 0x255efc9863e1e2d2ULL,
    0x10eab6008d5642cfULL, 0x46f04863257ac804ULL, 0xa52dc42a789a27d3ULL, 0xdaaadf9ce77af565ULL,
    0x6b479cd53d87febbULL, 0x6309e2d3f93db72fULL, 0xc5738ffbaa1ff9d6ULL, 0x6bd57f3f25af7968ULL,
    0x67605486d90d0a4aULL, 0xe14d0b9663bfbdaeULL, 0xb7bbd8d816eb0414ULL, 0xdef8a4f16b35a116ULL,
    0xe7932d85aaaffed6ULL, 0x08161cbae90cfd48ULL, 0x855507beb294f08bULL, 0x91234ea6ffd399b2ULL,
    0xad70cf4b2435f302ULL, 0xd289a97565bc2d27ULL, 0x8e558437ffca99deULL, 0x96d2704b7115c040ULL,
    0x0889bbcdfc660e41ULL, 0x5e0d4e67dc92128dULL, 0x72a9f8917063ed97ULL, 0x438b69d409e016e3ULL,
    0xdf4fed8a5d8a4397ULL, 0x00f41dcf41d403f7ULL, 0x4814eb038e52603fULL, 0x9dafbacc58e2d651ULL,
    0xfe2f458e4be170afULL, 0x4457ec414df6a940ULL, 0x06e62f1451123314ULL, 0xbd1014d173ba92ccULL,
    0xdef318e25ed57760ULL, 0x9fea0de9dfca8525ULL, 0x459de1e76c20624bULL, 0xaeec189617e2d666ULL,
    0x126a2c06ab5a83cbULL, 0xb1321532360f6132ULL, 0x65421503dbb40123ULL, 0x2d67c287ea089ab3ULL,
    0x6c93bff5a56bd6b6ULL, 0x4ffb2036cab6d98dULL, 0xce7b785b1be7ad4fULL, 0xedb42ef6189fd163ULL,
    0xdc905288703988f6ULL, 0x365f9c1d2c691884ULL, 0xc640583680d99bfeULL, 0x3cd4624c07593ec6ULL,
    0x7f1ea8d85d7c5805ULL, 0x014842d480b57149ULL, 0x0b649bcb5a828688ULL, 0xbcd5708ed79b18f0ULL,
    0xe987c862fbd2f2f0ULL, 0x982731671f0cd82cULL, 0xbaf13e8b16d8c063ULL, 0x8ea3109cbd951bbaULL,
    0xd141045bfb385cadULL, 0x2acbc1a0af1f7d30ULL, 0xe6444d89df03bfdfULL, 0xa18cc771b8188ff9ULL,
    0x9834429db01c39bbULL, 0x214add07fe086a1fULL, 0x8f07c19b1f6b3ff9ULL, 0x56a297b1bf4ffe55ULL,
    0x94d558e493c54fc7ULL, 0x40bfc24c764552cbULL, 0x931a706f8a8520cbULL, 0x32229d322935bd52ULL,
    0x2560d0f5dc4fefafULL, 0x9dbcc48355969bb6ULL, 0x0fd81c3985c0b56aULL, 0xe03817e1560f2bdaULL,
    0xc1bb4f81d892b2d5ULL, 0xb0c4864f4e28d2d7ULL, 0x3ecc49f9d9d6c263ULL, 0x51307e99b52ba65eULL,
    0x8af2b688da84a752ULL, 0xf5d72523b91b20b6ULL, 0x6d95ff1ff4634806ULL, 0x562f21555458339aULL,
    0xc0ce47f889336346ULL, 0x487823e5089b40d8ULL, 0xe4727c7ebc6d9592ULL, 0x5a8f7277e94970baULL,
    0xfca2f406b1c8bb50ULL, 0x5b1f8a95f1791070ULL, 0xd304af9fc9028605ULL, 0x5440ab7fc930e748ULL,
    0x312d25fbca2ab5a1ULL, 0x10f4a4b234a4d575ULL, 0x90301d55047e7473ULL, 0x3b6372886c61591eULL,
    0x293402b77c444e06ULL, 0x451f34a4d3e97dd7ULL, 0x3158d814d81bc57bULL, 0x034942425b9bda69ULL,
    0xe2032ff9e532d9bbULL, 0x62ae066b8b2179e5ULL, 0x9545e10c2f8d71d8ULL, 0x7ff7483eb2d23fc0ULL,
    0x00945fcebdc98d86ULL, 0x8764bbbe99b26ca2ULL, 0x1b1ec62284c0bfc3ULL, 0x58e0fcc4f0aa362bULL,
    0x5f4abefa878d458dULL, 0xfd74ac2f9607c519ULL, 0xa4e3fb37df8cbfa9ULL, 0xbf697e43cac574e5ULL,
    0x86f14a3f68f4cd53ULL, 0x24a23d076f1ce522ULL, 0xe725cd8048868cc8ULL, 0xbf3c729eb2464362ULL,
    0xd8f6cd57b3cc1ed8ULL, 0x6329e52425541577ULL, 0x62aa688ad5ae1ac0ULL, 0x0a242566269bf845ULL,
    0x168b1a4753aca74bULL, 0xf789afefff2e7e3cULL, 0x6c3362093b6fccdbULL, 0x4ce8f50bd28c09b2ULL,
    0x006a2db95ae8aa93ULL, 0x975b0d623c3d1a8cULL, 0x18605d3935338c5bULL, 0x5bb6f6136cad3c71ULL,
    0x0f53a20701f8d8a6ULL, 0xab8c5ad2e7e93c67ULL, 0x40b5ac5127acaa29ULL, 0x8c7bf63c2075895fULL,
    0x78bd9f7e014a805cULL, 0xb2c9e9f4f9c8c032ULL, 0xefd6049827eb91f3ULL, 0x2be459f482c16fbdULL,
    0xd92ce0c5745aaa8cULL, 0x0aaa8fb298d965b9ULL, 0x2b37f92c6c803b15ULL, 0x8c54a5e94e0f0e78ULL,
    0x95f9b6e90c0a3032ULL, 0xe7939faa436c7874ULL, 0xd16bfe8f6a8a40c9ULL, 0x44982b86263fd2faULL,
    0xe285fb39f984e583ULL, 0x779a8df72d7619d3ULL, 0xf2d79a8de8d5dd1eULL, 0xd1037354d66684e2ULL,
    0x004c82a4e668a8e5ULL, 0x31d40a7668b044e6ULL, 0xd70578538bd02c11ULL, 0xdb45431078c5f482ULL,
    0x977121bb7f6a51adULL, 0x73d5ccbd34eff8ddULL, 0xe437a07d356e17cdULL, 0x47b2782043c95627ULL,
    0x9fb251413e41d49aULL, 0xccd70b60652513d3ULL, 0x1c95b31e8a1b49b2ULL, 0xcae73dfd1bcb4c1bULL,
    0x34d98331b1f5b70fULL, 0x784e39f22338d92fULL, 0x18613d4a064df420ULL, 0xf1d8dae25f0bcebeULL,
    0x33f77c15ae855efcULL, 0x3c88b3b912eb109cULL, 0x956a2ec96bafeea5ULL, 0x1aa005b5e0ad0e87ULL,
    0x5500d70527c4bb8eULL, 0xe36c57196421cc44ULL, 0x13c4d286cc36ee39ULL, 0x5654a23d818b2a81ULL,
    0x77b1dc13d161abdcULL, 0x734f44de5f8d5eb5ULL, 0x60717e174a6c89a2ULL, 0xd47d9649266a211eULL,
    0x5b13a4322bb69e90ULL, 0xf7669609f8b5fc3cULL, 0x21e6ac55bedcdac9ULL, 0x9b56b62b61166deaULL,
    0xf48f66b939797e9cULL, 0x35f332f9c0e6ae9aULL, 0xcc733f6a9a878db0ULL, 0x3da161e41cc108c2ULL,
    0xb7d74ae535914d51ULL, 0x4d493b0b11d36469ULL, 0xce264d1dfba9741aULL, 0xa9d1f2dc7436dc06ULL,
    0x70738016604c2a27ULL, 0x231d36e96e93f3d5ULL, 0x7666881197838d19ULL, 0x4a2a83090aaad40cULL,
    0xf1e761591668b35dULL, 0x7363236497f730a7ULL, 0x301080e37379dd4dULL, 0x502dea2971827042ULL,
    0xc2c5eb858f32625fULL, 0x786afb9edfafbdffULL, 0xdaee0d868490b2a4ULL, 0x617366b3268609f6ULL,
    0xae0e35a0fe46173eULL, 0xd1a07de93e824f11ULL, 0x079b8b115ea4cca8ULL, 0x93a99274558faebbULL,
    0xfb1e6e22e08a03b3ULL, 0xea635fdba3698dd0ULL, 0xcf53659328503a5cULL, 0xcde3b31e6fd5d780ULL,
    0x8e3e4221d3614413ULL, 0xef14d0d86bf1a22cULL, 0xe1d830d3f16c5ddbULL, 0xaabd2b2a451504e1ULL,
};
// clang-format on

// GEAR shifted left by one, precomputed so the two-bytes-per-iteration loop in
// cut() does not have to shift on every step.
constexpr std::array<uint64_t, GEAR_TABLE_SIZE> makeGearLs()
{
    std::array<uint64_t, GEAR_TABLE_SIZE> table{};
    // Computed at compile time, so at() has no runtime cost and avoids raw
    // array-subscript lints.
    for (size_t i = 0; i < GEAR_TABLE_SIZE; ++i) {
        table.at(i) = GEAR.at(i) << 1;
    }
    return table;
}
constexpr std::array<uint64_t, GEAR_TABLE_SIZE> GEAR_LS = makeGearLs();

// Number of entries in the mask table. Indices 0-4 are unused padding.
constexpr size_t MASKS_SIZE = 26;

// Masks for each desired number of bits. The values for 64 bytes through 128
// kilobytes come from the C reference implementation (destor); the rest come
// from the restic-FastCDC repository. The bits are spread relatively evenly,
// which the FastCDC paper claims slightly improves the deduplication ratio,
// hence these seemingly "magic" values.
// clang-format off
constexpr std::array<uint64_t, MASKS_SIZE> MASKS = {
    0,                     // 0: padding
    0,                     // 1: padding
    0,                     // 2: padding
    0,                     // 3: padding
    0,                     // 4: padding
    0x0000000001804110ULL, // 5: unused except for NC 3
    0x0000000001803110ULL, // 6: 64B
    0x0000000018035100ULL, // 7: 128B
    0x0000001800035300ULL, // 8: 256B
    0x0000019000353000ULL, // 9: 512B
    0x0000590003530000ULL, // 10: 1KB
    0x0000d90003530000ULL, // 11: 2KB
    0x0000d90103530000ULL, // 12: 4KB
    0x0000d90303530000ULL, // 13: 8KB
    0x0000d90313530000ULL, // 14: 16KB
    0x0000d90f03530000ULL, // 15: 32KB
    0x0000d90303537000ULL, // 16: 64KB
    0x0000d90703537000ULL, // 17: 128KB
    0x0000d90707537000ULL, // 18: 256KB
    0x0000d91707537000ULL, // 19: 512KB
    0x0000d91747537000ULL, // 20: 1MB
    0x0000d91767537000ULL, // 21: 2MB
    0x0000d93767537000ULL, // 22: 4MB
    0x0000d93777537000ULL, // 23: 8MB
    0x0000d93777577000ULL, // 24: 16MB
    0x0000db3777577000ULL, // 25: unused except for NC 3
};
// clang-format on

// Range of mask bit-counts that map to a meaningful (non-padding) MASKS entry.
constexpr int MIN_MASK_BITS = 5;
constexpr int MAX_MASK_BITS = 25;

} // namespace

FastCDCUtils::FastCDCUtils(const Config &config)
    : d_avgSize(config.avgChunkSize)
{
    if (d_avgSize == 0 || (d_avgSize & (d_avgSize - 1)) != 0) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "avgChunkSize must be a positive power of two, got " << d_avgSize);
    }
    if (config.normalizationLevel < 0 || config.normalizationLevel > 3) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument, "normalizationLevel must be in [0, 3], got "
                                       << config.normalizationLevel);
    }

    d_minSize = d_avgSize / 4;
    d_maxSize = d_avgSize * 4;

    // log2(avgSize); avgSize is a power of two so this is exact.
    const int bits = std::countr_zero(d_avgSize);
    const int smallBits = bits + config.normalizationLevel;
    const int largeBits = bits - config.normalizationLevel;
    if (smallBits > MAX_MASK_BITS || largeBits < MIN_MASK_BITS) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::invalid_argument,
            "normalization level too extreme for avgChunkSize");
    }

    // Below avgSize maskS discourages early (too-small) cuts; above it maskL
    // encourages cuts to avoid (too-large) chunks.
    d_maskS = MASKS.at(static_cast<size_t>(smallBits));
    d_maskL = MASKS.at(static_cast<size_t>(largeBits));
    d_maskSLs = d_maskS << 1;
    d_maskLLs = d_maskL << 1;

    d_seed = config.seed;
    d_shiftedSeed = d_seed << 1;
}

size_t FastCDCUtils::cut(std::string_view data) const
{
    const size_t len = data.size();
    if (len <= d_minSize) {
        return len;
    }

    const size_t n = std::min(len, d_maxSize);
    const size_t center = std::min(n, d_avgSize);

    // Round down to even boundaries for two-byte processing so we don't need
    // to divide by two in the loop.
    const size_t minLimit = d_minSize & ~static_cast<size_t>(1);
    const size_t centerLimit = center & ~static_cast<size_t>(1);
    const size_t remainingLimit = n & ~static_cast<size_t>(1);

    const uint64_t s = d_seed;
    const uint64_t sLs = d_shiftedSeed;
    uint64_t hash = 0;

    // GEAR/GEAR_LS are indexed by a byte value (0-255) into 256-entry tables,
    // so the lookups are always in bounds; operator[] (rather than at()) keeps
    // this per-byte inner loop fast.
    // NOLINTBEGIN(cppcoreguidelines-pro-bounds-constant-array-index)

    // Below avgSize: use maskS to discourage early cuts (too-small chunks).
    for (size_t a = minLimit; a < centerLimit; a += 2) {
        const size_t byte0 = static_cast<unsigned char>(data[a]);
        hash = (hash << 2) + (GEAR_LS[byte0] ^ sLs);
        if ((hash & d_maskSLs) == 0) {
            return a;
        }
        const size_t byte1 = static_cast<unsigned char>(data[a + 1]);
        hash = hash + (GEAR[byte1] ^ s);
        if ((hash & d_maskS) == 0) {
            return a + 1;
        }
    }

    // Above avgSize: use maskL to encourage cuts (too-large chunks).
    for (size_t a = centerLimit; a < remainingLimit; a += 2) {
        const size_t byte0 = static_cast<unsigned char>(data[a]);
        hash = (hash << 2) + (GEAR_LS[byte0] ^ sLs);
        if ((hash & d_maskLLs) == 0) {
            return a;
        }
        const size_t byte1 = static_cast<unsigned char>(data[a + 1]);
        hash = hash + (GEAR[byte1] ^ s);
        if ((hash & d_maskL) == 0) {
            return a + 1;
        }
    }
    // NOLINTEND(cppcoreguidelines-pro-bounds-constant-array-index)

    return n;
}

std::vector<Digest> FastCDCUtils::chunkToDigests(int fd) const
{
    std::vector<Digest> digests;

    std::string buf(d_maxSize * 2, '\0');
    size_t cursor = 0;
    size_t end = 0;
    bool eof = false;

    while (true) {
        size_t available = end - cursor;
        if (available < d_maxSize && !eof) {
            // Slide the unconsumed tail to the front, then top the buffer up.
            if (cursor > 0 && available > 0) {
                std::memmove(&buf[0], &buf[cursor], available);
            }
            cursor = 0;
            end = available;

            while (end < buf.size()) {
                const ssize_t got = ::read(fd, &buf[end], buf.size() - end);
                if (got < 0) {
                    if (errno == EINTR) {
                        // Interrupted by a signal before reading anything;
                        // retry.
                        continue;
                    }
                    BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
                        std::system_error, errno, std::system_category,
                        "Error reading from file descriptor " << fd);
                }
                if (got == 0) {
                    eof = true;
                    break;
                }
                end += static_cast<size_t>(got);
            }
            available = end - cursor;
        }

        if (available == 0) {
            break;
        }

        const std::string_view window =
            std::string_view(buf).substr(cursor, available);
        const size_t chunkLen = cut(window);

        // Digest the chunk in place without copying it out of the buffer.
        DigestContext ctx = DigestGenerator::createDigestContext();
        ctx.update(&buf[cursor], chunkLen);
        digests.push_back(ctx.finalizeDigest());

        cursor += chunkLen;
    }

    return digests;
}

std::vector<Digest> FastCDCUtils::chunkToDigests(const std::string &path) const
{
    const FileDescriptor fd(::open(path.c_str(), O_RDONLY | O_CLOEXEC));
    if (fd.get() == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to open file \"" << path << "\"");
    }
    return chunkToDigests(fd.get());
}

std::vector<Digest> FastCDCUtils::chunkToDigests(int dirFd,
                                                 const std::string &path) const
{
    const FileDescriptor fd(
        ::openat(dirFd, path.c_str(), O_RDONLY | O_CLOEXEC));
    if (fd.get() == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Failed to open file \"" << path << "\"");
    }
    return chunkToDigests(fd.get());
}

} // namespace buildboxcommon
