"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GoProxyDatasource = void 0;
const tslib_1 = require("tslib");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const luxon_1 = require("luxon");
const moo_1 = tslib_1.__importDefault(require("moo"));
const logger_1 = require("../../../logger");
const decorator_1 = require("../../../util/cache/package/decorator");
const http_1 = require("../../../util/http");
const p = tslib_1.__importStar(require("../../../util/promises"));
const regex_1 = require("../../../util/regex");
const go_mod_directive_1 = tslib_1.__importDefault(require("../../versioning/go-mod-directive"));
const datasource_1 = require("../datasource");
const base_1 = require("./base");
const common_1 = require("./common");
const releases_direct_1 = require("./releases-direct");
const parsedGoproxy = {};
const modRegex = (0, regex_1.regEx)(/^(?<baseMod>.*?)(?:[./]v(?<majorVersion>\d+))?$/);
const pversionRegexp = (0, regex_1.regEx)(/v\d+\.\d+\.\d+-(?:"\w+\.)?(?:0\.)?(?<releaseTimestamp>\d{14})-(?<digest>[a-f0-9]{12})/);
class GoProxyDatasource extends datasource_1.Datasource {
    static id = 'go-proxy';
    constructor() {
        super(GoProxyDatasource.id);
    }
    direct = new releases_direct_1.GoDirectDatasource();
    async getReleases(config) {
        const { packageName } = config;
        logger_1.logger.trace(`goproxy.getReleases(${packageName})`);
        const goproxy = process.env.GOPROXY ?? 'https://proxy.golang.org,direct';
        if (goproxy === 'direct') {
            return this.direct.getReleases(config);
        }
        const proxyList = this.parseGoproxy(goproxy);
        const noproxy = GoProxyDatasource.parseNoproxy();
        let result = null;
        if (noproxy?.test(packageName)) {
            logger_1.logger.debug(`Fetching ${packageName} via GONOPROXY match`);
            result = await this.direct.getReleases(config);
            return result;
        }
        for (const { url, fallback } of proxyList) {
            try {
                if (url === 'off') {
                    break;
                }
                else if (url === 'direct') {
                    result = await this.direct.getReleases(config);
                    break;
                }
                const res = await this.getVersionsWithInfo(url, packageName);
                if (res.releases.length) {
                    result = res;
                    break;
                }
                if (res.tags?.latest) {
                    result = res;
                }
            }
            catch (err) {
                const statusCode = err?.response?.statusCode;
                const canFallback = fallback === '|' ? true : statusCode === 404 || statusCode === 410;
                const msg = canFallback
                    ? 'Goproxy error: trying next URL provided with GOPROXY'
                    : 'Goproxy error: skipping other URLs provided with GOPROXY';
                logger_1.logger.debug({ err }, msg);
                if (!canFallback) {
                    break;
                }
            }
        }
        if (result && !result.sourceUrl) {
            try {
                const datasource = await base_1.BaseGoDatasource.getDatasource(packageName);
                const sourceUrl = (0, common_1.getSourceUrl)(datasource);
                if (sourceUrl) {
                    result.sourceUrl = sourceUrl;
                }
            }
            catch (err) {
                logger_1.logger.trace({ err }, `Can't get datasource for ${packageName}`);
            }
        }
        return result;
    }
    /**
     * Parse `GOPROXY` to the sequence of url + fallback strategy tags.
     *
     * @example
     * parseGoproxy('foo.example.com|bar.example.com,baz.example.com')
     * // [
     * //   { url: 'foo.example.com', fallback: '|' },
     * //   { url: 'bar.example.com', fallback: ',' },
     * //   { url: 'baz.example.com', fallback: '|' },
     * // ]
     *
     * @see https://golang.org/ref/mod#goproxy-protocol
     */
    parseGoproxy(input = process.env.GOPROXY) {
        if (!is_1.default.string(input)) {
            return [];
        }
        if (parsedGoproxy[input]) {
            return parsedGoproxy[input];
        }
        const result = input
            .split((0, regex_1.regEx)(/([^,|]*(?:,|\|))/))
            .filter(Boolean)
            .map((s) => s.split(/(?=,|\|)/)) // TODO: #12872 lookahead
            .map(([url, separator]) => ({
            url,
            fallback: separator === ',' ? ',' : '|',
        }));
        parsedGoproxy[input] = result;
        return result;
    }
    // https://golang.org/pkg/path/#Match
    static lexer = moo_1.default.states({
        main: {
            separator: {
                match: /\s*?,\s*?/, // TODO #12870
                value: (_) => '|',
            },
            asterisk: {
                match: '*',
                value: (_) => '[^/]*',
            },
            qmark: {
                match: '?',
                value: (_) => '[^/]',
            },
            characterRangeOpen: {
                match: '[',
                push: 'characterRange',
                value: (_) => '[',
            },
            trailingSlash: {
                match: /\/$/,
                value: (_) => '',
            },
            char: {
                match: /[^*?\\[\n]/,
                value: (s) => s.replace((0, regex_1.regEx)('\\.', 'g'), '\\.'),
            },
            escapedChar: {
                match: /\\./, // TODO #12870
                value: (s) => s.slice(1),
            },
        },
        characterRange: {
            char: /[^\\\]\n]/, // TODO #12870
            escapedChar: {
                match: /\\./, // TODO #12870
                value: (s) => s.slice(1),
            },
            characterRangeEnd: {
                match: ']',
                pop: 1,
            },
        },
    });
    static parsedNoproxy = {};
    static parseNoproxy(input = process.env.GONOPROXY ?? process.env.GOPRIVATE) {
        if (!is_1.default.string(input)) {
            return null;
        }
        if (this.parsedNoproxy[input] !== undefined) {
            return this.parsedNoproxy[input];
        }
        this.lexer.reset(input);
        const noproxyPattern = [...this.lexer].map(({ value }) => value).join('');
        const result = noproxyPattern
            ? (0, regex_1.regEx)(`^(?:${noproxyPattern})(?:/.*)?$`)
            : null;
        this.parsedNoproxy[input] = result;
        return result;
    }
    /**
     * Avoid ambiguity when serving from case-insensitive file systems.
     *
     * @see https://golang.org/ref/mod#goproxy-protocol
     */
    encodeCase(input) {
        return input.replace((0, regex_1.regEx)(/([A-Z])/g), (x) => `!${x.toLowerCase()}`);
    }
    async listVersions(baseUrl, packageName) {
        const url = `${baseUrl}/${this.encodeCase(packageName)}/@v/list`;
        const { body } = await this.http.get(url);
        return body
            .split(regex_1.newlineRegex)
            .filter(is_1.default.nonEmptyStringAndNotWhitespace)
            .map((str) => {
            let version;
            let newDigest;
            let releaseTimestamp;
            [version, releaseTimestamp] = str.split((0, regex_1.regEx)(/\s+/));
            if (!luxon_1.DateTime.fromISO(releaseTimestamp).isValid) {
                version = str;
                releaseTimestamp = undefined;
            }
            const digestMatch = pversionRegexp.exec(version);
            if (!releaseTimestamp && digestMatch?.groups?.releaseTimestamp) {
                releaseTimestamp = digestMatch.groups.releaseTimestamp;
            }
            if (digestMatch?.groups?.digest) {
                newDigest = digestMatch.groups.digest;
            }
            return { version, newDigest, releaseTimestamp };
        });
    }
    async versionInfo(baseUrl, packageName, version) {
        const url = `${baseUrl}/${this.encodeCase(packageName)}/@v/${version}.info`;
        const res = await this.http.getJson(url);
        const result = {
            version: res.body.Version,
        };
        if (res.body.Time) {
            result.releaseTimestamp = res.body.Time;
        }
        return result;
    }
    async getLatestVersion(baseUrl, packageName) {
        try {
            const url = `${baseUrl}/${this.encodeCase(packageName)}/@latest`;
            const res = await this.http.getJson(url);
            return res.body.Version;
        }
        catch (err) {
            logger_1.logger.trace({ err }, 'Failed to get latest version');
            return null;
        }
    }
    async getVersionsWithInfo(baseUrl, packageName) {
        const isGopkgin = packageName.startsWith('gopkg.in/');
        const majorSuffixSeparator = isGopkgin ? '.' : '/';
        const modParts = packageName.match(modRegex)?.groups;
        const baseMod = modParts?.baseMod ?? /* istanbul ignore next */ packageName;
        const packageMajor = parseInt(modParts?.majorVersion ?? '0');
        const result = { releases: [] };
        for (let major = packageMajor;; major += 1) {
            let pkg = `${baseMod}${majorSuffixSeparator}v${major}`;
            if (!isGopkgin && major < 2) {
                pkg = baseMod;
                major += 1; // v0 and v1 are the same module
            }
            try {
                const res = await this.listVersions(baseUrl, pkg);
                const releases = await p.map(res, async (versionInfo) => {
                    const { version, newDigest, releaseTimestamp } = versionInfo;
                    if (releaseTimestamp) {
                        return { version, newDigest, releaseTimestamp };
                    }
                    try {
                        return await this.versionInfo(baseUrl, pkg, version);
                    }
                    catch (err) {
                        logger_1.logger.trace({ err }, `Can't obtain data from ${baseUrl}`);
                        return { version };
                    }
                });
                result.releases.push(...releases);
            }
            catch (err) {
                if (err instanceof http_1.HttpError &&
                    err.response?.statusCode === 404 &&
                    major !== packageMajor) {
                    break;
                }
                throw err;
            }
            const latestVersion = await this.getLatestVersion(baseUrl, pkg);
            if (latestVersion) {
                result.tags ??= {};
                result.tags.latest ??= latestVersion;
                if (go_mod_directive_1.default.isGreaterThan(latestVersion, result.tags.latest)) {
                    result.tags.latest = latestVersion;
                }
            }
        }
        return result;
    }
    static getCacheKey({ packageName }) {
        const goproxy = process.env.GOPROXY;
        const noproxy = GoProxyDatasource.parseNoproxy();
        // TODO: types (#22198)
        return `${packageName}@@${goproxy}@@${noproxy?.toString()}`;
    }
}
exports.GoProxyDatasource = GoProxyDatasource;
tslib_1.__decorate([
    (0, decorator_1.cache)({
        namespace: `datasource-${GoProxyDatasource.id}`,
        key: (config) => GoProxyDatasource.getCacheKey(config),
    })
], GoProxyDatasource.prototype, "getReleases", null);
//# sourceMappingURL=releases-goproxy.js.map