"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DebDatasource = void 0;
const tslib_1 = require("tslib");
const readline_1 = tslib_1.__importDefault(require("readline"));
const nanoid_1 = require("nanoid");
const upath_1 = tslib_1.__importDefault(require("upath"));
const logger_1 = require("../../../logger");
const decorator_1 = require("../../../util/cache/package/decorator");
const fs = tslib_1.__importStar(require("../../../util/fs"));
const hash_1 = require("../../../util/hash");
const url_1 = require("../../../util/url");
const datasource_1 = require("../datasource");
const checksum_1 = require("./checksum");
const common_1 = require("./common");
const file_1 = require("./file");
const release_1 = require("./release");
const url_2 = require("./url");
class DebDatasource extends datasource_1.Datasource {
    static id = 'deb';
    constructor() {
        super(DebDatasource.id);
    }
    /**
     * Users are able to specify custom Debian repositories as long as they follow
     * the Debian package repository format as specified here
     * @see{https://wiki.debian.org/DebianRepository/Format}
     */
    customRegistrySupport = true;
    /**
     * Users can specify multiple upstream repositories and the datasource will aggregate the release
     * @example
     * When specifying multiple dependencies both internal and external dependencies from internal/external artifactory
     */
    registryStrategy = 'merge';
    /**
     * The original apt source list file format is
     * deb uri distribution [component1] [component2] [...]
     * @see{https://wiki.debian.org/DebianRepository/Format}
     *
     * However, for Renovate, we require the registry URLs to be
     * valid URLs which is why the parameters are encoded in the URL.
     *
     * The following query parameters are required:
     * - components: comma separated list of components
     * - suite: stable, oldstable or other alias for a release, either this or release must be given
     * - release: buster, etc.
     * - binaryArch: e.g. amd64 resolves to http://deb.debian.org/debian/dists/stable/non-free/binary-amd64/
     */
    defaultRegistryUrls = [
        'https://deb.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64',
    ];
    defaultVersioning = 'deb';
    /**
     * Downloads and extracts a package file from a component URL.
     *
     * @param componentUrl - The URL of the component.
     * @returns The path to the extracted file and the last modification timestamp.
     * @throws Will throw an error if no valid compression method is found.
     */
    async downloadAndExtractPackage(componentUrl) {
        const packageUrlHash = (0, hash_1.toSha256)(componentUrl);
        const fullCacheDir = await fs.ensureCacheDir(common_1.cacheSubDir);
        const extractedFile = upath_1.default.join(fullCacheDir, `${packageUrlHash}.txt`);
        let lastTimestamp = await (0, file_1.getFileCreationTime)(extractedFile);
        const compression = 'gz';
        const compressedFile = upath_1.default.join(fullCacheDir, `${(0, nanoid_1.nanoid)()}_${packageUrlHash}.${compression}`);
        const wasUpdated = await this.downloadPackageFile(componentUrl, compression, compressedFile, lastTimestamp);
        if (wasUpdated || !lastTimestamp) {
            try {
                await (0, file_1.extract)(compressedFile, compression, extractedFile);
                lastTimestamp = await (0, file_1.getFileCreationTime)(extractedFile);
            }
            catch (error) {
                logger_1.logger.error({
                    componentUrl,
                    compression,
                    error: error.message,
                }, `Failed to extract package file from ${compressedFile}`);
            }
            finally {
                await fs.rmCache(compressedFile);
            }
        }
        if (!lastTimestamp) {
            //extracting went wrong
            throw new Error('Missing metadata in extracted package index file!');
        }
        return { extractedFile, lastTimestamp };
    }
    /**
     * Downloads a package file if it has been modified since the last download timestamp.
     *
     * @param basePackageUrl - The base URL of the package.
     * @param compression - The compression method used (e.g., 'gz').
     * @param compressedFile - The path where the compressed file will be saved.
     * @param lastDownloadTimestamp - The timestamp of the last download.
     * @returns True if the file was downloaded, otherwise false.
     */
    async downloadPackageFile(basePackageUrl, compression, compressedFile, lastDownloadTimestamp) {
        const baseReleaseUrl = (0, url_2.getBaseReleaseUrl)(basePackageUrl);
        const packageUrl = (0, url_1.joinUrlParts)(basePackageUrl, `Packages.${compression}`);
        let needsToDownload = true;
        if (lastDownloadTimestamp) {
            needsToDownload = await this.checkIfModified(packageUrl, lastDownloadTimestamp);
        }
        if (!needsToDownload) {
            logger_1.logger.debug(`No need to download ${packageUrl}, file is up to date.`);
            return false;
        }
        const readStream = this.http.stream(packageUrl);
        const writeStream = fs.createCacheWriteStream(compressedFile);
        await fs.pipeline(readStream, writeStream);
        logger_1.logger.debug({ url: packageUrl, targetFile: compressedFile }, 'Downloading Debian package file');
        let inReleaseContent = '';
        try {
            inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl);
        }
        catch (error) {
            // This is expected to fail for Artifactory if GPG verification is not enabled
            logger_1.logger.debug({ url: baseReleaseUrl, err: error }, 'Could not fetch InRelease file');
        }
        if (inReleaseContent) {
            const actualChecksum = await (0, checksum_1.computeFileChecksum)(compressedFile);
            const expectedChecksum = (0, checksum_1.parseChecksumsFromInRelease)(inReleaseContent, 
            // path to the Package.gz file
            packageUrl.replace(`${baseReleaseUrl}/`, ''));
            if (actualChecksum !== expectedChecksum) {
                await fs.rmCache(compressedFile);
                throw new Error('SHA256 checksum validation failed');
            }
        }
        return needsToDownload;
    }
    /**
     * Fetches the content of the InRelease file from the given base release URL.
     *
     * @param baseReleaseUrl - The base URL of the release (e.g., 'https://deb.debian.org/debian/dists/bullseye').
     * @returns resolves to the content of the InRelease file.
     * @throws An error if the InRelease file could not be downloaded.
     */
    async fetchInReleaseFile(baseReleaseUrl) {
        const inReleaseUrl = (0, url_1.joinUrlParts)(baseReleaseUrl, 'InRelease');
        const response = await this.http.get(inReleaseUrl);
        return response.body;
    }
    /**
     * Checks if a packageUrl content has been modified since the specified timestamp.
     *
     * @param packageUrl - The URL to check.
     * @param lastDownloadTimestamp - The timestamp of the last download.
     * @returns True if the content has been modified, otherwise false.
     * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
     */
    async checkIfModified(packageUrl, lastDownloadTimestamp) {
        const options = {
            headers: {
                'If-Modified-Since': lastDownloadTimestamp.toUTCString(),
            },
        };
        try {
            const response = await this.http.head(packageUrl, options);
            return response.statusCode !== 304;
        }
        catch (error) {
            logger_1.logger.warn(`Could not determine if ${packageUrl} is modified since ${lastDownloadTimestamp.toUTCString()}: ${error.message}`);
            return true; // Assume it needs to be downloaded if check fails
        }
    }
    /**
     * Parses the extracted package index file.
     *
     * @param extractedFile - The path to the extracted package file.
     * @param lastTimestamp - The timestamp of the last modification.
     * @returns a list of packages with minimal Metadata.
     */
    async parseExtractedPackageIndex(extractedFile, lastTimestamp) {
        // read line by line to avoid high memory consumption as the extracted Packages
        // files can be multiple MBs in size
        const rl = readline_1.default.createInterface({
            input: fs.createCacheReadStream(extractedFile),
            terminal: false,
        });
        let currentPackage = {};
        // A Package Index can contain multiple Versions of the package on private Artifactory (e.g. Jfrog)
        const allPackages = {};
        for await (const line of rl) {
            if (line === '') {
                // All information of the package are available, add to the list of packages
                if (common_1.requiredPackageKeys.every((key) => key in currentPackage)) {
                    if (!allPackages[currentPackage.Package]) {
                        allPackages[currentPackage.Package] = [];
                    }
                    allPackages[currentPackage.Package].push(currentPackage);
                    currentPackage = {};
                }
            }
            else {
                for (const key of common_1.packageKeys) {
                    if (line.startsWith(`${key}:`)) {
                        currentPackage[key] = line.substring(key.length + 1).trim();
                        break;
                    }
                }
            }
        }
        // Check the last package after file reading is complete
        if (common_1.requiredPackageKeys.every((key) => key in currentPackage)) {
            if (!allPackages[currentPackage.Package]) {
                allPackages[currentPackage.Package] = [];
            }
            allPackages[currentPackage.Package].push(currentPackage);
        }
        return allPackages;
    }
    async getPackageIndex(componentUrl) {
        const { extractedFile, lastTimestamp } = await this.downloadAndExtractPackage(componentUrl);
        return await this.parseExtractedPackageIndex(extractedFile, lastTimestamp);
    }
    /**
     * Fetches the release information for a given package from the registry URL.
     *
     * @param config - Configuration for fetching releases.
     * @returns The release result if the package is found, otherwise null.
     */
    async getReleases({ registryUrl, packageName, }) {
        // istanbul ignore if
        if (!registryUrl) {
            return null;
        }
        const componentUrls = (0, url_2.constructComponentUrls)(registryUrl);
        let aggregatedRelease = null;
        for (const componentUrl of componentUrls) {
            try {
                const packageIndex = await this.getPackageIndex(componentUrl);
                const parsedPackages = packageIndex[packageName];
                if (parsedPackages) {
                    const newRelease = (0, release_1.formatReleaseResult)(parsedPackages);
                    if (aggregatedRelease === null) {
                        aggregatedRelease = newRelease;
                    }
                    else {
                        if (!(0, release_1.releaseMetaInformationMatches)(aggregatedRelease, newRelease)) {
                            logger_1.logger.warn({ packageName }, 'Package occurred in more than one repository with different meta information. Aggregating releases anyway.');
                        }
                        aggregatedRelease.releases.push(...newRelease.releases);
                    }
                }
            }
            catch (error) {
                logger_1.logger.debug({ componentUrl, error }, 'Skipping component due to an error');
            }
        }
        return aggregatedRelease;
    }
}
exports.DebDatasource = DebDatasource;
tslib_1.__decorate([
    (0, decorator_1.cache)({
        namespace: `datasource-${DebDatasource.id}`,
        key: (extractedFile, lastTimestamp) => `${extractedFile}:${lastTimestamp.getTime()}`,
        ttlMinutes: 24 * 60,
    })
], DebDatasource.prototype, "parseExtractedPackageIndex", null);
tslib_1.__decorate([
    (0, decorator_1.cache)({
        namespace: `datasource-${DebDatasource.id}`,
        key: (componentUrl) => componentUrl,
    })
], DebDatasource.prototype, "getPackageIndex", null);
tslib_1.__decorate([
    (0, decorator_1.cache)({
        namespace: `datasource-${DebDatasource.id}`,
        key: ({ registryUrl, packageName }) => `${registryUrl}:${packageName}`,
    })
], DebDatasource.prototype, "getReleases", null);
//# sourceMappingURL=index.js.map