renovate/lib/modules/manager/homebrew/extract.ts

190 lines
5.4 KiB
TypeScript

import { logger } from '../../../logger';
import type { SkipReason } from '../../../types';
import { regEx } from '../../../util/regex';
import { GithubTagsDatasource } from '../../datasource/github-tags';
import type { PackageDependency, PackageFileContent } from '../types';
import type { UrlPathParsedResult } from './types';
import { isSpace, removeComments, skip } from './util';
function parseSha256(idx: number, content: string): string | null {
let i = idx;
i += 'sha256'.length;
i = skip(i, content, (c) => isSpace(c));
if (content[i] !== '"' && content[i] !== "'") {
return null;
}
i += 1;
const j = skip(i, content, (c) => c !== '"' && c !== "'");
const sha256 = content.slice(i, j);
return sha256;
}
function extractSha256(content: string): string | null {
const sha256RegExp = regEx(/(^|\s)sha256(\s)/);
let i = content.search(sha256RegExp);
if (isSpace(content[i])) {
i += 1;
}
return parseSha256(i, content);
}
function parseUrl(idx: number, content: string): string | null {
let i = idx;
i += 'url'.length;
i = skip(i, content, (c) => isSpace(c));
const chr = content[i];
if (chr !== '"' && chr !== "'") {
return null;
}
i += 1;
const j = skip(i, content, (c) => c !== '"' && c !== "'" && !isSpace(c));
const url = content.slice(i, j);
return url;
}
function extractUrl(content: string): string | null {
const urlRegExp = regEx(/(^|\s)url(\s)/);
let i = content.search(urlRegExp);
// content.search() returns -1 if not found
if (i === -1) {
return null;
}
/* istanbul ignore else */
if (isSpace(content[i])) {
i += 1;
}
return parseUrl(i, content);
}
export function parseUrlPath(
urlStr: string | null | undefined,
): UrlPathParsedResult | null {
if (!urlStr) {
return null;
}
try {
const url = new URL(urlStr);
if (url.hostname !== 'github.com') {
return null;
}
let s = url.pathname.split('/');
s = s.filter((val) => val);
const ownerName = s[0];
const repoName = s[1];
let currentValue: string | undefined;
if (s[2] === 'archive') {
// old archive url in form: [...]/archive/<tag>.tar.gz
currentValue = s[3];
if (currentValue === 'refs') {
// new archive url in form: [...]/archive/refs/tags/<tag>.tar.gz
currentValue = s[5];
}
const targz = currentValue.slice(
currentValue.length - 7,
currentValue.length,
);
if (targz === '.tar.gz') {
currentValue = currentValue.substring(0, currentValue.length - 7);
}
} else if (s[2] === 'releases' && s[3] === 'download') {
currentValue = s[4];
}
if (!currentValue) {
return null;
}
return { currentValue, ownerName, repoName };
} catch {
return null;
}
}
/* This function parses the "class className < Formula" header
and returns the className and index of the character just after the header */
function parseClassHeader(idx: number, content: string): string | null {
let i = idx;
i += 'class'.length;
i = skip(i, content, (c) => isSpace(c));
// Skip all non space and non '<' characters
let j = skip(i, content, (c) => !isSpace(c) && c !== '<');
const className = content.slice(i, j);
i = j;
// Skip spaces
i = skip(i, content, (c) => isSpace(c));
if (content[i] === '<') {
i += 1;
} else {
return null;
} // Skip spaces
i = skip(i, content, (c) => isSpace(c));
// Skip non-spaces
j = skip(i, content, (c) => !isSpace(c));
if (content.slice(i, j) !== 'Formula') {
return null;
}
return className;
}
function extractClassName(content: string): string | null {
const classRegExp = regEx(/(^|\s)class\s/);
let i = content.search(classRegExp);
if (isSpace(content[i])) {
i += 1;
}
return parseClassHeader(i, content);
}
// TODO: Maybe check if quotes/double-quotes are balanced (#9591)
export function extractPackageFile(content: string): PackageFileContent | null {
logger.trace('extractPackageFile()');
/*
1. match "class className < Formula"
2. extract className
3. extract url field (get depName from url)
4. extract sha256 field
*/
const cleanContent = removeComments(content);
const className = extractClassName(cleanContent);
if (!className) {
logger.debug('Invalid class definition');
return null;
}
const url = extractUrl(cleanContent);
if (!url) {
logger.debug('Invalid URL field');
}
const urlPathResult = parseUrlPath(url);
let skipReason: SkipReason | undefined;
let currentValue: string | null = null;
let ownerName: string | null = null;
let repoName: string | null = null;
if (urlPathResult) {
currentValue = urlPathResult.currentValue;
ownerName = urlPathResult.ownerName;
repoName = urlPathResult.repoName;
} else {
logger.debug('Error: Unsupported URL field');
skipReason = 'unsupported-url';
}
const sha256 = extractSha256(cleanContent);
if (!sha256 || sha256.length !== 64) {
logger.debug('Error: Invalid sha256 field');
skipReason = 'invalid-sha256';
}
const dep: PackageDependency = {
// TODO: types (#22198)
depName: `${ownerName}/${repoName}`,
managerData: { ownerName, repoName, sha256, url },
currentValue,
datasource: GithubTagsDatasource.id,
};
if (skipReason) {
dep.skipReason = skipReason;
if (skipReason === 'unsupported-url') {
dep.depName = className;
dep.datasource = undefined;
}
}
const deps = [dep];
return { deps };
}