renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
187 lines • 8.21 kB
JavaScript
;
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