import { HOST_DISABLED } from '../../constants/error-messages' ; import { logger } from '../../logger' ; import { ExternalHostError } from '../../types/errors/external-host-error' ; import * as packageCache from '../../util/cache/package' ; import { Http } from '../../util/http' ; import { getQueryString , joinUrlParts } from '../../util/url' ; import type { GetReleasesConfig , ReleaseResult } from '../types' ; import type { RepologyPackage , RepologyPackageType } from './types' ; export const id = 'repology' ; export const customRegistrySupport = true ; export const defaultRegistryUrls = [ 'https://repology.org/' ] ; export const registryStrategy = 'hunt' ; const http = new Http ( id ) ; const cacheNamespace = `datasource-${ id }-list` ; const cacheMinutes = 60 ; const packageTypes = [ 'binname' , 'srcname' ] ; async function queryPackages ( url ) { try { const res = await http . getJson < RepologyPackage [ ] > ( url ) ; return res . body ; } catch ( err ) { if ( err . statusCode === 404 ) { return [ ] ; } throw err ; } } async function queryPackagesViaResolver ( registryUrl , repoName , packageName , packageType ) { const query = getQueryString ( { repo : repoName , name_type : packageType , target_page : 'api_v1_project' , noautoresolve : 'on' , name : packageName , } ) ; const packages = await queryPackages ( joinUrlParts ( registryUrl , `tools/project-by?${ query }` ) ) ; return packages ; } async function queryPackagesViaAPI ( registryUrl , packageName ) { const packages = await queryPackages ( joinUrlParts ( registryUrl , `api/v1/project` , packageName ) ) ; return packages ; } function findPackageInResponse ( response , repoName , pkgName , types ) { const repoPackages = response . filter ( ( pkg ) => pkg . repo === repoName ) ; if ( repoPackages . length === 0 ) { return null ; } if ( repoPackages . length === 1 ) { return repoPackages ; } const packagesWithType = repoPackages . filter ( ( pkg ) => { for ( const pkgType of types ) { if ( pkg [ pkgType ] && pkg [ pkgType ] === pkgName ) { return true ; } } return false ; } ) ; return packagesWithType . length > 0 ? packagesWithType : null ; } async function queryPackage ( registryUrl , repoName , pkgName ) { let response ; let pkg ; try { for ( const pkgType of packageTypes ) { response = await queryPackagesViaResolver ( registryUrl , repoName , pkgName , pkgType ) ; if ( response ) { pkg = findPackageInResponse ( response , repoName , pkgName , [ pkgType ] ) ; if ( pkg ) { return pkg ; } } } } catch ( err ) { if ( err . statusCode === 403 ) { logger . debug ( { repoName , pkgName } , 'Repology does not support tools/project-by lookups for repository. Will try direct API access now' ) ; response = await queryPackagesViaAPI ( registryUrl , pkgName ) ; pkg = findPackageInResponse ( response , repoName , pkgName , packageTypes ) ; if ( pkg ) { return pkg ; } } else if ( err . statusCode === 300 ) { logger . warn ( { repoName , pkgName } , 'Ambiguous redirection from package name to project name in Repology. Skipping this package' ) ; return null ; } throw err ; } logger . debug ( { repoName , pkgName } , 'Repository or package not found on Repology' ) ; return null ; } async function getCachedPackage ( registryUrl , repoName , pkgName ) { const cacheKey = joinUrlParts ( registryUrl , repoName , pkgName ) ; const cachedResult = await packageCache . get < RepologyPackage [ ] > ( cacheNamespace , cacheKey ) ; if ( cachedResult ) { return cachedResult ; } const pkg = await queryPackage ( registryUrl , repoName , pkgName ) ; if ( pkg && pkg . length > 0 ) { await packageCache . set ( cacheNamespace , cacheKey , pkg , cacheMinutes ) ; return pkg ; } return null ; } export async function getReleases ( { lookupName , registryUrl , } ) { const [ repoName , pkgName ] = lookupName . split ( '/' , 2 ) ; if ( ! repoName || ! pkgName ) { throw new ExternalHostError ( new Error ( 'Repology lookup name must contain repository and package separated by slash (<repo>/<pkg>)' ) ) ; } logger . trace ( `repology.getReleases(${ repoName }, ${ pkgName })` ) ; try { const pkg = await getCachedPackage ( registryUrl , repoName , pkgName ) ; if ( ! pkg ) { return null ; } const releases = pkg . map ( ( item ) => ( { version : item . origversion ?? item . version , } ) ) ; return { releases } ; } catch ( err ) { if ( err . message === HOST_DISABLED ) { logger . trace ( { lookupName , err } , 'Host disabled' ) ; } else { logger . warn ( { lookupName , err } , 'Repology lookup failed with unexpected error' ) ; } throw new ExternalHostError ( err ) ; } }