UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

187 lines • 8.21 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RpmDatasource = void 0; const tslib_1 = require("tslib"); const node_stream_1 = require("node:stream"); const node_zlib_1 = require("node:zlib"); const util_1 = require("util"); const sax_1 = tslib_1.__importDefault(require("sax")); const xmldoc_1 = require("xmldoc"); const logger_1 = require("../../../logger"); const decorator_1 = require("../../../util/cache/package/decorator"); const url_1 = require("../../../util/url"); const datasource_1 = require("../datasource"); const gunzipAsync = (0, util_1.promisify)(node_zlib_1.gunzip); class RpmDatasource extends datasource_1.Datasource { static id = 'rpm'; // repomd.xml is a standard file name in RPM repositories which contains metadata about the repository static repomdXmlFileName = 'repomd.xml'; constructor() { super(RpmDatasource.id); } /** * Users are able to specify custom RPM repositories as long as they follow the format. * There is a URI http://linux.duke.edu/metadata/common in the <sha>-primary.xml. * But according to this post, it's not something we can really look into or reference. * @see{https://lists.rpm.org/pipermail/rpm-ecosystem/2015-October/000283.html} */ customRegistrySupport = true; /** * Fetches the release information for a given package from the registry URL. * * @param registryUrl - the registryUrl should be the folder which contains repodata.xml and its corresponding file list <sha256>-primary.xml.gz, e.g.: https://packages.microsoft.com/azurelinux/3.0/prod/cloud-native/x86_64/repodata/ * @param packageName - the name of the package to fetch releases for. * @returns The release result if the package is found, otherwise null. */ async getReleases({ registryUrl, packageName, }) { if (!registryUrl || !packageName) { return null; } try { const primaryGzipUrl = await this.getPrimaryGzipUrl(registryUrl); if (!primaryGzipUrl) { return null; } return await this.getReleasesByPackageName(primaryGzipUrl, packageName); } catch (err) { this.handleGenericErrors(err); } } // Fetches the primary.xml.gz URL from the repomd.xml file. async getPrimaryGzipUrl(registryUrl) { const repomdUrl = (0, url_1.joinUrlParts)(registryUrl, RpmDatasource.repomdXmlFileName); const response = await this.http.getText(repomdUrl.toString()); // check if repomd.xml is in XML format if (!response.body.startsWith('<?xml')) { logger_1.logger.debug({ datasource: RpmDatasource.id, url: repomdUrl }, 'Invalid response format'); throw new Error(`${repomdUrl} is not in XML format. Response body: ${response.body}`); } // parse repomd.xml using XmlDocument const xml = new xmldoc_1.XmlDocument(response.body); const primaryData = xml.childWithAttribute('type', 'primary'); if (!primaryData) { logger_1.logger.debug(`No primary data found in ${repomdUrl}, xml contents: ${response.body}`); throw new Error(`No primary data found in ${repomdUrl}`); } const locationElement = primaryData.childNamed('location'); if (!locationElement) { throw new Error(`No location element found in ${repomdUrl}`); } const href = locationElement.attr.href; if (!href) { throw new Error(`No href found in ${repomdUrl}`); } // replace trailing "repodata/" from registryUrl, if it exists, with a "/" because href includes "repodata/" const registryUrlWithoutRepodata = registryUrl.replace(/\/repodata\/?$/, '/'); return (0, url_1.joinUrlParts)(registryUrlWithoutRepodata, href); } async getReleasesByPackageName(primaryGzipUrl, packageName) { let response; let decompressedBuffer; try { // primaryGzipUrl is a .gz file, need to extract it before parsing response = await this.http.getBuffer(primaryGzipUrl); if (response.body.length === 0) { logger_1.logger.debug(`Empty response body from getting ${primaryGzipUrl}.`); throw new Error(`Empty response body from getting ${primaryGzipUrl}.`); } // decompress the gzipped file decompressedBuffer = await gunzipAsync(response.body); } catch (err) { logger_1.logger.debug(`Failed to fetch or decompress ${primaryGzipUrl}: ${err instanceof Error ? err.message : err}`); throw err; } // Use sax streaming parser to handle large XML files efficiently // This allows us to parse the XML file without loading the entire file into memory. const releases = {}; let insidePackage = false; let isTargetPackage = false; let insideName = false; // Create a SAX parser in strict mode const saxParser = sax_1.default.createStream(true, { lowercase: true, // normalize tag names to lowercase trim: true, }); saxParser.on('opentag', (node) => { if (node.name === 'package' && node.attributes.type === 'rpm') { insidePackage = true; isTargetPackage = false; } if (insidePackage && node.name === 'name') { insideName = true; } if (insidePackage && isTargetPackage && node.name === 'version') { // rel is optional if (node.attributes.rel === undefined) { const version = `${node.attributes.ver}`; releases[version] = { version }; } else { const version = `${node.attributes.ver}-${node.attributes.rel}`; releases[version] = { version }; } } }); saxParser.on('text', (text) => { if (insidePackage && insideName) { if (text.trim() === packageName) { isTargetPackage = true; } } }); saxParser.on('closetag', (tag) => { if (tag === 'name' && insidePackage) { insideName = false; } if (tag === 'package') { insidePackage = false; isTargetPackage = false; } }); await new Promise((resolve, reject) => { let settled = false; saxParser.on('error', (err) => { if (settled) { return; } settled = true; logger_1.logger.debug(`SAX parsing error in ${primaryGzipUrl}: ${err.message}`); setImmediate(() => saxParser.removeAllListeners()); reject(err); }); saxParser.on('end', () => { settled = true; setImmediate(() => saxParser.removeAllListeners()); resolve(); }); node_stream_1.Readable.from(decompressedBuffer).pipe(saxParser); }); if (Object.keys(releases).length === 0) { logger_1.logger.trace(`No releases found for package ${packageName} in ${primaryGzipUrl}`); return null; } return { releases: Object.values(releases).map((release) => ({ version: release.version, })), }; } } exports.RpmDatasource = RpmDatasource; tslib_1.__decorate([ (0, decorator_1.cache)({ namespace: `datasource-${RpmDatasource.id}`, key: ({ registryUrl, packageName }) => `${registryUrl}:${packageName}`, ttlMinutes: 1440, }) ], RpmDatasource.prototype, "getReleases", null); tslib_1.__decorate([ (0, decorator_1.cache)({ namespace: `datasource-${RpmDatasource.id}`, key: (registryUrl) => registryUrl, ttlMinutes: 1440, }) ], RpmDatasource.prototype, "getPrimaryGzipUrl", null); //# sourceMappingURL=index.js.map