renovate/lib/modules/manager/nuget/util.ts

185 lines
5.4 KiB
TypeScript

import upath from 'upath';
import type { XmlElement } from 'xmldoc';
import { XmlDocument } from 'xmldoc';
import { logger } from '../../../logger';
import { findUpLocal, readLocalFile } from '../../../util/fs';
import { minimatch } from '../../../util/minimatch';
import { regEx } from '../../../util/regex';
import { nugetOrg } from '../../datasource/nuget';
import type { NugetPackageDependency, Registry } from './types';
export async function readFileAsXmlDocument(
file: string,
): Promise<XmlDocument | undefined> {
try {
// TODO #22198
const doc = new XmlDocument((await readLocalFile(file, 'utf8'))!);
// don't return empty documents
return doc?.firstChild ? doc : undefined;
} catch (err) {
logger.debug({ err, file }, `failed to parse file as XML document`);
return undefined;
}
}
/**
* The default `nuget.org` named registry.
* @returns the default registry for NuGet
*/
export function getDefaultRegistries(): Registry[] {
return [{ url: nugetOrg, name: 'nuget.org' }];
}
export async function getConfiguredRegistries(
packageFile: string,
): Promise<Registry[] | undefined> {
// Valid file names taken from https://github.com/NuGet/NuGet.Client/blob/f64621487c0b454eda4b98af853bf4a528bef72a/src/NuGet.Core/NuGet.Configuration/Settings/Settings.cs#L34
const nuGetConfigFileNames = ['nuget.config', 'NuGet.config', 'NuGet.Config'];
// normalize paths, otherwise startsWith can fail because of path delimitter mismatch
const nuGetConfigPath = await findUpLocal(
nuGetConfigFileNames,
upath.dirname(packageFile),
);
if (!nuGetConfigPath) {
return undefined;
}
logger.debug(`Found NuGet.config at ${nuGetConfigPath}`);
const nuGetConfig = await readFileAsXmlDocument(nuGetConfigPath);
if (!nuGetConfig) {
return undefined;
}
const packageSources = nuGetConfig.childNamed('packageSources');
if (!packageSources) {
return undefined;
}
const packageSourceMapping = nuGetConfig.childNamed('packageSourceMapping');
const registries = getDefaultRegistries();
// Map optional source mapped package patterns to default registries
for (const registry of registries) {
const sourceMappedPackagePatterns = packageSourceMapping
?.childWithAttribute('key', registry.name)
?.childrenNamed('package')
.map((packagePattern) => packagePattern.attr['pattern']);
registry.sourceMappedPackagePatterns = sourceMappedPackagePatterns;
}
for (const child of packageSources.children) {
if (child.type === 'element') {
if (child.name === 'clear') {
logger.debug(`clearing registry URLs`);
registries.length = 0;
} else if (child.name === 'add') {
const isHttpUrl = regEx(/^https?:\/\//i).test(child.attr.value);
if (isHttpUrl) {
let registryUrl = child.attr.value;
if (child.attr.protocolVersion) {
registryUrl += `#protocolVersion=${child.attr.protocolVersion}`;
}
const sourceMappedPackagePatterns = packageSourceMapping
?.childWithAttribute('key', child.attr.key)
?.childrenNamed('package')
.map((packagePattern) => packagePattern.attr['pattern']);
logger.debug(
{
name: child.attr.key,
registryUrl,
sourceMappedPackagePatterns,
},
`Adding registry URL ${registryUrl}`,
);
registries.push({
name: child.attr.key,
url: registryUrl,
sourceMappedPackagePatterns,
});
} else {
logger.debug(
{ registryUrl: child.attr.value },
'ignoring local registry URL',
);
}
}
// child.name === 'remove' not supported
}
}
return registries;
}
export function findVersion(parsedXml: XmlDocument): XmlElement | null {
for (const tag of ['Version', 'VersionPrefix']) {
for (const l1Elem of parsedXml.childrenNamed('PropertyGroup')) {
for (const l2Elem of l1Elem.childrenNamed(tag)) {
return l2Elem;
}
}
}
return null;
}
export function applyRegistries(
dep: NugetPackageDependency,
registries: Registry[] | undefined,
): NugetPackageDependency {
if (registries) {
if (!registries.some((reg) => reg.sourceMappedPackagePatterns)) {
dep.registryUrls = registries.map((reg) => reg.url);
return dep;
}
const regs = registries.filter((r) => r.sourceMappedPackagePatterns);
const map = new Map<string, Registry[]>(
regs.flatMap((r) => r.sourceMappedPackagePatterns!.map((p) => [p, []])),
);
const depName = dep.depName;
for (const reg of regs) {
for (const pattern of reg.sourceMappedPackagePatterns!) {
map.get(pattern)!.push(reg);
}
}
const urls: string[] = [];
for (const [pattern, regs] of [...map].sort(sortPatterns)) {
if (minimatch(pattern, { nocase: true }).match(depName)) {
urls.push(...regs.map((r) => r.url));
break;
}
}
if (urls.length) {
dep.registryUrls = urls;
}
}
return dep;
}
/*
* Sorts patterns by specificity:
* 1. Exact match patterns
* 2. Wildcard match patterns
*/
function sortPatterns(
a: [string, Registry[]],
b: [string, Registry[]],
): number {
if (a[0].endsWith('*') && !b[0].endsWith('*')) {
return 1;
}
if (!a[0].endsWith('*') && b[0].endsWith('*')) {
return -1;
}
return a[0].localeCompare(b[0]) * -1;
}