From a019ba8ed26b16ea46d13dbc3ce57dcf9c8fc7b3 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 6 Jul 2024 22:44:10 +0200 Subject: [PATCH 01/62] feat(datasource): add debian datasource --- lib/modules/datasource/api.ts | 2 + .../datasource/deb/__fixtures__/Packages | 56 +++ .../datasource/deb/__fixtures__/Packages.gz | Bin 0 -> 121725 bytes .../datasource/deb/__fixtures__/Packages2.gz | Bin 0 -> 742 bytes lib/modules/datasource/deb/index.spec.ts | 235 ++++++++++ lib/modules/datasource/deb/index.ts | 433 ++++++++++++++++++ lib/modules/datasource/deb/readme.md | 69 +++ lib/modules/datasource/deb/types.ts | 11 + lib/modules/datasource/repology/readme.md | 2 +- lib/util/cache/package/types.ts | 1 + lib/util/fs/index.ts | 15 + 11 files changed, 823 insertions(+), 1 deletion(-) create mode 100644 lib/modules/datasource/deb/__fixtures__/Packages create mode 100644 lib/modules/datasource/deb/__fixtures__/Packages.gz create mode 100644 lib/modules/datasource/deb/__fixtures__/Packages2.gz create mode 100644 lib/modules/datasource/deb/index.spec.ts create mode 100644 lib/modules/datasource/deb/index.ts create mode 100644 lib/modules/datasource/deb/readme.md create mode 100644 lib/modules/datasource/deb/types.ts diff --git a/lib/modules/datasource/api.ts b/lib/modules/datasource/api.ts index eff7da102edd81..e269a512b8ca0e 100644 --- a/lib/modules/datasource/api.ts +++ b/lib/modules/datasource/api.ts @@ -14,6 +14,7 @@ import { CrateDatasource } from './crate'; import { CustomDatasource } from './custom'; import { DartDatasource } from './dart'; import { DartVersionDatasource } from './dart-version'; +import { DebDatasource } from './deb'; import { DenoDatasource } from './deno'; import { DockerDatasource } from './docker'; import { DotnetVersionDatasource } from './dotnet-version'; @@ -82,6 +83,7 @@ api.set(CrateDatasource.id, new CrateDatasource()); api.set(CustomDatasource.id, new CustomDatasource()); api.set(DartDatasource.id, new DartDatasource()); api.set(DartVersionDatasource.id, new DartVersionDatasource()); +api.set(DebDatasource.id, new DebDatasource()); api.set(DenoDatasource.id, new DenoDatasource()); api.set(DockerDatasource.id, new DockerDatasource()); api.set(DotnetVersionDatasource.id, new DotnetVersionDatasource()); diff --git a/lib/modules/datasource/deb/__fixtures__/Packages b/lib/modules/datasource/deb/__fixtures__/Packages new file mode 100644 index 00000000000000..67716b40d9635c --- /dev/null +++ b/lib/modules/datasource/deb/__fixtures__/Packages @@ -0,0 +1,56 @@ +Package: album +Version: 4.15-1 +Installed-Size: 288 +Maintainer: Salvo 'LtWorf' Tomaselli +Architecture: all +Depends: perl:any, imagemagick +Recommends: album-data +Suggests: httpd, jhead, libav-tools +Description: HTML photo album generator with theme support +Homepage: http://marginalhacks.com/Hacks/album +Description-md5: 3eaaefa453087570fb45ac51eeccbe7c +Tag: implemented-in::perl, interface::commandline, interface::web, + role::program, scope::application, use::browsing, use::organizing, + web::application, works-with-format::html, works-with::image, + works-with::image:raster, works-with::text +Section: non-free/web +Priority: optional +Filename: pool/non-free/a/album/album_4.15-1_all.deb +Size: 89962 +MD5sum: 7ed9561371bc198dab9f88741b3386ae +SHA256: 336d96db2998e9a80d00fef36ad38756463cfbd516d1f89b97ecdff22f2b6ec1 + +Package: album-data +Version: 4.05-7.2 +Installed-Size: 7936 +Maintainer: Salvo 'LtWorf' Tomaselli +Architecture: all +Depends: album +Description: themes, plugins and translations for album +Homepage: http://marginalhacks.com/Hacks/album +Description-md5: 34adea76df6b2c02712e3838461edbb2 +Tag: role::app-data +Section: non-free/web +Priority: optional +Filename: pool/non-free/a/album-data/album-data_4.05-7.2_all.deb +Size: 5469888 +MD5sum: bfecda545171260a755ab5b4acd97aa6 +SHA256: 8e16c340d46e53752d5916c7f8aca665aa81a78614bcae0ad7b9decf555c114e + +Package: alien-arena-data +Version: 7.71.3+ds-1 +Installed-Size: 1918877 +Maintainer: Debian Games Team +Architecture: all +Depends: fonts-freefont-ttf, fonts-aenigma +Enhances: alien-arena (>= 7.71.3+ds), alien-arena-server (>= 7.71.3+ds) +Description: Game data files for Alien Arena +Homepage: https://martianbackup.com +Description-md5: f930829d2a1207940bee317dc2015735 +Tag: game::fps, role::app-data, use::gameplaying +Section: non-free/games +Priority: optional +Filename: pool/non-free/a/alien-arena-data/alien-arena-data_7.71.3+ds-1_all.deb +Size: 766655844 +MD5sum: 447e0d3a42973dbf3d87df28359501d2 +SHA256: a3fdaf2b0b9e969149642300f0781f7cfdd3ed33f49be38afe58e89531a21b70 \ No newline at end of file diff --git a/lib/modules/datasource/deb/__fixtures__/Packages.gz b/lib/modules/datasource/deb/__fixtures__/Packages.gz new file mode 100644 index 0000000000000000000000000000000000000000..3e3ed5b66f70278718ec04bfbbdf9394d72114f6 GIT binary patch literal 121725 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 literal 0 HcmV?d00001 diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts new file mode 100644 index 00000000000000..287ddecfc2e234 --- /dev/null +++ b/lib/modules/datasource/deb/index.spec.ts @@ -0,0 +1,235 @@ +import { createHash } from 'crypto'; +import { copyFile, stat } from 'fs-extra'; +import { DirectoryResult, dir } from 'tmp-promise'; +import upath from 'upath'; +import { getPkgReleases } from '..'; +import { Fixtures } from '../../../../test/fixtures'; +import * as httpMock from '../../../../test/http-mock'; +import { fs } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import type { GetPkgReleasesConfig } from '../types'; +import { DebDatasource } from '.'; + +const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); +const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); +const fixturePackagesPath = Fixtures.getPath(`Packages`); + +describe('modules/datasource/deb/index', () => { + let debDatasource: DebDatasource; + let cacheDir: DirectoryResult; + let cfg: GetPkgReleasesConfig; + let extractionFolder: string; + let extractedPackageFile: string; + + beforeEach(async () => { + jest.resetAllMocks(); + debDatasource = new DebDatasource(); + cacheDir = await dir({ unsafeCleanup: true }); + GlobalConfig.set({ cacheDir: cacheDir.path }); + + extractionFolder = await fs.ensureCacheDir(DebDatasource.cacheSubDir); + extractedPackageFile = upath.join( + extractionFolder, + `${createHash('sha256').update('http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64').digest('hex')}.txt`, + ); + + cfg = { + datasource: 'deb', + packageName: 'album', + registryUrls: [ + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + ], + }; + }); + + describe('getReleases', () => { + it('returns a valid version for the package `album` and does not require redownload', async () => { + await copyFile(fixturePackagesPath, extractedPackageFile); + const stats = await stat(extractedPackageFile); + const ts = stats.ctime; + + httpMock + .scope('http://ftp.debian.org') + .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .reply(304); + + const res = await getPkgReleases(cfg); + expect(res).toBeObject(); + expect(res!.releases).toHaveLength(1); + + expect(httpMock.getTrace()).toHaveLength(1); + const modifiedTs = httpMock.getTrace()[0].headers['if-modified-since']; + expect(modifiedTs).toBeDefined(); + expect(modifiedTs).toEqual(ts.toUTCString()); + }); + + describe('parsing of registry url', () => { + it('returns null when registry url misses components', async () => { + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?suite=stable&binaryArch=amd64', + ]; + const res = await getPkgReleases(cfg); + expect(res).toBeNull(); + }); + + it('returns null when registry url misses binaryArch', async () => { + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?suite=stable&components=non-free', + ]; + const res = await getPkgReleases(cfg); + expect(res).toBeNull(); + }); + + it('returns null when registry url misses suite or release', async () => { + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?components=non-free&binaryArch=amd64', + ]; + const res = await getPkgReleases(cfg); + expect(res).toBeNull(); + }); + }); + + describe('without local version', () => { + beforeEach(() => { + httpMock + .scope('http://ftp.debian.org') + .get('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .replyWithFile(200, fixturePackagesArchivePath); + }); + + it('returns a valid version for the package `album`', async () => { + const res = await getPkgReleases(cfg); + expect(res).toBeObject(); + expect(res!.releases).toHaveLength(1); + }); + + it('returns a valid version for the package `album` if release is used in the registryUrl', async () => { + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?release=stable&components=non-free&binaryArch=amd64', + ]; + const res = await getPkgReleases(cfg); + expect(res).toBeObject(); + expect(res!.releases).toHaveLength(1); + }); + + it('returns null for an unknown package', async () => { + cfg.packageName = 'you-will-never-find-me'; + const res = await getPkgReleases(cfg); + expect(res).toBeNull(); + }); + + describe('with two components', () => { + beforeEach(() => { + httpMock + .scope('http://ftp.debian.org') + .get( + '/debian/dists/stable/non-free-second/binary-amd64/Packages.gz', + ) + .replyWithFile(200, fixturePackagesArchivePath2); + + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + ]; + }); + + it('returns two releases for `album` which is the same across the components', async () => { + const res = await getPkgReleases(cfg); + expect(res).toBeObject(); + expect(res!.releases).toHaveLength(2); + }); + + it('returns two releases for `album` which has different metadata across the components', async () => { + cfg.packageName = 'album'; + const res = await getPkgReleases(cfg); + expect(res?.releases).toHaveLength(2); + }); + }); + }); + + describe('without server response', () => { + beforeEach(() => { + httpMock + .scope('http://ftp.debian.org') + .get('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .reply(404); + }); + + it('returns null for the package', async () => { + cfg.packageName = 'you-will-never-find-me'; + const res = await getPkgReleases(cfg); + expect(res).toBeNull(); + }); + }); + + it('supports specifying a custom binary arch', async () => { + httpMock + .scope('http://ftp.debian.org') + .get('/debian/dists/stable/non-free/binary-riscv/Packages.gz') + .replyWithFile(200, fixturePackagesArchivePath); + + cfg.registryUrls = [ + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv', + ]; + + const res = await getPkgReleases(cfg); + expect(res).toBeObject(); + expect(res!.releases).toHaveLength(1); + }); + }); + + describe('constructComponentUrls', () => { + it('constructs URLs correctly from registry URL with suite', () => { + const registryUrl = + 'https://ftp.debian.org/debian?suite=stable&components=main,contrib&binaryArch=amd64'; + const expectedUrls = [ + 'https://ftp.debian.org/debian/dists/stable/main/binary-amd64', + 'https://ftp.debian.org/debian/dists/stable/contrib/binary-amd64', + ]; + const componentUrls = debDatasource.constructComponentUrls(registryUrl); + expect(componentUrls).toEqual(expectedUrls); + }); + + it('constructs URLs correctly from registry URL with release', () => { + const registryUrl = + 'https://ftp.debian.org/debian?release=bullseye&components=main,contrib&binaryArch=amd64'; + const expectedUrls = [ + 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', + 'https://ftp.debian.org/debian/dists/bullseye/contrib/binary-amd64', + ]; + const componentUrls = debDatasource.constructComponentUrls(registryUrl); + expect(componentUrls).toEqual(expectedUrls); + }); + + it('throws an error if required parameters are missing', () => { + const registryUrl = + 'https://ftp.debian.org/debian?components=main,contrib'; + expect(() => debDatasource.constructComponentUrls(registryUrl)).toThrow( + 'Missing required query parameter', + ); + }); + }); + + describe('parseExtractedPackage', () => { + it('should parse the last package', async () => { + const release = await debDatasource.parseExtractedPackage( + fixturePackagesPath, + 'alien-arena-data', + new Date(), + ); + expect(release?.releases?.[0]?.version).toBe('7.71.3+ds-1'); + }); + }); + + describe('extract', () => { + it('should throw error for unsupported compression', async () => { + expect( + async () => + await DebDatasource.extract( + fixturePackagesArchivePath, + 'xz', + extractedPackageFile, + ), + ).toThrow(); + }); + }); +}); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts new file mode 100644 index 00000000000000..ee062b9f239604 --- /dev/null +++ b/lib/modules/datasource/deb/index.ts @@ -0,0 +1,433 @@ +import { createHash } from 'crypto'; +import readline from 'readline'; +import { createUnzip } from 'zlib'; +import upath from 'upath'; +import { logger } from '../../../logger'; +import { cache } from '../../../util/cache/package/decorator'; +import * as fs from '../../../util/fs'; +import type { HttpOptions } from '../../../util/http/types'; +import { joinUrlParts } from '../../../util/url'; +import { Datasource } from '../datasource'; +import type { GetReleasesConfig, ReleaseResult } from '../types'; +import type { PackageDescription } from './types'; + +export class DebDatasource extends Datasource { + static readonly id = 'deb'; + + constructor() { + super(DebDatasource.id); + } + + /** + * This is just an internal list of compressions that are supported and tried to be downloaded from the remote + */ + static readonly compressions = ['gz']; + + /** + * This specifies the directory where the extracted and downloaded packages files are stored relative to cacheDir. + * The folder will be created automatically if it doesn't exist. + */ + static readonly cacheSubDir: string = 'deb'; + + /** + * Users are able to specify custom Debian repositories as long as they follow + * the Debian package repository format as specified here + * @see{https://wiki.debian.org/DebianRepository/Format} + */ + override readonly customRegistrySupport = true; + + /** + * The original apt source list file format is + * deb uri distribution [component1] [component2] [...] + * @see{https://wiki.debian.org/DebianRepository/Format} + * + * However, for Renovate, we require the registry URLs to be + * valid URLs which is why the parameters are encoded in the URL. + * + * The following query parameters are required: + * - components: comma separated list of components + * - suite: stable, oldstable or other alias for a release, either this or release must be given + * - release: buster, etc. + * - binaryArch: e.g. amd64 resolves to http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/ + */ + override readonly defaultRegistryUrls = [ + 'https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64', + ]; + + override readonly caching = true; + + /** + * Here, we tell Renovate that this data source can respect multiple upstream repositories + */ + override readonly registryStrategy = 'merge'; + + /** + * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to + * have enough tests in your application. + */ + override readonly defaultVersioning = 'loose'; + + static requiredPackageKeys: Array = [ + 'Package', + 'Version', + 'Homepage', + ]; + + /** + * Extracts the specified compressed file to the output file. + * + * @param compressedFile - The path to the compressed file. + * @param compression - The compression method used (currently only 'gz' is supported). + * @param outputFile - The path where the extracted content will be stored. + * @throws Will throw an error if the compression method is unknown. + */ + static async extract( + compressedFile: string, + compression: string, + outputFile: string, + ): Promise { + if (compression === 'gz') { + const source = fs.createReadStream(compressedFile); + const destination = fs.createCacheWriteStream(outputFile); + await fs.pipeline(source, createUnzip(), destination); + } else { + throw new Error('Unsupported compression standard'); + } + } + + /** + * Checks if the file exists and retrieves its creation time. + * + * @param filePath - The path to the file. + * @returns The creation time if the file exists, otherwise undefined. + */ + async getFileCreationTime(filePath: string): Promise { + const stats = await fs.statCacheFile(filePath); + return stats?.ctime; + } + + /** + * Downloads and extracts a package file from a component URL. + * + * @param componentUrl - The URL of the component. + * @returns The path to the extracted file and the last modification timestamp. + * @throws Will throw an error if no valid compression method is found. + */ + async downloadAndExtractPackage( + componentUrl: string, + ): Promise<{ extractedFile: string; lastTimestamp: Date }> { + const packageUrlHash = createHash('sha256') + .update(componentUrl) + .digest('hex'); + const fullCacheDir = await fs.ensureCacheDir(DebDatasource.cacheSubDir); + const extractedFile = upath.join(fullCacheDir, `${packageUrlHash}.txt`); + let lastTimestamp = await this.getFileCreationTime(extractedFile); + + for (const compression of DebDatasource.compressions) { + const compressedFile = upath.join( + fullCacheDir, + `${packageUrlHash}.${compression}`, + ); + + const wasUpdated = await this.downloadPackageFile( + componentUrl, + compression, + compressedFile, + lastTimestamp, + ); + if (wasUpdated || !lastTimestamp) { + try { + await DebDatasource.extract( + compressedFile, + compression, + extractedFile, + ); + lastTimestamp = await this.getFileCreationTime(extractedFile); + } catch (error) { + logger.error( + { + componentUrl, + compression, + error: error.message, + }, + `Failed to extract package file from ${compressedFile}`, + ); + } finally { + await fs.rmCache(compressedFile); + } + } + return { extractedFile, lastTimestamp: lastTimestamp! }; + } + + throw new Error(`No compression standard worked for ${componentUrl}`); + } + + /** + * Downloads a package file if it has been modified since the last download timestamp. + * + * @param basePackageUrl - The base URL of the package. + * @param compression - The compression method used (e.g., 'gz'). + * @param compressedFile - The path where the compressed file will be saved. + * @param lastDownloadTimestamp - The timestamp of the last download. + * @returns True if the file was downloaded, otherwise false. + */ + async downloadPackageFile( + basePackageUrl: string, + compression: string, + compressedFile: string, + lastDownloadTimestamp?: Date, + ): Promise { + const packageUrl = `${basePackageUrl}/Packages.${compression}`; + let needsToDownload = true; + + if (lastDownloadTimestamp) { + needsToDownload = await this.checkIfModified( + packageUrl, + lastDownloadTimestamp, + ); + } + + if (needsToDownload) { + try { + const readStream = this.http.stream(packageUrl); + const writeStream = fs.createCacheWriteStream(compressedFile); + await fs.pipeline(readStream, writeStream); + logger.debug( + { url: packageUrl, targetFile: compressedFile }, + 'Downloading Debian package file', + ); + } catch (error) { + logger.error( + `Failed to download package file from ${packageUrl}: ${error.message}`, + ); + needsToDownload = false; + } + } else { + logger.debug(`No need to download ${packageUrl}, file is up to date.`); + } + + return needsToDownload; + } + + /** + * Checks if a packageUrl content has been modified since the specified timestamp. + * + * @param packageUrl - The URL to check. + * @param lastDownloadTimestamp - The timestamp of the last download. + * @returns True if the content has been modified, otherwise false. + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since + */ + async checkIfModified( + packageUrl: string, + lastDownloadTimestamp: Date, + ): Promise { + const options: HttpOptions = { + headers: { + 'If-Modified-Since': lastDownloadTimestamp.toUTCString(), + }, + }; + + try { + const response = await this.http.head(packageUrl, options); + return response.statusCode !== 304; + } catch (error) { + logger.warn( + `Could not determine if ${packageUrl} is modified since ${lastDownloadTimestamp.toUTCString()}: ${error.message}`, + ); + return true; // Assume it needs to be downloaded if check fails + } + } + + /** + * Parses the extracted package file to find the specified package. + * + * @param extractedFile - The path to the extracted package file. + * @param packageName - The name of the package to find. + * @param lastTimestamp - The timestamp of the last modification. + * @returns The release result if found, otherwise null. + */ + @cache({ + namespace: `datasource-${DebDatasource.id}-package`, + key: (extractedFile: string, packageName: string, lastTimestamp: Date) => + `${extractedFile}:${packageName}:${lastTimestamp.getTime()}`, + ttlMinutes: 24 * 60, + }) + async parseExtractedPackage( + extractedFile: string, + packageName: string, + lastTimestamp: Date, + ): Promise { + // read line by line to avoid high memory consumption as the extracted Packages + // files can be multiple MBs in size + const rl = readline.createInterface({ + input: fs.createReadStream(extractedFile), + terminal: false, + }); + + let currentPackage: PackageDescription = {}; + + for await (const line of rl) { + if (line === '') { + // All information of the package are available, early return possible + if (currentPackage.Package === packageName) { + return this.formatReleaseResult(currentPackage); + } + currentPackage = {}; + } else { + for (const key of DebDatasource.requiredPackageKeys) { + if (line.startsWith(`${key}:`)) { + currentPackage[key] = line.substring(key.length + 1).trim(); + break; + } + } + } + } + + // Check the last package after file reading is complete + if (currentPackage.Package === packageName) { + return this.formatReleaseResult(currentPackage); + } + + return null; + } + + /** + * Formats the package description into a ReleaseResult. + * + * @param packageDesc - The package description object. + * @returns A formatted ReleaseResult. + */ + formatReleaseResult(packageDesc: PackageDescription): ReleaseResult { + return { + releases: [{ version: packageDesc.Version! }], + homepage: packageDesc.Homepage, + }; + } + + /** + * Constructs the component URLs from the given registry URL. + * + * @param registryUrl - The base URL of the registry. + * @returns An array of component URLs. + * @throws Will throw an error if required parameters are missing from the URL. + */ + constructComponentUrls(registryUrl: string): string[] { + const REQUIRED_PARAMS = ['components', 'binaryArch']; + const OPTIONAL_PARAMS = ['release', 'suite']; + + const validateUrlAndParams = (url: URL): void => { + REQUIRED_PARAMS.forEach((param) => { + if (!url.searchParams.has(param)) { + throw new Error(`Missing required query parameter '${param}'`); + } + }); + }; + + const getReleaseParam = (url: URL): string => { + for (const param of OPTIONAL_PARAMS) { + if (url.searchParams.has(param)) { + return url.searchParams.get(param) ?? ''; + } + } + throw new Error( + `Missing one of ${OPTIONAL_PARAMS.join(', ')} query parameter`, + ); + }; + + try { + const url = new URL(registryUrl); + validateUrlAndParams(url); + + const release = getReleaseParam(url); + const binaryArch = url.searchParams.get('binaryArch'); + const components = url.searchParams.get('components')!.split(','); + + // Clean up URL search parameters for constructing new URLs + [...REQUIRED_PARAMS, ...OPTIONAL_PARAMS].forEach((param) => + url.searchParams.delete(param), + ); + + return components.map((component) => { + return joinUrlParts( + url.toString(), + `dists`, + release, + component, + `binary-${binaryArch}`, + ); + }); + } catch (error) { + throw new Error( + `Invalid deb repo URL: ${registryUrl} - see documentation: ${error.message}`, + ); + } + } + + /** + * Fetches the release information for a given package from the registry URL. + * + * @param config - Configuration for fetching releases. + * @returns The release result if the package is found, otherwise null. + */ + async getReleases({ + registryUrl, + packageName, + }: GetReleasesConfig): Promise { + // istanbul ignore if + if (!registryUrl) { + return null; + } + + const componentUrls = this.constructComponentUrls(registryUrl); + let aggregatedRelease: ReleaseResult | null = null; + + for (const componentUrl of componentUrls) { + try { + const { extractedFile, lastTimestamp } = + await this.downloadAndExtractPackage(componentUrl); + const newRelease = await this.parseExtractedPackage( + extractedFile, + packageName, + lastTimestamp, + ); + + if (newRelease) { + if (aggregatedRelease === null) { + aggregatedRelease = newRelease; + } else { + if ( + !this.releaseMetaInformationMatches(aggregatedRelease, newRelease) + ) { + logger.warn( + { packageName }, + 'Package occurred in more than one repository with different meta information. Aggregating releases anyway.', + ); + } + aggregatedRelease.releases.push(...newRelease.releases); + } + } + } catch (error) { + logger.warn( + { componentUrl, error }, + 'Skipping component due to an error', + ); + } + } + + return aggregatedRelease; + } + + /** + * Checks if two release metadata objects match. + * + * @param lhs - The first release result. + * @param rhs - The second release result. + * @returns True if the metadata matches, otherwise false. + */ + releaseMetaInformationMatches( + lhs: ReleaseResult, + rhs: ReleaseResult, + ): boolean { + return lhs.homepage === rhs.homepage; + } +} diff --git a/lib/modules/datasource/deb/readme.md b/lib/modules/datasource/deb/readme.md new file mode 100644 index 00000000000000..a342745b485b5c --- /dev/null +++ b/lib/modules/datasource/deb/readme.md @@ -0,0 +1,69 @@ +The Debian datasource enables Renovate to update packages from Debian repositories. It is ideal for projects that depend on Debian-based systems or distributions. You will need to combine Debian datasource with [regex managers](../../manager/regex/index.md) to update dependencies. + +**Registry URL** +To use a Debian repository with the datasource, you need a properly formatted URL with specific query parameters: + +- `components`: Comma-separated list of repository components (e.g., `main,contrib,non-free`). +- `binaryArch`: Architecture of the binary packages (e.g., `amd64`,`all`). +- Either `suite` or `release`: + - `suite`: A rolling release alias like `stable`. + - `release`: A fixed release name such as `bullseye` or `buster`. + +**Example**: + +``` +https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64 +``` + +This URL points to the `stable` suite of the Debian repository for `amd64` architecture, including `main`, `contrib`, and `non-free` components. + +**Usage Example** + +Say you're using apt packages in a Dockerfile and want to update them. +With the debian datasource you can "pin" each dependency, and get automatic updates. + +First you would set a custom manager in your `renovate.json` file for `Dockerfile`: + +```json +{ + "customManagers": [ + { + "customType": "regex", + "fileMatch": ["^Dockerfile$"], + "matchStrings": [ + "#\\s*renovate:\\s* depName=(?.*?)( versioning=(?loose))?\\sENV .*?_VERSION=\"(?.*)\"\\s" + ], + "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}loose{{/if}}" + } + ], + "packageRules": [ + { + "datasources": ["deb"], + "registryUrls": [ + "https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64" + ] + } + ] +} +``` + +Then you would put comments in your Dockerfile, to tell Renovate where to find the updates: + +```dockerfile +FROM debian:bullseye + +# renovate: depName=gcc versioning=loose +ENV GCC_VERSION="10.2.1-6" + +RUN apt-get update && \ + apt-get install -y \ + gcc="${GCC_VERSION}" && \ + apt-get clean +``` + +When the apt package for `gcc` is updated, Renovate updates the environment variable. + + +!!! tip + We recommend you try `loose` versioning for distribution packages first. + This is because the version number usually doesn't match Renovate's default `semver-coerced` specification. diff --git a/lib/modules/datasource/deb/types.ts b/lib/modules/datasource/deb/types.ts new file mode 100644 index 00000000000000..b2e4082050ab73 --- /dev/null +++ b/lib/modules/datasource/deb/types.ts @@ -0,0 +1,11 @@ +/** + * A package file contains multiple package descriptions which are each separated by an completely empty line. + * A package description contains meta data properties in the form of + * + * PropertyName: value + */ +export interface PackageDescription { + Package?: string; // Package + Version?: string; // Version + Homepage?: string; // Homepage +} diff --git a/lib/modules/datasource/repology/readme.md b/lib/modules/datasource/repology/readme.md index 787585515704bd..0f04a81c919e0f 100644 --- a/lib/modules/datasource/repology/readme.md +++ b/lib/modules/datasource/repology/readme.md @@ -33,7 +33,7 @@ First you would set a custom manager in your `renovate.json` file for `Dockerfil Then you would put comments in your Dockerfile, to tell Renovate where to find the updates: -```docker +```dockerfile FROM alpine:3.12.0@sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # renovate: datasource=repology depName=alpine_3_12/gcc versioning=loose diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index e8a479bd1dce51..dfcaf076950926 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -41,6 +41,7 @@ export type PackageCacheNamespace = | 'datasource-cpan' | 'datasource-crate-metadata' | 'datasource-crate' + | 'datasource-deb-package' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts index 64512d6257a546..3b2493706e2eec 100644 --- a/lib/util/fs/index.ts +++ b/lib/util/fs/index.ts @@ -176,6 +176,10 @@ export function createCacheWriteStream(path: string): fs.WriteStream { return fs.createWriteStream(fullPath); } +export function createReadStream(path: string): fs.ReadStream { + return fs.createReadStream(path); +} + export async function localPathIsFile(pathName: string): Promise { const path = ensureLocalPath(pathName); try { @@ -249,6 +253,17 @@ export async function statLocalFile( } } +export async function statCacheFile( + pathName: string, +): Promise { + const path = ensureCachePath(pathName); + try { + return await fs.stat(path); + } catch (_) { + return null; + } +} + export function listCacheDir( path: string, options: { recursive: boolean } = { recursive: false }, From daa1a80ecb54ad400ae3039d5b26fbbd56e0d48e Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 6 Jul 2024 22:47:36 +0200 Subject: [PATCH 02/62] disable failing test --- lib/modules/datasource/deb/index.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 287ddecfc2e234..e5612c6d4f18e7 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -220,7 +220,7 @@ describe('modules/datasource/deb/index', () => { }); }); - describe('extract', () => { + describe.skip('extract', () => { it('should throw error for unsupported compression', async () => { expect( async () => From d57eaba408a57a9b20ea3d02517146833a68217d Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 6 Jul 2024 23:52:05 +0200 Subject: [PATCH 03/62] fix example --- lib/modules/datasource/deb/readme.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/modules/datasource/deb/readme.md b/lib/modules/datasource/deb/readme.md index a342745b485b5c..784a6e342ba98f 100644 --- a/lib/modules/datasource/deb/readme.md +++ b/lib/modules/datasource/deb/readme.md @@ -26,19 +26,20 @@ First you would set a custom manager in your `renovate.json` file for `Dockerfil ```json { + "$schema": "https://docs.renovatebot.com/renovate-schema.json", "customManagers": [ { "customType": "regex", "fileMatch": ["^Dockerfile$"], "matchStrings": [ - "#\\s*renovate:\\s* depName=(?.*?)( versioning=(?loose))?\\sENV .*?_VERSION=\"(?.*)\"\\s" + "#\\s*renovate:\\s*?depName=(?.*?)?\\sENV .*?_VERSION=\"(?.*)\"\\s" ], - "versioningTemplate": "{{#if versioning}}{{{versioning}}}{{else}}loose{{/if}}" + "datasourceTemplate": "deb" } ], "packageRules": [ { - "datasources": ["deb"], + "matchDatasources": ["deb"], "registryUrls": [ "https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64" ] @@ -52,12 +53,12 @@ Then you would put comments in your Dockerfile, to tell Renovate where to find t ```dockerfile FROM debian:bullseye -# renovate: depName=gcc versioning=loose -ENV GCC_VERSION="10.2.1-6" +# renovate: depName=gcc-11 +ENV GCC_VERSION="11.2.0-19" RUN apt-get update && \ apt-get install -y \ - gcc="${GCC_VERSION}" && \ + gcc-11="${GCC_VERSION}" && \ apt-get clean ``` From 762d1165b868ccb84eae12561a15f801126f8ae7 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sun, 7 Jul 2024 11:17:08 +0200 Subject: [PATCH 04/62] add missing tests --- lib/modules/datasource/deb/index.spec.ts | 65 ++++++++++++++++++++---- lib/modules/datasource/deb/index.ts | 13 +++-- 2 files changed, 66 insertions(+), 12 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index e5612c6d4f18e7..de6ddbd35e47c0 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -220,16 +220,63 @@ describe('modules/datasource/deb/index', () => { }); }); - describe.skip('extract', () => { + describe('extract', () => { it('should throw error for unsupported compression', async () => { - expect( - async () => - await DebDatasource.extract( - fixturePackagesArchivePath, - 'xz', - extractedPackageFile, - ), - ).toThrow(); + await expect( + DebDatasource.extract( + fixturePackagesArchivePath, + 'xz', + extractedPackageFile, + ), + ).rejects.toThrow('Unsupported compression standard'); + }); + }); + + describe('downloadAndExtractPackage', () => { + beforeEach(() => { + httpMock + .scope('http://ftp.debian.org') + .get('/debian/dists/bullseye/main/binary-amd64/Packages.gz') + .replyWithFile(200, fixturePackagesArchivePath2); + }); + + it('should throw error for unsupported compression', async () => { + DebDatasource.extract = jest.fn().mockRejectedValueOnce(new Error()); + await expect( + debDatasource.downloadAndExtractPackage( + 'http://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', + ), + ).rejects.toThrow(`No compression standard worked for `); + }); + }); + + describe('checkIfModified', () => { + it('should return true for different status code', async () => { + httpMock + .scope('http://ftp.debian.org') + .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .reply(200); + + await expect( + debDatasource.checkIfModified( + 'http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/Packages.gz', + new Date(), + ), + ).resolves.toBe(true); + }); + + it('should return true if request failed', async () => { + httpMock + .scope('http://ftp.debian.org') + .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .replyWithError('Unexpected Error'); + + await expect( + debDatasource.checkIfModified( + 'http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/Packages.gz', + new Date(), + ), + ).resolves.toBe(true); }); }); }); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index ee062b9f239604..48825c18ed164e 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -156,7 +156,13 @@ export class DebDatasource extends Datasource { await fs.rmCache(compressedFile); } } - return { extractedFile, lastTimestamp: lastTimestamp! }; + + if (!lastTimestamp) { + //extracting went wrong + break; + } + + return { extractedFile, lastTimestamp }; } throw new Error(`No compression standard worked for ${componentUrl}`); @@ -325,8 +331,9 @@ export class DebDatasource extends Datasource { const getReleaseParam = (url: URL): string => { for (const param of OPTIONAL_PARAMS) { - if (url.searchParams.has(param)) { - return url.searchParams.get(param) ?? ''; + const paramValue = url.searchParams.get(param); + if (paramValue !== null) { + return paramValue; } } throw new Error( From 864988527ffb73aab74b130f8ecea06049ccffcf Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sun, 7 Jul 2024 11:56:31 +0200 Subject: [PATCH 05/62] use function for test urls --- lib/modules/datasource/deb/index.spec.ts | 87 ++++++++++++++++-------- 1 file changed, 58 insertions(+), 29 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index de6ddbd35e47c0..7764f6f2547643 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -10,6 +10,8 @@ import { GlobalConfig } from '../../../config/global'; import type { GetPkgReleasesConfig } from '../types'; import { DebDatasource } from '.'; +const debBaseUrl = 'http://ftp.debian.org'; + const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); const fixturePackagesPath = Fixtures.getPath(`Packages`); @@ -30,14 +32,16 @@ describe('modules/datasource/deb/index', () => { extractionFolder = await fs.ensureCacheDir(DebDatasource.cacheSubDir); extractedPackageFile = upath.join( extractionFolder, - `${createHash('sha256').update('http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64').digest('hex')}.txt`, + `${createHash('sha256') + .update(getComponentUrl(debBaseUrl, 'stable', 'non-free', 'amd64')) + .digest('hex')}.txt`, ); cfg = { datasource: 'deb', packageName: 'album', registryUrls: [ - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'), ], }; }); @@ -49,8 +53,8 @@ describe('modules/datasource/deb/index', () => { const ts = stats.ctime; httpMock - .scope('http://ftp.debian.org') - .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .head(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(304); const res = await getPkgReleases(cfg); @@ -66,7 +70,7 @@ describe('modules/datasource/deb/index', () => { describe('parsing of registry url', () => { it('returns null when registry url misses components', async () => { cfg.registryUrls = [ - 'http://ftp.debian.org/debian?suite=stable&binaryArch=amd64', + `${debBaseUrl}/debian?suite=stable&binaryArch=amd64`, ]; const res = await getPkgReleases(cfg); expect(res).toBeNull(); @@ -74,7 +78,7 @@ describe('modules/datasource/deb/index', () => { it('returns null when registry url misses binaryArch', async () => { cfg.registryUrls = [ - 'http://ftp.debian.org/debian?suite=stable&components=non-free', + `${debBaseUrl}/debian?suite=stable&components=non-free`, ]; const res = await getPkgReleases(cfg); expect(res).toBeNull(); @@ -82,7 +86,7 @@ describe('modules/datasource/deb/index', () => { it('returns null when registry url misses suite or release', async () => { cfg.registryUrls = [ - 'http://ftp.debian.org/debian?components=non-free&binaryArch=amd64', + `${debBaseUrl}/debian?components=non-free&binaryArch=amd64`, ]; const res = await getPkgReleases(cfg); expect(res).toBeNull(); @@ -92,8 +96,8 @@ describe('modules/datasource/deb/index', () => { describe('without local version', () => { beforeEach(() => { httpMock - .scope('http://ftp.debian.org') - .get('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath); }); @@ -105,7 +109,7 @@ describe('modules/datasource/deb/index', () => { it('returns a valid version for the package `album` if release is used in the registryUrl', async () => { cfg.registryUrls = [ - 'http://ftp.debian.org/debian?release=stable&components=non-free&binaryArch=amd64', + getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'), ]; const res = await getPkgReleases(cfg); expect(res).toBeObject(); @@ -121,14 +125,17 @@ describe('modules/datasource/deb/index', () => { describe('with two components', () => { beforeEach(() => { httpMock - .scope('http://ftp.debian.org') - .get( - '/debian/dists/stable/non-free-second/binary-amd64/Packages.gz', - ) + .scope(debBaseUrl) + .get(getPackageUrl('', 'stable', 'non-free-second', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath2); cfg.registryUrls = [ - 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + getRegistryUrl( + debBaseUrl, + 'stable', + ['non-free', 'non-free-second'], + 'amd64', + ), ]; }); @@ -149,8 +156,8 @@ describe('modules/datasource/deb/index', () => { describe('without server response', () => { beforeEach(() => { httpMock - .scope('http://ftp.debian.org') - .get('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(404); }); @@ -163,12 +170,12 @@ describe('modules/datasource/deb/index', () => { it('supports specifying a custom binary arch', async () => { httpMock - .scope('http://ftp.debian.org') - .get('/debian/dists/stable/non-free/binary-riscv/Packages.gz') + .scope(debBaseUrl) + .get(getPackageUrl('', 'stable', 'non-free', 'riscv')) .replyWithFile(200, fixturePackagesArchivePath); cfg.registryUrls = [ - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv', + getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'riscv'), ]; const res = await getPkgReleases(cfg); @@ -235,8 +242,8 @@ describe('modules/datasource/deb/index', () => { describe('downloadAndExtractPackage', () => { beforeEach(() => { httpMock - .scope('http://ftp.debian.org') - .get('/debian/dists/bullseye/main/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .get(getPackageUrl('', 'bullseye', 'main', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath2); }); @@ -244,7 +251,7 @@ describe('modules/datasource/deb/index', () => { DebDatasource.extract = jest.fn().mockRejectedValueOnce(new Error()); await expect( debDatasource.downloadAndExtractPackage( - 'http://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', + getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), ), ).rejects.toThrow(`No compression standard worked for `); }); @@ -253,13 +260,13 @@ describe('modules/datasource/deb/index', () => { describe('checkIfModified', () => { it('should return true for different status code', async () => { httpMock - .scope('http://ftp.debian.org') - .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .head(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(200); await expect( debDatasource.checkIfModified( - 'http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/Packages.gz', + getPackageUrl(debBaseUrl, 'stable', 'non-free', 'amd64'), new Date(), ), ).resolves.toBe(true); @@ -267,16 +274,38 @@ describe('modules/datasource/deb/index', () => { it('should return true if request failed', async () => { httpMock - .scope('http://ftp.debian.org') - .head('/debian/dists/stable/non-free/binary-amd64/Packages.gz') + .scope(debBaseUrl) + .head(getPackageUrl('', 'stable', 'non-free', 'amd64')) .replyWithError('Unexpected Error'); await expect( debDatasource.checkIfModified( - 'http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/Packages.gz', + getPackageUrl(debBaseUrl, 'stable', 'non-free', 'amd64'), new Date(), ), ).resolves.toBe(true); }); }); }); + +const getComponentUrl = ( + baseUrl: string, + release: string, + component: string, + arch: string, +) => `${baseUrl}/debian/dists/${release}/${component}/binary-${arch}`; + +const getPackageUrl = ( + baseUrl: string, + release: string, + component: string, + arch: string, +) => `${getComponentUrl(baseUrl, release, component, arch)}/Packages.gz`; + +const getRegistryUrl = ( + baseUrl: string, + release: string, + components: string[], + arch: string, +) => + `${baseUrl}/debian?suite=${release}&components=${components.join(',')}&binaryArch=${arch}`; From 0f511de8a1055714cf83015d34b471a4b6c14d45 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sun, 7 Jul 2024 12:04:47 +0200 Subject: [PATCH 06/62] remove unnecessary trace --- lib/modules/datasource/deb/index.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 7764f6f2547643..eb78d38bf518e2 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -61,7 +61,6 @@ describe('modules/datasource/deb/index', () => { expect(res).toBeObject(); expect(res!.releases).toHaveLength(1); - expect(httpMock.getTrace()).toHaveLength(1); const modifiedTs = httpMock.getTrace()[0].headers['if-modified-since']; expect(modifiedTs).toBeDefined(); expect(modifiedTs).toEqual(ts.toUTCString()); From 31772b6710bdf9aa5f7dd78bab1035c14fce3786 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:55:02 +0200 Subject: [PATCH 07/62] remove loose versioning --- lib/modules/datasource/deb/index.ts | 2 +- lib/modules/datasource/deb/readme.md | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 48825c18ed164e..9d1b6797bd570d 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -65,7 +65,7 @@ export class DebDatasource extends Datasource { * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to * have enough tests in your application. */ - override readonly defaultVersioning = 'loose'; + override readonly defaultVersioning = 'deb'; static requiredPackageKeys: Array = [ 'Package', diff --git a/lib/modules/datasource/deb/readme.md b/lib/modules/datasource/deb/readme.md index 784a6e342ba98f..78b118f2fe8a8c 100644 --- a/lib/modules/datasource/deb/readme.md +++ b/lib/modules/datasource/deb/readme.md @@ -63,8 +63,3 @@ RUN apt-get update && \ ``` When the apt package for `gcc` is updated, Renovate updates the environment variable. - - -!!! tip - We recommend you try `loose` versioning for distribution packages first. - This is because the version number usually doesn't match Renovate's default `semver-coerced` specification. From 7e1ae2d729900ede2b10548d08395b7c16e07178 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:55:18 +0200 Subject: [PATCH 08/62] add ts docs --- lib/modules/datasource/deb/index.spec.ts | 27 ++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index eb78d38bf518e2..e097a6a8910c56 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -287,6 +287,15 @@ describe('modules/datasource/deb/index', () => { }); }); +/** + * Constructs a URL for accessing the component directory for a specific release and architecture. + * + * @param baseUrl - The base URL of the repository. + * @param release - The release name or codename (e.g., 'buster', 'bullseye'). + * @param component - The component name (e.g., 'main', 'contrib', 'non-free'). + * @param arch - The architecture name (e.g., 'amd64', 'i386'). + * @returns The complete URL to the component directory. + */ const getComponentUrl = ( baseUrl: string, release: string, @@ -294,6 +303,15 @@ const getComponentUrl = ( arch: string, ) => `${baseUrl}/debian/dists/${release}/${component}/binary-${arch}`; +/** + * Constructs a URL for accessing the Packages.gz file for a specific component, release, and architecture. + * + * @param baseUrl - The base URL of the repository. + * @param release - The release name or codename (e.g., 'buster', 'bullseye'). + * @param component - The component name (e.g., 'main', 'contrib', 'non-free'). + * @param arch - The architecture name (e.g., 'amd64', 'i386'). + * @returns The complete URL to the Packages.gz file. + */ const getPackageUrl = ( baseUrl: string, release: string, @@ -301,6 +319,15 @@ const getPackageUrl = ( arch: string, ) => `${getComponentUrl(baseUrl, release, component, arch)}/Packages.gz`; +/** + * Constructs a URL used generating the component url with specific release, components, and architecture. + * + * @param baseUrl - The base URL of the repository. + * @param release - The release name or codename (e.g., 'buster', 'bullseye'). + * @param components - An array of component names (e.g., ['main', 'contrib', 'non-free']). + * @param arch - The architecture name (e.g., 'amd64', 'i386'). + * @returns The complete URL to the package registry. + */ const getRegistryUrl = ( baseUrl: string, release: string, From db15fa7ac6a0c5f851c72eaada8dc2a23ff883bf Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:57:16 +0200 Subject: [PATCH 09/62] Apply suggestions from code review Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.ts | 10 +++------- lib/util/fs/index.ts | 5 +++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 48825c18ed164e..51c03cfa297a53 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -56,16 +56,12 @@ export class DebDatasource extends Datasource { override readonly caching = true; - /** - * Here, we tell Renovate that this data source can respect multiple upstream repositories - */ - override readonly registryStrategy = 'merge'; /** * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to * have enough tests in your application. */ - override readonly defaultVersioning = 'loose'; + override readonly defaultVersioning = 'deb'; static requiredPackageKeys: Array = [ 'Package', @@ -87,11 +83,11 @@ export class DebDatasource extends Datasource { outputFile: string, ): Promise { if (compression === 'gz') { - const source = fs.createReadStream(compressedFile); + const source = fs.createCacheReadStream(compressedFile); const destination = fs.createCacheWriteStream(outputFile); await fs.pipeline(source, createUnzip(), destination); } else { - throw new Error('Unsupported compression standard'); + throw new Error(`Unsupported compression standard '${compression}'`); } } diff --git a/lib/util/fs/index.ts b/lib/util/fs/index.ts index 3b2493706e2eec..5ac59f80278f08 100644 --- a/lib/util/fs/index.ts +++ b/lib/util/fs/index.ts @@ -176,8 +176,9 @@ export function createCacheWriteStream(path: string): fs.WriteStream { return fs.createWriteStream(fullPath); } -export function createReadStream(path: string): fs.ReadStream { - return fs.createReadStream(path); +export function createCacheReadStream(path: string): fs.ReadStream { + const fullPath = ensureCachePath(path); + return fs.createReadStream(fullPath); } export async function localPathIsFile(pathName: string): Promise { From b979bd1963846004a319c1d181326c27e16f1e10 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 9 Jul 2024 21:59:24 +0200 Subject: [PATCH 10/62] fix error --- lib/modules/datasource/deb/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 51c03cfa297a53..a2bb8ce6ff9e3d 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -56,7 +56,6 @@ export class DebDatasource extends Datasource { override readonly caching = true; - /** * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to * have enough tests in your application. @@ -262,7 +261,7 @@ export class DebDatasource extends Datasource { // read line by line to avoid high memory consumption as the extracted Packages // files can be multiple MBs in size const rl = readline.createInterface({ - input: fs.createReadStream(extractedFile), + input: fs.createCacheReadStream(extractedFile), terminal: false, }); From 1abdbb5338eda57cdfb87ad7a625233f8b0714be Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 9 Jul 2024 22:38:23 +0200 Subject: [PATCH 11/62] split up impl --- lib/modules/datasource/deb/index.spec.ts | 36 +------- lib/modules/datasource/deb/index.ts | 100 ++--------------------- lib/modules/datasource/deb/release.ts | 31 +++++++ lib/modules/datasource/deb/url.spec.ts | 32 ++++++++ lib/modules/datasource/deb/url.ts | 76 +++++++++++++++++ 5 files changed, 148 insertions(+), 127 deletions(-) create mode 100644 lib/modules/datasource/deb/release.ts create mode 100644 lib/modules/datasource/deb/url.spec.ts create mode 100644 lib/modules/datasource/deb/url.ts diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index e097a6a8910c56..165828e3d7efc9 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -183,42 +183,12 @@ describe('modules/datasource/deb/index', () => { }); }); - describe('constructComponentUrls', () => { - it('constructs URLs correctly from registry URL with suite', () => { - const registryUrl = - 'https://ftp.debian.org/debian?suite=stable&components=main,contrib&binaryArch=amd64'; - const expectedUrls = [ - 'https://ftp.debian.org/debian/dists/stable/main/binary-amd64', - 'https://ftp.debian.org/debian/dists/stable/contrib/binary-amd64', - ]; - const componentUrls = debDatasource.constructComponentUrls(registryUrl); - expect(componentUrls).toEqual(expectedUrls); - }); - - it('constructs URLs correctly from registry URL with release', () => { - const registryUrl = - 'https://ftp.debian.org/debian?release=bullseye&components=main,contrib&binaryArch=amd64'; - const expectedUrls = [ - 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', - 'https://ftp.debian.org/debian/dists/bullseye/contrib/binary-amd64', - ]; - const componentUrls = debDatasource.constructComponentUrls(registryUrl); - expect(componentUrls).toEqual(expectedUrls); - }); - - it('throws an error if required parameters are missing', () => { - const registryUrl = - 'https://ftp.debian.org/debian?components=main,contrib'; - expect(() => debDatasource.constructComponentUrls(registryUrl)).toThrow( - 'Missing required query parameter', - ); - }); - }); - describe('parseExtractedPackage', () => { it('should parse the last package', async () => { + await copyFile(fixturePackagesPath, extractedPackageFile); + const release = await debDatasource.parseExtractedPackage( - fixturePackagesPath, + extractedPackageFile, 'alien-arena-data', new Date(), ); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index a2bb8ce6ff9e3d..799a3ec196d0fe 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -6,10 +6,11 @@ import { logger } from '../../../logger'; import { cache } from '../../../util/cache/package/decorator'; import * as fs from '../../../util/fs'; import type { HttpOptions } from '../../../util/http/types'; -import { joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; import type { GetReleasesConfig, ReleaseResult } from '../types'; +import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; +import { constructComponentUrls } from './url'; export class DebDatasource extends Datasource { static readonly id = 'deb'; @@ -271,7 +272,7 @@ export class DebDatasource extends Datasource { if (line === '') { // All information of the package are available, early return possible if (currentPackage.Package === packageName) { - return this.formatReleaseResult(currentPackage); + return formatReleaseResult(currentPackage); } currentPackage = {}; } else { @@ -286,85 +287,12 @@ export class DebDatasource extends Datasource { // Check the last package after file reading is complete if (currentPackage.Package === packageName) { - return this.formatReleaseResult(currentPackage); + return formatReleaseResult(currentPackage); } return null; } - /** - * Formats the package description into a ReleaseResult. - * - * @param packageDesc - The package description object. - * @returns A formatted ReleaseResult. - */ - formatReleaseResult(packageDesc: PackageDescription): ReleaseResult { - return { - releases: [{ version: packageDesc.Version! }], - homepage: packageDesc.Homepage, - }; - } - - /** - * Constructs the component URLs from the given registry URL. - * - * @param registryUrl - The base URL of the registry. - * @returns An array of component URLs. - * @throws Will throw an error if required parameters are missing from the URL. - */ - constructComponentUrls(registryUrl: string): string[] { - const REQUIRED_PARAMS = ['components', 'binaryArch']; - const OPTIONAL_PARAMS = ['release', 'suite']; - - const validateUrlAndParams = (url: URL): void => { - REQUIRED_PARAMS.forEach((param) => { - if (!url.searchParams.has(param)) { - throw new Error(`Missing required query parameter '${param}'`); - } - }); - }; - - const getReleaseParam = (url: URL): string => { - for (const param of OPTIONAL_PARAMS) { - const paramValue = url.searchParams.get(param); - if (paramValue !== null) { - return paramValue; - } - } - throw new Error( - `Missing one of ${OPTIONAL_PARAMS.join(', ')} query parameter`, - ); - }; - - try { - const url = new URL(registryUrl); - validateUrlAndParams(url); - - const release = getReleaseParam(url); - const binaryArch = url.searchParams.get('binaryArch'); - const components = url.searchParams.get('components')!.split(','); - - // Clean up URL search parameters for constructing new URLs - [...REQUIRED_PARAMS, ...OPTIONAL_PARAMS].forEach((param) => - url.searchParams.delete(param), - ); - - return components.map((component) => { - return joinUrlParts( - url.toString(), - `dists`, - release, - component, - `binary-${binaryArch}`, - ); - }); - } catch (error) { - throw new Error( - `Invalid deb repo URL: ${registryUrl} - see documentation: ${error.message}`, - ); - } - } - /** * Fetches the release information for a given package from the registry URL. * @@ -380,7 +308,7 @@ export class DebDatasource extends Datasource { return null; } - const componentUrls = this.constructComponentUrls(registryUrl); + const componentUrls = constructComponentUrls(registryUrl); let aggregatedRelease: ReleaseResult | null = null; for (const componentUrl of componentUrls) { @@ -397,9 +325,7 @@ export class DebDatasource extends Datasource { if (aggregatedRelease === null) { aggregatedRelease = newRelease; } else { - if ( - !this.releaseMetaInformationMatches(aggregatedRelease, newRelease) - ) { + if (!releaseMetaInformationMatches(aggregatedRelease, newRelease)) { logger.warn( { packageName }, 'Package occurred in more than one repository with different meta information. Aggregating releases anyway.', @@ -418,18 +344,4 @@ export class DebDatasource extends Datasource { return aggregatedRelease; } - - /** - * Checks if two release metadata objects match. - * - * @param lhs - The first release result. - * @param rhs - The second release result. - * @returns True if the metadata matches, otherwise false. - */ - releaseMetaInformationMatches( - lhs: ReleaseResult, - rhs: ReleaseResult, - ): boolean { - return lhs.homepage === rhs.homepage; - } } diff --git a/lib/modules/datasource/deb/release.ts b/lib/modules/datasource/deb/release.ts new file mode 100644 index 00000000000000..4f4ccf72377b3d --- /dev/null +++ b/lib/modules/datasource/deb/release.ts @@ -0,0 +1,31 @@ +import type { ReleaseResult } from '..'; +import type { PackageDescription } from './types'; + +/** + * Checks if two release metadata objects match. + * + * @param lhs - The first release result. + * @param rhs - The second release result. + * @returns True if the metadata matches, otherwise false. + */ +export function releaseMetaInformationMatches( + lhs: ReleaseResult, + rhs: ReleaseResult, +): boolean { + return lhs.homepage === rhs.homepage; +} + +/** + * Formats the package description into a ReleaseResult. + * + * @param packageDesc - The package description object. + * @returns A formatted ReleaseResult. + */ +export function formatReleaseResult( + packageDesc: PackageDescription, +): ReleaseResult { + return { + releases: [{ version: packageDesc.Version! }], + homepage: packageDesc.Homepage, + }; +} diff --git a/lib/modules/datasource/deb/url.spec.ts b/lib/modules/datasource/deb/url.spec.ts new file mode 100644 index 00000000000000..3d3aa1386272f5 --- /dev/null +++ b/lib/modules/datasource/deb/url.spec.ts @@ -0,0 +1,32 @@ +import { constructComponentUrls } from './url'; + +describe('modules/datasource/deb/url', () => { + it('constructs URLs correctly from registry URL with suite', () => { + const registryUrl = + 'https://ftp.debian.org/debian?suite=stable&components=main,contrib&binaryArch=amd64'; + const expectedUrls = [ + 'https://ftp.debian.org/debian/dists/stable/main/binary-amd64', + 'https://ftp.debian.org/debian/dists/stable/contrib/binary-amd64', + ]; + const componentUrls = constructComponentUrls(registryUrl); + expect(componentUrls).toEqual(expectedUrls); + }); + + it('constructs URLs correctly from registry URL with release', () => { + const registryUrl = + 'https://ftp.debian.org/debian?release=bullseye&components=main,contrib&binaryArch=amd64'; + const expectedUrls = [ + 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', + 'https://ftp.debian.org/debian/dists/bullseye/contrib/binary-amd64', + ]; + const componentUrls = constructComponentUrls(registryUrl); + expect(componentUrls).toEqual(expectedUrls); + }); + + it('throws an error if required parameters are missing', () => { + const registryUrl = 'https://ftp.debian.org/debian?components=main,contrib'; + expect(() => constructComponentUrls(registryUrl)).toThrow( + 'Missing required query parameter', + ); + }); +}); diff --git a/lib/modules/datasource/deb/url.ts b/lib/modules/datasource/deb/url.ts new file mode 100644 index 00000000000000..37af1859794d4e --- /dev/null +++ b/lib/modules/datasource/deb/url.ts @@ -0,0 +1,76 @@ +import { joinUrlParts } from '../../../util/url'; + +/** + * Constructs the component URLs from the given registry URL. + * + * @param registryUrl - The base URL of the registry. + * @returns An array of component URLs. + * @throws Will throw an error if required parameters are missing from the URL. + */ +export function constructComponentUrls(registryUrl: string): string[] { + const REQUIRED_PARAMS = ['components', 'binaryArch']; + const OPTIONAL_PARAMS = ['release', 'suite']; + + try { + const url = new URL(registryUrl); + validateUrlAndParams(url, REQUIRED_PARAMS); + + const release = getReleaseParam(url, OPTIONAL_PARAMS); + const binaryArch = url.searchParams.get('binaryArch'); + const components = url.searchParams.get('components')!.split(','); + + // Clean up URL search parameters for constructing new URLs + [...REQUIRED_PARAMS, ...OPTIONAL_PARAMS].forEach((param) => + url.searchParams.delete(param), + ); + + return components.map((component) => + joinUrlParts( + url.toString(), + `dists`, + release, + component, + `binary-${binaryArch}`, + ), + ); + } catch (error) { + throw new Error( + `Invalid deb repo URL: ${registryUrl} - see documentation: ${error.message}`, + ); + } +} + +/** + * Validates that the required parameters are present in the URL. + * + * @param url - The URL to validate. + * @param requiredParams - The list of required query parameters. + * @throws Will throw an error if a required parameter is missing. + */ +function validateUrlAndParams(url: URL, requiredParams: string[]): void { + requiredParams.forEach((param) => { + if (!url.searchParams.has(param)) { + throw new Error(`Missing required query parameter '${param}'`); + } + }); +} + +/** + * Retrieves the release parameter from the URL. + * + * @param url - The URL to retrieve the release parameter from. + * @param optionalParams - The list of optional query parameters. + * @returns The value of the release parameter. + * @throws Will throw an error if none of the optional parameters are found. + */ +function getReleaseParam(url: URL, optionalParams: string[]): string { + for (const param of optionalParams) { + const paramValue = url.searchParams.get(param); + if (paramValue !== null) { + return paramValue; + } + } + throw new Error( + `Missing one of ${optionalParams.join(', ')} query parameter`, + ); +} From 4608379b864744a72a5fe74a7f81ffaf9fdc75a6 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:37:36 +0200 Subject: [PATCH 12/62] convert to named functions --- lib/modules/datasource/deb/index.spec.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 165828e3d7efc9..120e3f91798b76 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -266,12 +266,14 @@ describe('modules/datasource/deb/index', () => { * @param arch - The architecture name (e.g., 'amd64', 'i386'). * @returns The complete URL to the component directory. */ -const getComponentUrl = ( +function getComponentUrl( baseUrl: string, release: string, component: string, arch: string, -) => `${baseUrl}/debian/dists/${release}/${component}/binary-${arch}`; +): string { + return `${baseUrl}/debian/dists/${release}/${component}/binary-${arch}`; +} /** * Constructs a URL for accessing the Packages.gz file for a specific component, release, and architecture. @@ -282,12 +284,14 @@ const getComponentUrl = ( * @param arch - The architecture name (e.g., 'amd64', 'i386'). * @returns The complete URL to the Packages.gz file. */ -const getPackageUrl = ( +function getPackageUrl( baseUrl: string, release: string, component: string, arch: string, -) => `${getComponentUrl(baseUrl, release, component, arch)}/Packages.gz`; +) { + return `${getComponentUrl(baseUrl, release, component, arch)}/Packages.gz`; +} /** * Constructs a URL used generating the component url with specific release, components, and architecture. @@ -298,10 +302,11 @@ const getPackageUrl = ( * @param arch - The architecture name (e.g., 'amd64', 'i386'). * @returns The complete URL to the package registry. */ -const getRegistryUrl = ( +function getRegistryUrl( baseUrl: string, release: string, components: string[], arch: string, -) => - `${baseUrl}/debian?suite=${release}&components=${components.join(',')}&binaryArch=${arch}`; +) { + return `${baseUrl}/debian?suite=${release}&components=${components.join(',')}&binaryArch=${arch}`; +} From c7e543b972b654bc26b2bb4d4260d951ae8912fa Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:39:47 +0200 Subject: [PATCH 13/62] deactivate default caching --- lib/modules/datasource/deb/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 799a3ec196d0fe..cd5abfe3b85323 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -55,8 +55,6 @@ export class DebDatasource extends Datasource { 'https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64', ]; - override readonly caching = true; - /** * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to * have enough tests in your application. From 021ea951ceec3a541e97b087c6bf7f67468ee180 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:41:23 +0200 Subject: [PATCH 14/62] remove package rules example --- lib/modules/datasource/deb/readme.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/modules/datasource/deb/readme.md b/lib/modules/datasource/deb/readme.md index 78b118f2fe8a8c..cf216dd31f175e 100644 --- a/lib/modules/datasource/deb/readme.md +++ b/lib/modules/datasource/deb/readme.md @@ -36,14 +36,6 @@ First you would set a custom manager in your `renovate.json` file for `Dockerfil ], "datasourceTemplate": "deb" } - ], - "packageRules": [ - { - "matchDatasources": ["deb"], - "registryUrls": [ - "https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64" - ] - } ] } ``` From 5675c77ac5552b20f9243e19de02fcba72690d35 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:41:52 +0200 Subject: [PATCH 15/62] remove stale comment --- lib/modules/datasource/deb/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index cd5abfe3b85323..d4f0edf8c6fc0e 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -55,10 +55,6 @@ export class DebDatasource extends Datasource { 'https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64', ]; - /** - * Not all Debian packages follow Semver, so it's wise to keep this loose but make sure to - * have enough tests in your application. - */ override readonly defaultVersioning = 'deb'; static requiredPackageKeys: Array = [ From 47c75b5e0792efcda9f8b113e688c1614ff8ca16 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:43:12 +0200 Subject: [PATCH 16/62] use debug level instead --- lib/modules/datasource/deb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index d4f0edf8c6fc0e..6c286f4b12efe2 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -329,7 +329,7 @@ export class DebDatasource extends Datasource { } } } catch (error) { - logger.warn( + logger.debug( { componentUrl, error }, 'Skipping component due to an error', ); From 403d0cb7b1b3d629a59ccce77106287657808dc9 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:00:52 +0200 Subject: [PATCH 17/62] parse all package from extracted package index --- lib/modules/datasource/deb/index.spec.ts | 15 ++++++---- lib/modules/datasource/deb/index.ts | 38 +++++++++++++----------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 120e3f91798b76..998eca8fb2fcc1 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -183,16 +183,21 @@ describe('modules/datasource/deb/index', () => { }); }); - describe('parseExtractedPackage', () => { - it('should parse the last package', async () => { + describe('parseExtractedPackageIndex', () => { + it('should parse the extracted package', async () => { await copyFile(fixturePackagesPath, extractedPackageFile); - const release = await debDatasource.parseExtractedPackage( + const parsedPackages = await debDatasource.parseExtractedPackageIndex( extractedPackageFile, - 'alien-arena-data', new Date(), ); - expect(release?.releases?.[0]?.version).toBe('7.71.3+ds-1'); + + const firstPackage = parsedPackages.find((p) => p.Package === 'album'); + const lastPackage = parsedPackages.find( + (p) => p.Package === 'alien-arena-data', + ); + expect(firstPackage?.Version).toBe('4.15-1'); + expect(lastPackage?.Version).toBe('7.71.3+ds-1'); }); }); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 6c286f4b12efe2..b8b1e6e479ca5b 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -235,24 +235,22 @@ export class DebDatasource extends Datasource { } /** - * Parses the extracted package file to find the specified package. + * Parses the extracted package index file. * * @param extractedFile - The path to the extracted package file. - * @param packageName - The name of the package to find. * @param lastTimestamp - The timestamp of the last modification. - * @returns The release result if found, otherwise null. + * @returns a list of packages with minimal Metadata. */ @cache({ namespace: `datasource-${DebDatasource.id}-package`, - key: (extractedFile: string, packageName: string, lastTimestamp: Date) => - `${extractedFile}:${packageName}:${lastTimestamp.getTime()}`, + key: (extractedFile: string, lastTimestamp: Date) => + `${extractedFile}:${lastTimestamp.getTime()}`, ttlMinutes: 24 * 60, }) - async parseExtractedPackage( + async parseExtractedPackageIndex( extractedFile: string, - packageName: string, lastTimestamp: Date, - ): Promise { + ): Promise { // read line by line to avoid high memory consumption as the extracted Packages // files can be multiple MBs in size const rl = readline.createInterface({ @@ -261,14 +259,15 @@ export class DebDatasource extends Datasource { }); let currentPackage: PackageDescription = {}; + const allPackages: PackageDescription[] = []; for await (const line of rl) { if (line === '') { - // All information of the package are available, early return possible - if (currentPackage.Package === packageName) { - return formatReleaseResult(currentPackage); + // All information of the package are available, add to the list of packages + if (Object.keys(currentPackage).length > 0) { + allPackages.push(currentPackage); + currentPackage = {}; } - currentPackage = {}; } else { for (const key of DebDatasource.requiredPackageKeys) { if (line.startsWith(`${key}:`)) { @@ -280,11 +279,11 @@ export class DebDatasource extends Datasource { } // Check the last package after file reading is complete - if (currentPackage.Package === packageName) { - return formatReleaseResult(currentPackage); + if (Object.keys(currentPackage).length > 0) { + allPackages.push(currentPackage); } - return null; + return allPackages; } /** @@ -309,13 +308,16 @@ export class DebDatasource extends Datasource { try { const { extractedFile, lastTimestamp } = await this.downloadAndExtractPackage(componentUrl); - const newRelease = await this.parseExtractedPackage( + const parsedPackages = await this.parseExtractedPackageIndex( extractedFile, - packageName, lastTimestamp, ); + const parsedPackage = parsedPackages.find( + (p) => p.Package === packageName, + ); - if (newRelease) { + if (parsedPackage) { + const newRelease = formatReleaseResult(parsedPackage); if (aggregatedRelease === null) { aggregatedRelease = newRelease; } else { From 976b49c2d67d67ca7ada75912a926cd4aa72b688 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:08:27 +0200 Subject: [PATCH 18/62] downloadAndExtractPackage continue with next supported compression instead --- lib/modules/datasource/deb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index b8b1e6e479ca5b..9f0e999010ca61 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -149,7 +149,7 @@ export class DebDatasource extends Datasource { if (!lastTimestamp) { //extracting went wrong - break; + continue; } return { extractedFile, lastTimestamp }; From 30e479f920d695a339407167b4b2133103a66841 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:29:01 +0200 Subject: [PATCH 19/62] validate compressed package checksum using InRelease file --- .../datasource/deb/__fixtures__/InRelease | 1257 +++++++++++++++++ lib/modules/datasource/deb/checksum.spec.ts | 57 + lib/modules/datasource/deb/checksum.ts | 46 + lib/modules/datasource/deb/index.spec.ts | 131 +- lib/modules/datasource/deb/index.ts | 84 +- lib/modules/datasource/deb/url.spec.ts | 10 +- lib/modules/datasource/deb/url.ts | 15 + 7 files changed, 1578 insertions(+), 22 deletions(-) create mode 100644 lib/modules/datasource/deb/__fixtures__/InRelease create mode 100644 lib/modules/datasource/deb/checksum.spec.ts create mode 100644 lib/modules/datasource/deb/checksum.ts diff --git a/lib/modules/datasource/deb/__fixtures__/InRelease b/lib/modules/datasource/deb/__fixtures__/InRelease new file mode 100644 index 00000000000000..02678d03acc313 --- /dev/null +++ b/lib/modules/datasource/deb/__fixtures__/InRelease @@ -0,0 +1,1257 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +Origin: Debian +Label: Debian +Suite: oldstable +Version: 11.10 +Codename: bullseye +Changelogs: https://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog +Date: Sat, 29 Jun 2024 10:26:51 UTC +Acquire-By-Hash: yes +No-Support-for-Architecture-all: Packages +Architectures: all amd64 arm64 armel armhf i386 mips64el mipsel ppc64el s390x +Components: main contrib non-free +Description: Debian 11.10 Released 29 June 2024 +MD5Sum: + 7fdf4db15250af5368cc52a91e8edbce 738242 contrib/Contents-all + cbd7bc4d3eb517ac2b22f929dfc07b47 57319 contrib/Contents-all.gz + 931db2c4ebb0d65d4f7b2b5be758e165 787377 contrib/Contents-amd64 + a4f8bff6283fb477a1a6ad2ebcfa593b 54657 contrib/Contents-amd64.gz + 865355923820a3e1b451e0c885dc6367 370971 contrib/Contents-arm64 + f68ed0d7047e24636232b5f2c3c4d03e 29568 contrib/Contents-arm64.gz + b6d2673f17fbdb3a5ce92404a62c2d7e 359292 contrib/Contents-armel + d02d94be587d56a1246b407669d2a24c 28039 contrib/Contents-armel.gz + d272ba9da0f302b6c09a36899e738115 367655 contrib/Contents-armhf + 317aa67ea34d625837d245f6fb00bdc4 29236 contrib/Contents-armhf.gz + ccb13401b0f48dded08ed089f8074765 407328 contrib/Contents-i386 + e496015d7e6e8d5a91cec31fc4bde74c 33556 contrib/Contents-i386.gz + 44384de1db64f592fc69693b355a0ec7 359402 contrib/Contents-mips64el + a2abf38d14c1c7e3aafcb21881b0fe7d 27962 contrib/Contents-mips64el.gz + 457feed233db5ce7db62cc69e7a8a5c6 360549 contrib/Contents-mipsel + 90ec76d0dca539a4c4aa33404de4c633 27942 contrib/Contents-mipsel.gz + 3a72ad65c09327aa8e6eff7ac8e4509d 370782 contrib/Contents-ppc64el + bc210dc639016790594e15eaec632b3f 29413 contrib/Contents-ppc64el.gz + e2089c91540f7adb693675935dacf9e5 357860 contrib/Contents-s390x + bb90fb42e72d39da53b3e1e2c2f46bc3 27518 contrib/Contents-s390x.gz + f14ad5f55fa6f1513777cd2eb6f56fd0 6726318 contrib/Contents-source + 8cef10b1d92f37255942a584f2d031fa 471162 contrib/Contents-source.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-all + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-all.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-amd64 + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-amd64.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-arm64 + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-arm64.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-armel + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-armel.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-armhf + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-armhf.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-i386 + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-i386.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-mips64el + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-mips64el.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-mipsel + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-mipsel.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-ppc64el + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-ppc64el.gz + d41d8cd98f00b204e9800998ecf8427e 0 contrib/Contents-udeb-s390x + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/Contents-udeb-s390x.gz + 9beab7be06225fd762256eeb587b8693 103359 contrib/binary-all/Packages + 44fa757d01fc1e44eb61430f0972371c 27400 contrib/binary-all/Packages.gz + 23b0434c177980598aeca100bbb7d694 23852 contrib/binary-all/Packages.xz + 99ed9d1679bb6639150e50c384066c6d 121 contrib/binary-all/Release + b2e59c670fc1ab12275578c74c8cc775 232262 contrib/binary-amd64/Packages + 28dff8b86daf5731f94c272bca52d981 61020 contrib/binary-amd64/Packages.gz + 99d60189517f474da1091bb70b96861d 50396 contrib/binary-amd64/Packages.xz + 9d52cf936e47a6874ff6a35ca2a301f7 123 contrib/binary-amd64/Release + 4e43da325981cca4ed08605738cdc780 181252 contrib/binary-arm64/Packages + dc2772f8da1ba1b721d3067d85395579 49104 contrib/binary-arm64/Packages.gz + 9c3bf485eca774443190fd64d987577c 40832 contrib/binary-arm64/Packages.xz + 3b7fc3c9bcee8878f4b08bad6c442699 123 contrib/binary-arm64/Release + 26aa872ab3be6fdc5acb90e15fd41a17 163450 contrib/binary-armel/Packages + a0d557d60aee3b8c963e0e05c3db56ad 44618 contrib/binary-armel/Packages.gz + 7b764febdf7dbe176b8254b4a327d71e 37344 contrib/binary-armel/Packages.xz + 106017bdaa8e736d7f4b93ab52d7b672 123 contrib/binary-armel/Release + d39a4d5b094737badf7303b0589bbb42 175958 contrib/binary-armhf/Packages + 3acc16ea4fc55d92820da209d822e78b 47820 contrib/binary-armhf/Packages.gz + 84e7a3fcc974c97ac862f93a9d4a97a7 40084 contrib/binary-armhf/Packages.xz + 0d4cc37644da9860aeb1067452e131be 123 contrib/binary-armhf/Release + a9424be2633227854362e75c981540b5 203906 contrib/binary-i386/Packages + 1114aa7b024df3f38bfd8f67ce50055c 54303 contrib/binary-i386/Packages.gz + e4ff6c267179b517c98c6364e7fb7838 45180 contrib/binary-i386/Packages.xz + 7af89a37cbee7bb14fe37f6018ff769f 122 contrib/binary-i386/Release + a5f53e85e45aad8f479bc5eaedf58300 163915 contrib/binary-mips64el/Packages + c26fc1bfee0496c015b531741f9784fa 44741 contrib/binary-mips64el/Packages.gz + 9aaf45def79d113abeff45635e45e28f 37380 contrib/binary-mips64el/Packages.xz + e64e94b3689b1bb241f6ed339c05ce4d 126 contrib/binary-mips64el/Release + b89d04eb565595db41bb16ad1de5c619 165055 contrib/binary-mipsel/Packages + bebbe90cb4e749f4e14fef3c8f9fbfad 45042 contrib/binary-mipsel/Packages.gz + ecbed7f84bd713877c90858c1baff5f3 37724 contrib/binary-mipsel/Packages.xz + a9deb5bb934d2909d050648b0695aa6e 124 contrib/binary-mipsel/Release + beaeb6d65696a36d0257326f4e9c16ee 182035 contrib/binary-ppc64el/Packages + f4636e58bb1d72141c4c9be966978ffa 49196 contrib/binary-ppc64el/Packages.gz + 8e582c96f3f673239d189b3cd8a99fc5 40856 contrib/binary-ppc64el/Packages.xz + 4afde84a5f9bae333b2d0a32ec16dd8d 125 contrib/binary-ppc64el/Release + bf78f8547ba071133071d27ec1a0cb73 162658 contrib/binary-s390x/Packages + 1778af44e3fa01a5d09b1f41f9cff5ec 44315 contrib/binary-s390x/Packages.gz + 7c622005459200b76639a9d101fc9736 37132 contrib/binary-s390x/Packages.xz + a8b0bfbbe304be551de0538a318ab8a3 123 contrib/binary-s390x/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-all/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-all/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-all/Packages.xz + 99ed9d1679bb6639150e50c384066c6d 121 contrib/debian-installer/binary-all/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-amd64/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-amd64/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-amd64/Packages.xz + 9d52cf936e47a6874ff6a35ca2a301f7 123 contrib/debian-installer/binary-amd64/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-arm64/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-arm64/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-arm64/Packages.xz + 3b7fc3c9bcee8878f4b08bad6c442699 123 contrib/debian-installer/binary-arm64/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-armel/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-armel/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-armel/Packages.xz + 106017bdaa8e736d7f4b93ab52d7b672 123 contrib/debian-installer/binary-armel/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-armhf/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-armhf/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-armhf/Packages.xz + 0d4cc37644da9860aeb1067452e131be 123 contrib/debian-installer/binary-armhf/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-i386/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-i386/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-i386/Packages.xz + 7af89a37cbee7bb14fe37f6018ff769f 122 contrib/debian-installer/binary-i386/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-mips64el/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-mips64el/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-mips64el/Packages.xz + e64e94b3689b1bb241f6ed339c05ce4d 126 contrib/debian-installer/binary-mips64el/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-mipsel/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-mipsel/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-mipsel/Packages.xz + a9deb5bb934d2909d050648b0695aa6e 124 contrib/debian-installer/binary-mipsel/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-ppc64el/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-ppc64el/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-ppc64el/Packages.xz + 4afde84a5f9bae333b2d0a32ec16dd8d 125 contrib/debian-installer/binary-ppc64el/Release + d41d8cd98f00b204e9800998ecf8427e 0 contrib/debian-installer/binary-s390x/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 contrib/debian-installer/binary-s390x/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 contrib/debian-installer/binary-s390x/Packages.xz + a8b0bfbbe304be551de0538a318ab8a3 123 contrib/debian-installer/binary-s390x/Release + fc412a0e8fed50416ae55ca3a34c2654 119152 contrib/dep11/Components-amd64.yml + 7473c932902284e9c636636a5ff0587b 15579 contrib/dep11/Components-amd64.yml.gz + 751b272121122fce4882d17a9d099c44 13564 contrib/dep11/Components-amd64.yml.xz + 49911a9d2f76ed13124c7cff0081266b 113437 contrib/dep11/Components-arm64.yml + ee72e145d0e71d94c0d418d36dabfd8c 14251 contrib/dep11/Components-arm64.yml.gz + 65f48dc9acec772076e60ce35239703f 12480 contrib/dep11/Components-arm64.yml.xz + b1f970bbcdd889ccff5c2646bc2835ba 113437 contrib/dep11/Components-armel.yml + d2a414b1147562c0ecfa1aab53fc0260 14029 contrib/dep11/Components-armel.yml.gz + b450a677c3a5d4a52d2a0df274c222cf 12524 contrib/dep11/Components-armel.yml.xz + 75c6b8bd42fc863caa66c454306c7d39 113437 contrib/dep11/Components-armhf.yml + ac52f103d1c493d0f8d8e5662d758f78 14127 contrib/dep11/Components-armhf.yml.gz + 80f4310b2d68bf09c7fbba34a0eec794 12480 contrib/dep11/Components-armhf.yml.xz + a46b6878a89f45fab86aca68bffe081d 118972 contrib/dep11/Components-i386.yml + 751ea67ac68d2e755726b4e9d62ab15e 15566 contrib/dep11/Components-i386.yml.gz + 82c956565311c8a7d90bff6e0a226fbe 13560 contrib/dep11/Components-i386.yml.xz + 6f822ef8f2c13dc4212ade261b4a8752 113437 contrib/dep11/Components-mips64el.yml + a072aab0fb45dab4a6e25295f23e9b5f 14056 contrib/dep11/Components-mips64el.yml.gz + e5c2dd7fd785fa1ab66099d7763bd670 12500 contrib/dep11/Components-mips64el.yml.xz + 432a29a22c4a782f6edad376f386937f 113437 contrib/dep11/Components-ppc64el.yml + b5202b5037949e593060f92290d6f949 14219 contrib/dep11/Components-ppc64el.yml.gz + dd92a500c7807091665dbc207c9bef68 12496 contrib/dep11/Components-ppc64el.yml.xz + 53c6b87820861b0ed316a88f7542cd76 113437 contrib/dep11/Components-s390x.yml + 5a4872d3187bc79418b468890be4b5fe 14050 contrib/dep11/Components-s390x.yml.gz + eefb3301e486aedbbbb1d735e2522a00 12488 contrib/dep11/Components-s390x.yml.xz + 5d8e37f26e7e15f367751089fa13c876 271360 contrib/dep11/icons-128x128.tar + 500b14a4cafa23b9106b402737f863a7 195507 contrib/dep11/icons-128x128.tar.gz + d9651fb188be2221d2f583aeba83d8fc 83968 contrib/dep11/icons-48x48.tar + 6b5ea4675ad78554aaa53b344f1bd146 47168 contrib/dep11/icons-48x48.tar.gz + 7115d3a3d41fc9bca9cfcc3c608bebf2 138752 contrib/dep11/icons-64x64.tar + c839e679f1d60d294d39884d0911e514 93294 contrib/dep11/icons-64x64.tar.gz + 01e75740c90a7df7e474a1c6152b2aa6 192685 contrib/i18n/Translation-en + a2e9608c3e388d26e031583f200e2f92 46929 contrib/i18n/Translation-en.bz2 + a9deb5649bbcee10ef0051499621b883 124 contrib/source/Release + 5c6779469f7704a21ddc9adee114e28d 178818 contrib/source/Sources + cd9405bbadd352142170a2041043d6e1 51518 contrib/source/Sources.gz + 82d8ed5b335182b6406c0d9a80fd3612 43180 contrib/source/Sources.xz + bb1c95c3661722052d71bc92b9473b93 481615325 main/Contents-all + a23c447fe6e25aa9384566a34bb51501 31274162 main/Contents-all.gz + 9ed48a76f1abbc65e37f28341b15d685 129675985 main/Contents-amd64 + 14036820dcf0f4bbb349b567da33fcd2 10326393 main/Contents-amd64.gz + d58a018d50a16de7da0b0fcdffa95927 123035834 main/Contents-arm64 + 42adb4219ebc92a3ed0b20398ae1f4aa 9886926 main/Contents-arm64.gz + 36be0feafbf2f35e105d34d2a64b905c 105223777 main/Contents-armel + fb5a21332cb8120208bbe36a2ee958dc 8750503 main/Contents-armel.gz + fb791357f9885e198c4b61f9520dd8e2 114286689 main/Contents-armhf + bb29c4a428e540096638328142eee2e6 9360391 main/Contents-armhf.gz + 410fe871653bb92ef7d31c2c9a98fbd8 129640940 main/Contents-i386 + 9425a41e0b583f703e7cf72ea6b790e3 10257245 main/Contents-i386.gz + 58e2abcb3712c9ab77442723d5680193 111617889 main/Contents-mips64el + 0544b23db2bc005e48394bcba7f93ea7 9088306 main/Contents-mips64el.gz + 1d191fcd9298178026c13ed07ba52fc4 112502390 main/Contents-mipsel + 31387b232ecfac335d450d9db4e3b288 9170633 main/Contents-mipsel.gz + 06763893333c1dae4edf690f08efaf7a 116637320 main/Contents-ppc64el + 95dd183992a8cc6fba5749f963847e7b 9408238 main/Contents-ppc64el.gz + bc6b1b71649792e31d23a20c84a12440 104233350 main/Contents-s390x + 3dfed74637b57518acba90b43432523e 8763164 main/Contents-s390x.gz + 4a48ce80a1c96e6228199b15586e3313 708782673 main/Contents-source + 52fc08b620ad5ba7f4f13ea7927ed720 74632984 main/Contents-source.gz + 3488bb04877feba191a892051a322dcd 157474 main/Contents-udeb-all + 0fd83120714c442753d415b70bb4fdeb 13511 main/Contents-udeb-all.gz + da6dffb54dffb50374c46fba0f424d88 476840 main/Contents-udeb-amd64 + 3f82997ce29fe395ad193c743d1e69de 36123 main/Contents-udeb-amd64.gz + 09dc7d1b6695baed56898dbdbabb8416 508817 main/Contents-udeb-arm64 + 75e72fec92180cf3960515549b82a57d 38016 main/Contents-udeb-arm64.gz + 88a7581d1678ca9bc33df4dd670c36bc 323083 main/Contents-udeb-armel + 89e08bf01cf68d4cf72d02b611b3707b 25444 main/Contents-udeb-armel.gz + a8117b8d55193ec34a25a1fd657b566a 579793 main/Contents-udeb-armhf + e23943058d88f4077641e17be7a7f14f 43537 main/Contents-udeb-armhf.gz + 493e649651f40879db71311d6cf58e89 750959 main/Contents-udeb-i386 + e37f0eb19dd093d27cce01adc787c0fb 54470 main/Contents-udeb-i386.gz + b41ef4e51b54fbec5ccad0b33d1f2a58 760308 main/Contents-udeb-mips64el + f1c3ff8e92c9a768f933e32ecf3cb16d 52564 main/Contents-udeb-mips64el.gz + 100114d9357bb04fdc3bb67e7e6e7082 759984 main/Contents-udeb-mipsel + 29a1bb80f9c360ca75229ca92b3a56f7 52894 main/Contents-udeb-mipsel.gz + c4332479df44fd6545b9f3235f010871 401645 main/Contents-udeb-ppc64el + 8bc6b3507018920b6504e2719304fc6b 29327 main/Contents-udeb-ppc64el.gz + d7a8d7f9699ccccc4b991c92384c6960 258318 main/Contents-udeb-s390x + cbdd71bc2f5c2999f57142487875d453 21034 main/Contents-udeb-s390x.gz + 3555a7194a060e326c4b18ea2287b47a 20549447 main/binary-all/Packages + 8c8ca236677d39307664bd4841207f45 5221543 main/binary-all/Packages.gz + 258a6d52ce0860a6f63d9c0b18442ccc 3872680 main/binary-all/Packages.xz + a767f4873d6e52861b6d5567fa9e95d0 118 main/binary-all/Release + 505da756acdf6332d1cec66322179100 45560272 main/binary-amd64/Packages + bc4f4f9c89a4ef4d7f555bf0c93923cd 11091667 main/binary-amd64/Packages.gz + 3f3f0aa2373daea063f705c5c6d2f9b2 8066552 main/binary-amd64/Packages.xz + 0a18662937c75f355acb9d6082f8a5df 120 main/binary-amd64/Release + b15ac7897b3cee0e9769c734ec5b5158 44839134 main/binary-arm64/Packages + bc62a052ccc780a434569f19434ab439 10936444 main/binary-arm64/Packages.gz + 523e3b85decd0cc03414892ea0595cb3 7955944 main/binary-arm64/Packages.xz + 903c97d0be3504adcb71835cffeb00e5 120 main/binary-arm64/Release + 5803bd2fe56ee523aba5b66088d913d8 43472829 main/binary-armel/Packages + 73be6981fb4ce386917bc3fef13127f2 10686811 main/binary-armel/Packages.gz + f361d1a6137e8ea6a6402676d3618768 7779872 main/binary-armel/Packages.xz + 79801363715d0de21dd555dfd58dcc22 120 main/binary-armel/Release + 40f3dc8564e33a3beac0c20c234fea12 43914977 main/binary-armhf/Packages + a2e8b4956420192882497b9b7b69c527 10777938 main/binary-armhf/Packages.gz + a711b74422a6e2ea1f3b2d322d594e3f 7840448 main/binary-armhf/Packages.xz + 17939e5e0d58f373f9d2043d20ab2505 120 main/binary-armhf/Release + a352439a73f764251a00a1a4267fecdb 45116776 main/binary-i386/Packages + 0fdd01e32786551c443f2887327bc5e6 11009325 main/binary-i386/Packages.gz + 6dc1f9aa50516970c30a01fa0f1fdc79 8007036 main/binary-i386/Packages.xz + faec5d2b1618248418f2f127ee74ccc7 119 main/binary-i386/Release + 4f54781db1360d772473d238bc081ac3 43745682 main/binary-mips64el/Packages + 35ce9e1cfd9fde8e80cdcdb4edacf4f6 10714581 main/binary-mips64el/Packages.gz + dffba497cf73edf17f75a4199100c492 7798860 main/binary-mips64el/Packages.xz + 8bbc71c959845dbe1da243824649dd22 123 main/binary-mips64el/Release + e0c5405af8b68307d3a6fe3618ac7d62 43699706 main/binary-mipsel/Packages + 54a7e115835d8957daaf1578a3c0756b 10719540 main/binary-mipsel/Packages.gz + d51a9456a84af9a32ea8a5e16fdbec64 7800188 main/binary-mipsel/Packages.xz + 6c3b8b97a5543812b4d2ac2659407636 121 main/binary-mipsel/Release + 617e60f3661d71dd4ad6c736424dca95 44694611 main/binary-ppc64el/Packages + 2daa2387ad622c6d7dfd32ee29af24a7 10879131 main/binary-ppc64el/Packages.gz + e8c3644a02fad29d73a8b1d2e87c3c37 7919384 main/binary-ppc64el/Packages.xz + 2285032eb6b8b3b4aac588f5733c1f9c 122 main/binary-ppc64el/Release + eac4570bf74a06eb32b550451b75333d 43359927 main/binary-s390x/Packages + 3f9444b9a6e3eea932dc0f24604638bd 10679541 main/binary-s390x/Packages.gz + bdfb265f2e69401c2b569968b9926a08 7772456 main/binary-s390x/Packages.xz + 5c099c492da7dccf96d0e5a764853520 120 main/binary-s390x/Release + 90bc5b675a79a2091c332342c8d77648 61176 main/debian-installer/binary-all/Packages + eb9111185e243c5071ecd4241b523432 16521 main/debian-installer/binary-all/Packages.gz + 09e759541fa53aea35a88002f16a656a 14524 main/debian-installer/binary-all/Packages.xz + a767f4873d6e52861b6d5567fa9e95d0 118 main/debian-installer/binary-all/Release + b56020d9bcdd29b3811ea79d1f63ea78 274532 main/debian-installer/binary-amd64/Packages + 7c5b8b0841bc8d1b7031fb94f193d0f9 67562 main/debian-installer/binary-amd64/Packages.gz + b99892c8a261cb23b03d511885535ccf 55224 main/debian-installer/binary-amd64/Packages.xz + 0a18662937c75f355acb9d6082f8a5df 120 main/debian-installer/binary-amd64/Release + 2de06572eadfb9ad7942113fce6c1651 257529 main/debian-installer/binary-arm64/Packages + 60f870b0f3ee708e66bff0b17a7a821c 64500 main/debian-installer/binary-arm64/Packages.gz + 64266b4ca1025b4a68b177b9c4fd4cf2 53128 main/debian-installer/binary-arm64/Packages.xz + 903c97d0be3504adcb71835cffeb00e5 120 main/debian-installer/binary-arm64/Release + 4e3bf3c234279ba5ed2526477b303c85 248543 main/debian-installer/binary-armel/Packages + 1ded558f66933bcb4f20487c6378cb12 63326 main/debian-installer/binary-armel/Packages.gz + 71fb9d8b51b366e572dae5b037b202d3 52408 main/debian-installer/binary-armel/Packages.xz + 79801363715d0de21dd555dfd58dcc22 120 main/debian-installer/binary-armel/Release + 05879d2dd7dc40662d0ce05ba55d9278 251968 main/debian-installer/binary-armhf/Packages + a2cf9b3b2698018ca9c78ee42273aeb0 64653 main/debian-installer/binary-armhf/Packages.gz + a23270ff356f5ae88f2eb9d4ecef58b6 53088 main/debian-installer/binary-armhf/Packages.xz + 17939e5e0d58f373f9d2043d20ab2505 120 main/debian-installer/binary-armhf/Release + 15efd5db6751f0c98706ec329bc6207f 349625 main/debian-installer/binary-i386/Packages + 422131a7f1306c15933373d4a96689af 77105 main/debian-installer/binary-i386/Packages.gz + bbb6661cf26c227880b9a989f0cdadd8 63268 main/debian-installer/binary-i386/Packages.xz + faec5d2b1618248418f2f127ee74ccc7 119 main/debian-installer/binary-i386/Release + 286a1c912e3a52a98e98a51d2761c0d2 364896 main/debian-installer/binary-mips64el/Packages + b8ba0b0d68ff20336993e0260090c221 79808 main/debian-installer/binary-mips64el/Packages.gz + 7de50b9c7612040ae6ea60d4f6a38f90 64948 main/debian-installer/binary-mips64el/Packages.xz + 8bbc71c959845dbe1da243824649dd22 123 main/debian-installer/binary-mips64el/Release + b784583181ec22b091f40bc0ede59f5e 364382 main/debian-installer/binary-mipsel/Packages + 8c3f7f5c3e4f6c05b6dae36190cfb0fa 79714 main/debian-installer/binary-mipsel/Packages.gz + ea4180d38718f00904a929b8807ad651 65208 main/debian-installer/binary-mipsel/Packages.xz + 6c3b8b97a5543812b4d2ac2659407636 121 main/debian-installer/binary-mipsel/Release + 718544dc64d327a8ea611bb3069f70ac 257113 main/debian-installer/binary-ppc64el/Packages + 0a732d3a65b3fb27bd909fe4331912ea 65212 main/debian-installer/binary-ppc64el/Packages.gz + 7a08c41925a20f8072c45fdedd207d63 53132 main/debian-installer/binary-ppc64el/Packages.xz + 2285032eb6b8b3b4aac588f5733c1f9c 122 main/debian-installer/binary-ppc64el/Release + b3333a6b6de1ec953b6856714a147539 226455 main/debian-installer/binary-s390x/Packages + a38fc8e1100b7d3da70b8920dba9658b 60253 main/debian-installer/binary-s390x/Packages.gz + 555453597372b4463799d0ec758d7a84 49424 main/debian-installer/binary-s390x/Packages.xz + 5c099c492da7dccf96d0e5a764853520 120 main/debian-installer/binary-s390x/Release + 97a6eda13094854f8838218d5869a796 18520413 main/dep11/Components-amd64.yml + 9cd807c0b66a8489b5385bf4f343b288 6213469 main/dep11/Components-amd64.yml.gz + c16ba02c289510dce9857dfa6cde4550 4048504 main/dep11/Components-amd64.yml.xz + 3e8ecb0bbaecb88d0b16dfaa037dba73 18436837 main/dep11/Components-arm64.yml + 09ef5a87673c946f916b0d8ef0c2471d 6191092 main/dep11/Components-arm64.yml.gz + fef127cee05f3efb96261e78b4fe4568 4033216 main/dep11/Components-arm64.yml.xz + 67becc674b536e310fe22492d55c8652 17658848 main/dep11/Components-armel.yml + 34cd8a6a1206f804e6d5c54dcdd3ef63 5952269 main/dep11/Components-armel.yml.gz + d7cc0222cae53bcfa1de29218fe5cb94 3879744 main/dep11/Components-armel.yml.xz + 09010fea4c1cf082bd54aecc24182e45 18205252 main/dep11/Components-armhf.yml + f5b7fd1a9cb147fa6b90e60a4d2139c1 6110587 main/dep11/Components-armhf.yml.gz + f1f223ca9e69ad1901345ceb404a5666 3983180 main/dep11/Components-armhf.yml.xz + ee8f83c597007ab84b58feec05d647fa 18485654 main/dep11/Components-i386.yml + 5a6b35ea7b54d88842ab30bbbd469623 6201776 main/dep11/Components-i386.yml.gz + 239cc12774e7c2925d1d783faaf01b5d 4041608 main/dep11/Components-i386.yml.xz + dd59f50383f269a8e1ec09c49d8a786c 17819116 main/dep11/Components-mips64el.yml + e3f03ed2f2c22dac3207e5f3fb98f862 5977494 main/dep11/Components-mips64el.yml.gz + 437c9fa1e058fc9a3486fb8b224740f6 3896708 main/dep11/Components-mips64el.yml.xz + 09d0cb63fdf4a4904155dc0d56ccc04b 17947079 main/dep11/Components-ppc64el.yml + 3d396ef7d8293620c5160a75fda04d39 6023058 main/dep11/Components-ppc64el.yml.gz + 23ebc600f44eb4973c351a4a324ba219 3925796 main/dep11/Components-ppc64el.yml.xz + 64acc85d1d2ce3e3dc551ae85e80ca57 17735785 main/dep11/Components-s390x.yml + b7f851e780c93532c1707895dfa22474 5976062 main/dep11/Components-s390x.yml.gz + 117c2f52a672bca008f2c206ad8527a6 3894008 main/dep11/Components-s390x.yml.xz + 3f40799bee1a72a060f7dff19efa7b05 13048320 main/dep11/icons-128x128.tar + 6ac207d4fb6b76c25dc59edb50c3bf6b 11409337 main/dep11/icons-128x128.tar.gz + 66ce5f075d189138824e736123711450 4878336 main/dep11/icons-48x48.tar + 260bbc45bfa6b33e31399b4adb3b1f6d 3477622 main/dep11/icons-48x48.tar.gz + 47dea6d08e37b4a5154a072f3ad92cf0 9378816 main/dep11/icons-64x64.tar + 417f46677b9086f9dd0a425f0f39ee31 7315395 main/dep11/icons-64x64.tar.gz + 180389879ed6715b463d05b637e191dc 6191 main/i18n/Translation-ca + 8f8b7ffa4659d4f03b65ed28e69821f9 2673 main/i18n/Translation-ca.bz2 + b4ef33a20d80c576c7b352e96a86e063 1205166 main/i18n/Translation-cs + d70ae6198f35f876b3070d928d5cdba2 323247 main/i18n/Translation-cs.bz2 + 3fa5a10989da6ec5b19b5b6ba161b0bf 20240560 main/i18n/Translation-da + e83f678061ca99aaedd2f20cb75bba77 4411163 main/i18n/Translation-da.bz2 + 9f5077418506388082a72c7023c56f8f 7801238 main/i18n/Translation-de + a57e3821e975f45d21bf2388a190b770 1717951 main/i18n/Translation-de.bz2 + a344219bf0eec9139d5270017ecfceee 1347 main/i18n/Translation-de_DE + 0fe0725f74bb5249f15f30ce965142d5 830 main/i18n/Translation-de_DE.bz2 + 87bf9810c05aba15fb4aca6791feb73d 6257 main/i18n/Translation-el + 002ddfc4187acd8414873fe9f0a6442a 1835 main/i18n/Translation-el.bz2 + 5bac05bd4023fc42332a7628a2c1f895 30255023 main/i18n/Translation-en + cba2b052ca7862064fe87e26014f807d 6236235 main/i18n/Translation-en.bz2 + 0fdd8948881357f49ead0845c7e621c1 2261 main/i18n/Translation-eo + 43bd21f8b5d52b955e509e5893eef37e 1196 main/i18n/Translation-eo.bz2 + 2ad9740f4bf39f163c04bd0b7266c1aa 1325929 main/i18n/Translation-es + b4d4140461b4d6195e3337dcf541554f 317946 main/i18n/Translation-es.bz2 + 2f7f0aac6c4ae5bd9c1499fd612ef996 10093 main/i18n/Translation-eu + 3178567e5f21fe43e4cf1f1a38ed6adc 3914 main/i18n/Translation-eu.bz2 + d1e71d50a88504d6b48c27960250acae 269212 main/i18n/Translation-fi + 9ca11408c191cfc5270f39467ed80f9b 75849 main/i18n/Translation-fi.bz2 + 945a63eed28af4c45fd5185b334b33b3 11857302 main/i18n/Translation-fr + 06100e8db22b6d72d2c466bc85ea117b 2433064 main/i18n/Translation-fr.bz2 + f543980d7c6e8335eb0bb5d00b787418 1427 main/i18n/Translation-gl + 09c22bb0dfa3874802c4e7e4389f2b58 824 main/i18n/Translation-gl.bz2 + 363537eb238e19bd527554a2d1de2533 21069 main/i18n/Translation-hr + 3fbd3535dcc2e805f0283d54bd38f5f3 4695 main/i18n/Translation-hr.bz2 + 5393df220c56a4a92b91b2cac6843067 65236 main/i18n/Translation-hu + 61236a1bada04fd4ab090269498c5393 22243 main/i18n/Translation-hu.bz2 + d8d93a0510fedeb68fbbdae0342520c0 3983 main/i18n/Translation-id + 7542ee230bbc1f2f9f873c265b3b467f 1780 main/i18n/Translation-id.bz2 + 87ba73fdeb9bac4348a4be42b2386f32 24489940 main/i18n/Translation-it + 9c9cd08156baf73f9f088bb97ac00662 4844227 main/i18n/Translation-it.bz2 + 0f39595a0a049759d0d50ead781f73fd 4511401 main/i18n/Translation-ja + 74ff41ba40e19c9ceb4c607b122b7811 803966 main/i18n/Translation-ja.bz2 + 85c4f9ec1e8e2d6faab177ef030ad2aa 11879 main/i18n/Translation-km + 46d57c586859cecf5c1a4470f666000d 2371 main/i18n/Translation-km.bz2 + def6a2d200b3c67b6a1c497524d0a631 2606190 main/i18n/Translation-ko + 3210a7e112a3f29ecf785ba05a78559a 584643 main/i18n/Translation-ko.bz2 + d41d8cd98f00b204e9800998ecf8427e 0 main/i18n/Translation-ml + 4059d198768f9f8dc9372dc1c54bc3c3 14 main/i18n/Translation-ml.bz2 + 904af013a9ba73cd72f71a1ca451be5a 1193 main/i18n/Translation-nb + bf917a722cf4d90cf2f56acb8edb1b31 738 main/i18n/Translation-nb.bz2 + cb57eb70e5645204174caec8edcc4a2b 174332 main/i18n/Translation-nl + ad8c86dde21a892ff20203dc71eb981c 47973 main/i18n/Translation-nl.bz2 + bc88d84933fd8ae64ea0a7ba32a1e814 2051811 main/i18n/Translation-pl + 3095483ca3926b759de515651199283a 491993 main/i18n/Translation-pl.bz2 + d1736cf50b7994e7c6ce66962b7f4b03 1074959 main/i18n/Translation-pt + 7f9e024af1c410635fc69db5bf5d090a 272186 main/i18n/Translation-pt.bz2 + c3453467a749e3888da35949b643835d 3306707 main/i18n/Translation-pt_BR + 89726f5a5abac29bd3a6069e27019c9a 802734 main/i18n/Translation-pt_BR.bz2 + b50c9c49ea0a9da73b0a76db38a36ea4 1717 main/i18n/Translation-ro + 22696f68e30228ffbd84b26dbc821f81 982 main/i18n/Translation-ro.bz2 + 52035b6ff376a4d7c38eea8bbd406751 3058931 main/i18n/Translation-ru + d6c7de740e63ee4ce0e2044a0d449804 494782 main/i18n/Translation-ru.bz2 + 2b383f6dbb23852965418241eda484de 5984088 main/i18n/Translation-sk + 04f2970e8de7fc5a090b84ab700cbb23 1304539 main/i18n/Translation-sk.bz2 + cf58326418b53f94289ad593878bfda2 323953 main/i18n/Translation-sr + 096b962e3404fbc28ebfb174e7587136 58385 main/i18n/Translation-sr.bz2 + 366024c5bc4dabb550f8481c2d662611 85612 main/i18n/Translation-sv + 22b0c4eaa8e59ee11318ce2e68953f4b 27320 main/i18n/Translation-sv.bz2 + ced97abb44ee155f744680871aa5a6e2 14670 main/i18n/Translation-tr + 233a8366a334283e9b802cae336ed09b 5362 main/i18n/Translation-tr.bz2 + c8840c6e4bbe54b098d5b589e5d9e08b 3740343 main/i18n/Translation-uk + 7ed20cfd2585b8f77be6e2bab7561133 576766 main/i18n/Translation-uk.bz2 + 2adb559c8ab8415644e43781db4f739a 21882 main/i18n/Translation-vi + 82caa7c535a1c4c7589a7b1647017f53 6510 main/i18n/Translation-vi.bz2 + f895594ce62c202132bbbe9ae32f1bc2 2007 main/i18n/Translation-zh + 3d2be55ee5ef9a79e0db9f90acc449cf 1215 main/i18n/Translation-zh.bz2 + 91e9eec000876a989969a700ac7b3821 425199 main/i18n/Translation-zh_CN + ab34838b3553d042d515eb65f5aa8816 113621 main/i18n/Translation-zh_CN.bz2 + 34208715b80dcbd5fd1b87874a6705d4 39965 main/i18n/Translation-zh_TW + 6ed487c9d90ac9866174796ce73dec77 14859 main/i18n/Translation-zh_TW.bz2 + 0a69fc41352ebfc79ea841ad29eb078c 58277 main/installer-amd64/20210731+deb11u11/images/MD5SUMS + bc4e6587c876f39ed651345fe50c866d 78097 main/installer-amd64/20210731+deb11u11/images/SHA256SUMS + 8521cd018a0e0b50238dab3cf673c4f7 57705 main/installer-amd64/20210731/images/MD5SUMS + bb4d5d5a421f536dcaa3f2e4fc96c1c3 77333 main/installer-amd64/20210731/images/SHA256SUMS + 0a69fc41352ebfc79ea841ad29eb078c 58277 main/installer-amd64/current/images/MD5SUMS + bc4e6587c876f39ed651345fe50c866d 78097 main/installer-amd64/current/images/SHA256SUMS + 148134a15dccb6fa78eaee2f38ef8d91 69049 main/installer-arm64/20210731+deb11u11/images/MD5SUMS + 0849b91ac89c04d1588e4806b74ce7c9 94149 main/installer-arm64/20210731+deb11u11/images/SHA256SUMS + 8544dac6e811bff5ed42e276cf530ebf 68403 main/installer-arm64/20210731/images/MD5SUMS + 7989c6f2e37aeda05d7dfc58de88d7f5 93279 main/installer-arm64/20210731/images/SHA256SUMS + 148134a15dccb6fa78eaee2f38ef8d91 69049 main/installer-arm64/current/images/MD5SUMS + 0849b91ac89c04d1588e4806b74ce7c9 94149 main/installer-arm64/current/images/SHA256SUMS + 743a41e75d4ce1753e9e73d34bd939cf 20678 main/installer-armel/20210731+deb11u11/images/MD5SUMS + 7128616b60701e0728dfa3d919cdc839 28882 main/installer-armel/20210731+deb11u11/images/SHA256SUMS + 6e3afe07880cea11cee1a8ac19ce5d13 20182 main/installer-armel/20210731/images/MD5SUMS + 350c18339820cfa3989e1297c80b9f12 28194 main/installer-armel/20210731/images/SHA256SUMS + 743a41e75d4ce1753e9e73d34bd939cf 20678 main/installer-armel/current/images/MD5SUMS + 7128616b60701e0728dfa3d919cdc839 28882 main/installer-armel/current/images/SHA256SUMS + 6fb806cba0e076bb8f24156c2c0c604d 64380 main/installer-armhf/20210731+deb11u11/images/MD5SUMS + 29951eb72d35c6079b71e0121679124e 92680 main/installer-armhf/20210731+deb11u11/images/SHA256SUMS + 3dca9930d681a0ba4186171684027ec6 64240 main/installer-armhf/20210731/images/MD5SUMS + 869454c4efa0fcddd91e08ab8ccf9d3b 92476 main/installer-armhf/20210731/images/SHA256SUMS + 6fb806cba0e076bb8f24156c2c0c604d 64380 main/installer-armhf/current/images/MD5SUMS + 29951eb72d35c6079b71e0121679124e 92680 main/installer-armhf/current/images/SHA256SUMS + f301669bc99d88f3cb2e9112cd9bcc95 56840 main/installer-i386/20210731+deb11u11/images/MD5SUMS + 5964b7be27130cf6fcab22fb9a03d3ed 76724 main/installer-i386/20210731+deb11u11/images/SHA256SUMS + 8932831dfc7fb479ada48f6936639179 56286 main/installer-i386/20210731/images/MD5SUMS + 0ccfb273991e3302a49093743aa9032f 75978 main/installer-i386/20210731/images/SHA256SUMS + f301669bc99d88f3cb2e9112cd9bcc95 56840 main/installer-i386/current/images/MD5SUMS + 5964b7be27130cf6fcab22fb9a03d3ed 76724 main/installer-i386/current/images/SHA256SUMS + 5e8c9ca66dbac385da3417467cc158e5 630 main/installer-mips64el/20210731+deb11u11/images/MD5SUMS + 9a5de329783be3d608c8022c2681f20b 1026 main/installer-mips64el/20210731+deb11u11/images/SHA256SUMS + 9533fc15e5b64180b5ad78129a5230b2 627 main/installer-mips64el/20210731/images/MD5SUMS + a776640760fbaacfb1681f3abd0fb40b 1023 main/installer-mips64el/20210731/images/SHA256SUMS + 5e8c9ca66dbac385da3417467cc158e5 630 main/installer-mips64el/current/images/MD5SUMS + 9a5de329783be3d608c8022c2681f20b 1026 main/installer-mips64el/current/images/SHA256SUMS + d7678bd9572bc38d4fdcb68b74d65876 630 main/installer-mipsel/20210731+deb11u11/images/MD5SUMS + 3dbe173aa05add65bcc300a89fc19181 1026 main/installer-mipsel/20210731+deb11u11/images/SHA256SUMS + c3a9b6724a2ff5e2abf741f47a7600da 627 main/installer-mipsel/20210731/images/MD5SUMS + 01da3e1833ca954309023210e9b16159 1023 main/installer-mipsel/20210731/images/SHA256SUMS + d7678bd9572bc38d4fdcb68b74d65876 630 main/installer-mipsel/current/images/MD5SUMS + 3dbe173aa05add65bcc300a89fc19181 1026 main/installer-mipsel/current/images/SHA256SUMS + 4d00c1acb2f241a699cd0b61b79fe2c4 576 main/installer-ppc64el/20210731+deb11u11/images/MD5SUMS + 522ae441fe019c8311451e4453993d3e 972 main/installer-ppc64el/20210731+deb11u11/images/SHA256SUMS + 37515f49026f1bc4682fefba24e9decf 576 main/installer-ppc64el/20210731/images/MD5SUMS + 89c70369e7ab670f721a135f055d81a4 972 main/installer-ppc64el/20210731/images/SHA256SUMS + 4d00c1acb2f241a699cd0b61b79fe2c4 576 main/installer-ppc64el/current/images/MD5SUMS + 522ae441fe019c8311451e4453993d3e 972 main/installer-ppc64el/current/images/SHA256SUMS + af66c2c505061a1a8a3a93e38ed384bf 374 main/installer-s390x/20210731+deb11u11/images/MD5SUMS + b795f84533d6324b5db537f8b70837f4 674 main/installer-s390x/20210731+deb11u11/images/SHA256SUMS + 580b19117c2b6c6f2a8ad8aca5132826 374 main/installer-s390x/20210731/images/MD5SUMS + da16ad53b0185c6e48397e05f2efadfc 674 main/installer-s390x/20210731/images/SHA256SUMS + af66c2c505061a1a8a3a93e38ed384bf 374 main/installer-s390x/current/images/MD5SUMS + b795f84533d6324b5db537f8b70837f4 674 main/installer-s390x/current/images/SHA256SUMS + 8f7bf9a9fefb4e9282f241815537ba5a 121 main/source/Release + 51eb144a4572fc782ea30d897d70301f 44710563 main/source/Sources + 97d67ab1e1cff5dc97d618237135e55a 11432302 main/source/Sources.gz + 9cf6b9743840e7c8834512a0a7330f60 8502160 main/source/Sources.xz + 5f624011d3b0a82f23445c2861deac99 17347341 non-free/Contents-all + c64dcd5c2b4db85f729afa8623adb65a 888157 non-free/Contents-all.gz + 57e8cf89abc2e5dadb698b067105dd98 1100508 non-free/Contents-amd64 + 2d28ca58a1f5d756d5910e58e4846ec8 80075 non-free/Contents-amd64.gz + 053d69e15208f2f26f6da352fa22ff1b 503217 non-free/Contents-arm64 + c4efd5de7248f2f17cf63e1ac3ea4392 37848 non-free/Contents-arm64.gz + 934931c768b3299853c7ac8e23b8efa2 95417 non-free/Contents-armel + 8bb21b9463ec9ddcf1e5160516aed195 9294 non-free/Contents-armel.gz + 047bca361ecd9ae9587d19b6e5945573 146124 non-free/Contents-armhf + fea4b0fa26bc58af5264669255ba7b0d 13508 non-free/Contents-armhf.gz + 130644b7bb54d7085e21c814f0880d12 344734 non-free/Contents-i386 + cb1565a5cc61b763330dc04f9e1bf650 29303 non-free/Contents-i386.gz + ecbb1092d2f2e15dba7dfdbcbb6ed8b2 91215 non-free/Contents-mips64el + be5e7ca05883e17e336aeaf4a2c2b006 8661 non-free/Contents-mips64el.gz + c6bd3804f450e67ed92515d728b81854 92244 non-free/Contents-mipsel + f2864ee4f7d69645ffa6deb679e6853c 9013 non-free/Contents-mipsel.gz + 220c0b9ef60191fd0da813388341ff7c 784438 non-free/Contents-ppc64el + 72876d4b689b49244cd1d8ce6caeced6 54577 non-free/Contents-ppc64el.gz + f243f922bf7142bd1b85064a49645eff 74537 non-free/Contents-s390x + ccb803a4f3c7834caa06d95bc771c76f 7367 non-free/Contents-s390x.gz + 4f0192d0671824bf6b6c246316b5e092 10810052 non-free/Contents-source + cb9b016b590a26d1309a03df6c3cca1b 1064267 non-free/Contents-source.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-all + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-all.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-amd64 + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-amd64.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-arm64 + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-arm64.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-armel + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-armel.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-armhf + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-armhf.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-i386 + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-i386.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-mips64el + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-mips64el.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-mipsel + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-mipsel.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-ppc64el + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-ppc64el.gz + d41d8cd98f00b204e9800998ecf8427e 0 non-free/Contents-udeb-s390x + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/Contents-udeb-s390x.gz + 61ca55e2733e57f22216992c3b79a929 189039 non-free/binary-all/Packages + 36d8a3ae5d607dde5257e4a546d6a6a0 50997 non-free/binary-all/Packages.gz + 8514d2db7fb19bc2ef94689db5202d71 42824 non-free/binary-all/Packages.xz + 0b5211b51fc877b5279f0b1792b208b5 122 non-free/binary-all/Release + 00dfbb56295a1a61662aec7ad954313b 542476 non-free/binary-amd64/Packages + d8e734fcbb765c45788f5d014576fa0a 121003 non-free/binary-amd64/Packages.gz + f04a57a644dd3e43faa199722afc7dee 96176 non-free/binary-amd64/Packages.xz + d9bc3e50f6051d8841c632d7e27bed3a 124 non-free/binary-amd64/Release + 3b1d1b2819b7e1d5696dfd0cfeed541f 379784 non-free/binary-arm64/Packages + 565651bc1d27fd4fe4649d83eefde851 88157 non-free/binary-arm64/Packages.gz + fd7e7a29726f14b9ae21da71cf0ca557 72276 non-free/binary-arm64/Packages.xz + e2208586372b45ad74f6e04f3b6cc795 124 non-free/binary-arm64/Release + 6e9ad48e25c2ca2f209589f7f2154a11 227975 non-free/binary-armel/Packages + f84ccf6860868b05b6f085c4841ca60d 61785 non-free/binary-armel/Packages.gz + cf51305cee56b26ff5fb0443a233fbc9 51588 non-free/binary-armel/Packages.xz + 6c5b502d7b20b8d202b5b9f1a7b81037 124 non-free/binary-armel/Release + 7f4d1a6325db3db4d07fd107e6004c80 259198 non-free/binary-armhf/Packages + 66890db0dcdbc4e03a821e3b1ebe61b3 67349 non-free/binary-armhf/Packages.gz + 4c76459ba52afca6a95887fef1707462 56004 non-free/binary-armhf/Packages.xz + 4814905d241056cf56d3fcf0678c6af2 124 non-free/binary-armhf/Release + dbe2e0866045beb474903d92055c2fdd 421250 non-free/binary-i386/Packages + 501818c93858664f40354f3b9e50eb89 96578 non-free/binary-i386/Packages.gz + b891adebc3b32dde24be5f9924c96174 78372 non-free/binary-i386/Packages.xz + 0ce77cedefd62e816f5d4291fb4c8ab0 123 non-free/binary-i386/Release + 4725a4a3a357aac0ad0948ca0ed8850a 225548 non-free/binary-mips64el/Packages + dd0d0c5070eff4a81cae4f4c28be6f88 61129 non-free/binary-mips64el/Packages.gz + a79de301a0a007f1a513a2b3761e8521 50900 non-free/binary-mips64el/Packages.xz + 3f15a0a2c4007635eece06c7cbe13bc6 127 non-free/binary-mips64el/Release + 5a59e97246e0d2eaf2a32096c669aa44 226204 non-free/binary-mipsel/Packages + e95d0001d4d9752d9e955c12d87ec18f 61198 non-free/binary-mipsel/Packages.gz + 2a4c2ef99aa27dd94e53c5346ca6182c 51152 non-free/binary-mipsel/Packages.xz + fb83268a8eb8f2e1082c89ae30340f0b 125 non-free/binary-mipsel/Release + b0d017cfe04f60da3df6ff288babb7cc 405033 non-free/binary-ppc64el/Packages + 42d87582aa8aa3e603e3171b87302618 91696 non-free/binary-ppc64el/Packages.gz + 40adfeeed209b998a4e7d8cfd94df569 74316 non-free/binary-ppc64el/Packages.xz + f03072503127a9a01c706f06c8e8cb57 126 non-free/binary-ppc64el/Release + d0ff5025c5ccee136909a4bd25ab48a1 220612 non-free/binary-s390x/Packages + d4df31c20555df9b42fef388d4f3ce97 59881 non-free/binary-s390x/Packages.gz + e6206b4384ab8d00247ad12de07ffabb 49980 non-free/binary-s390x/Packages.xz + 2e3c168976fec4ec96ac2e946aea5358 124 non-free/binary-s390x/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-all/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-all/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-all/Packages.xz + 0b5211b51fc877b5279f0b1792b208b5 122 non-free/debian-installer/binary-all/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-amd64/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-amd64/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-amd64/Packages.xz + d9bc3e50f6051d8841c632d7e27bed3a 124 non-free/debian-installer/binary-amd64/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-arm64/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-arm64/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-arm64/Packages.xz + e2208586372b45ad74f6e04f3b6cc795 124 non-free/debian-installer/binary-arm64/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-armel/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-armel/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-armel/Packages.xz + 6c5b502d7b20b8d202b5b9f1a7b81037 124 non-free/debian-installer/binary-armel/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-armhf/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-armhf/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-armhf/Packages.xz + 4814905d241056cf56d3fcf0678c6af2 124 non-free/debian-installer/binary-armhf/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-i386/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-i386/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-i386/Packages.xz + 0ce77cedefd62e816f5d4291fb4c8ab0 123 non-free/debian-installer/binary-i386/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-mips64el/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-mips64el/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-mips64el/Packages.xz + 3f15a0a2c4007635eece06c7cbe13bc6 127 non-free/debian-installer/binary-mips64el/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-mipsel/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-mipsel/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-mipsel/Packages.xz + fb83268a8eb8f2e1082c89ae30340f0b 125 non-free/debian-installer/binary-mipsel/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-ppc64el/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-ppc64el/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-ppc64el/Packages.xz + f03072503127a9a01c706f06c8e8cb57 126 non-free/debian-installer/binary-ppc64el/Release + d41d8cd98f00b204e9800998ecf8427e 0 non-free/debian-installer/binary-s390x/Packages + 4a4dd3598707603b3f76a2378a4504aa 20 non-free/debian-installer/binary-s390x/Packages.gz + 8dc5aea5b03dff8595f096f9e368e888 32 non-free/debian-installer/binary-s390x/Packages.xz + 2e3c168976fec4ec96ac2e946aea5358 124 non-free/debian-installer/binary-s390x/Release + f7208886e345a2c1c5681b7bc1f891f3 278293 non-free/dep11/Components-amd64.yml + ab8bcc71919bb29e6a367d9058dc0125 29634 non-free/dep11/Components-amd64.yml.gz + afd21b4c476c6b604c4f998d90383234 17904 non-free/dep11/Components-amd64.yml.xz + 71e3cebf69c369e3d4e6b64e48fe037b 271451 non-free/dep11/Components-arm64.yml + 4b40bf8ff6579f425fd308cc4f32bb26 27686 non-free/dep11/Components-arm64.yml.gz + 04fa2b6c4dc8d23f6ee6334754b725df 16392 non-free/dep11/Components-arm64.yml.xz + 678290cc20fe4c69fac625c25f48577f 271451 non-free/dep11/Components-armel.yml + b76376c24cdd9bb014e63503830766f8 27606 non-free/dep11/Components-armel.yml.gz + b431acc1b0f700a021a3ab1305bc3c33 16448 non-free/dep11/Components-armel.yml.xz + 7f659804cad02381ed7735779c211771 271451 non-free/dep11/Components-armhf.yml + 0221ab3c0654617c6de5d2b74eac7b15 27691 non-free/dep11/Components-armhf.yml.gz + 2df1dfb4d502d5c01f744bac99e8a0bc 16364 non-free/dep11/Components-armhf.yml.xz + 1422b7cb028418049315374e46dcbf86 280613 non-free/dep11/Components-i386.yml + 7a014ddef58173efeb07ce9d7b866331 31098 non-free/dep11/Components-i386.yml.gz + ee2f702d30a2274d969a8e9044da54f2 19156 non-free/dep11/Components-i386.yml.xz + 2f39022b38ebd28b86acd148ad0389d2 271451 non-free/dep11/Components-mips64el.yml + 5e839450348a20fc9f81cdc9dd0b9663 27765 non-free/dep11/Components-mips64el.yml.gz + fbf40f634081acbde994e89d8731d159 16380 non-free/dep11/Components-mips64el.yml.xz + 4ff7e301bb5eaab539783f39c24b421f 271451 non-free/dep11/Components-ppc64el.yml + d7c37af104343f2eb2b10a0980c96661 27592 non-free/dep11/Components-ppc64el.yml.gz + afabe491b91df1be19287ea4e978e7aa 16576 non-free/dep11/Components-ppc64el.yml.xz + 05dc5f141a7ca96f1aae6d571dd37361 271451 non-free/dep11/Components-s390x.yml + 4a5b9e250991cd5d661db03f4bebefa8 27558 non-free/dep11/Components-s390x.yml.gz + b0593a88d870f066f1a83dfb382e09c5 16356 non-free/dep11/Components-s390x.yml.xz + 40dd67e0e1f81416405be5c0dc8ee47e 8192 non-free/dep11/icons-128x128.tar + b117213e4fd39f9c75c1699ebaf3d610 2394 non-free/dep11/icons-128x128.tar.gz + 08a465949d80332d065e6f4ec8459930 4096 non-free/dep11/icons-48x48.tar + 49466a3c36fe0d0cbb5940896da60960 741 non-free/dep11/icons-48x48.tar.gz + 5d6e61a41610797276e5b6f16d60f7e1 36864 non-free/dep11/icons-64x64.tar + 0196f7b979db4111a6d9b988e63101a0 27667 non-free/dep11/icons-64x64.tar.gz + 7f946a2e5a99f9d293560f7ef71fe24c 565058 non-free/i18n/Translation-en + 8d8b8f6380d6f186018d4384c0a48c80 92546 non-free/i18n/Translation-en.bz2 + 231eaa13df73ae9c8561fd24c43350bd 125 non-free/source/Release + 8af38c92a2418a501f7b3dbfc3c36b6d 360713 non-free/source/Sources + 7fbedbc3c66f131a0580e01a0df1e712 98365 non-free/source/Sources.gz + a501ec7ab698a8389541d603c7b2e3d9 81088 non-free/source/Sources.xz +SHA256: + 3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63 738242 contrib/Contents-all + 3e9a121d599b56c08bc8f144e4830807c77c29d7114316d6984ba54695d3db7b 57319 contrib/Contents-all.gz + b824177c8dd3e8829d619b5ed5b3a11f9ba79c5c6f89be4c3242716dc5533e11 787377 contrib/Contents-amd64 + a84d8388c2399459ca31521d67f71def340f21eadeab4d887cb2744e8990cba9 54657 contrib/Contents-amd64.gz + 37981287ca5edac1698f62bf7a9821a6e2561cd6eb404523699c961e4992fbc5 370971 contrib/Contents-arm64 + 4c2a8473d8a43ffb361a83ed7a91d0248e476e5d9003e1ebb10c609a8172051b 29568 contrib/Contents-arm64.gz + b4985377d670dbc4ab9bf0f7fb15d11b100c442050dee7c1e9203d3f0cfd3f37 359292 contrib/Contents-armel + f134666bc09535cbc917f63022ea31613da15ec3c0ce1c664981ace325acdd6a 28039 contrib/Contents-armel.gz + b5363d1e3ec276a0cb10bc16685bd02bdc330719d76c275bebd344adaa91583b 367655 contrib/Contents-armhf + fc4edd280f2b254dbfa98f495e5f4ca6047ec9a1539ccb8754a1f93546ea32b5 29236 contrib/Contents-armhf.gz + 77d465435ba8f5bad03b76624835f91e9ebf3bb09b124ab1a06e70c8b2629b30 407328 contrib/Contents-i386 + e4a82b31ac7b5b139fd3bd93ad466de75f7bf7d54410967253044895e41c36fb 33556 contrib/Contents-i386.gz + c0efa60eaa3b47bd93ca71220c6fc734d54b257e16bb6dd8dde43ca722f242dc 359402 contrib/Contents-mips64el + 4fccf5298ef664c2de3dc7eeb203eefa3bf8ec82b95b1c696b856a43af35e395 27962 contrib/Contents-mips64el.gz + db2388b4b8d300fdc265fe064288a8de5f69958b06ed6cfeff3b8528e719015b 360549 contrib/Contents-mipsel + 27db69688406433748363f4a70cac108f29b99555a6d5dc3eaba6b2e8b526dfc 27942 contrib/Contents-mipsel.gz + 60e0693cc2eadc7b19be74ada9157bb9f62de4c173fa60faffa1818c7f4d8504 370782 contrib/Contents-ppc64el + fa5dc348a594f6824ea86b30f90f14184c3b017b3e1e6add5d0294bbda28db53 29413 contrib/Contents-ppc64el.gz + bb1fdc3fafd28760f57d951e96a150e8ec7d6b0fb75443de93f08a61ffbd7042 357860 contrib/Contents-s390x + 009373ff8cde80de63a4303b8c6eab79af34d6c2c0c831d1b38e1f9329c396cc 27518 contrib/Contents-s390x.gz + e64bf6980568bf49dfac0adbc539efaaf2d5e94301915b7c30aae16dbc417e33 6726318 contrib/Contents-source + 5b5d761040282e0d2ce14fa2efa5d82740596833a97eb7d5a0ec28c90cc19a36 471162 contrib/Contents-source.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-all + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-all.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-amd64 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-amd64.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-arm64 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-arm64.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-armel + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-armel.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-armhf + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-armhf.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-i386 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-i386.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-mips64el + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-mips64el.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-mipsel + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-mipsel.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-ppc64el + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-ppc64el.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/Contents-udeb-s390x + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/Contents-udeb-s390x.gz + 8b865b9bc2257cb7fddf1da15f10a34a59f03473f470f5ccac1e98e5a4c0cecc 103359 contrib/binary-all/Packages + a718b844a3c0e364da1506764808e11dc6684031bc841879812e08f482cbcee5 27400 contrib/binary-all/Packages.gz + e3ea4772b2738f29f971610fa9a9e6e7b6c45c6d32f77d2edcb8f13094aa918b 23852 contrib/binary-all/Packages.xz + 362ad0ebc9b38f752f5eca5b7ec263d41b6d533db47e3f049beff6c2f17f51da 121 contrib/binary-all/Release + 6633b251325e687e3cdfa9441e383e68904225fc08c7824169d62a9e38a8b14e 232262 contrib/binary-amd64/Packages + bf77b15e68c5bfd7267c76a34172021de8f10f861f41ebda7b39d1390dd4bf9a 61020 contrib/binary-amd64/Packages.gz + 8c200092ac7a716e8983b1733ed7b83528b0a3f03a01d0abda0c8c5e3a54764d 50396 contrib/binary-amd64/Packages.xz + 87c1181b180937cdbf05f348b6e31bae98f3c716f5cb9432f64eb6e0519c3d8e 123 contrib/binary-amd64/Release + 9a5b1c6094ab03d3fac5e27fecb970149b814153efbae2c945b31cbd8a285b38 181252 contrib/binary-arm64/Packages + b24a5fc9eac72cc4e92b105b05131a6fe0cff17716891a47b798d6cd49739de9 49104 contrib/binary-arm64/Packages.gz + fb5412dcd4d8d40f515cace6033be0098174168189bbc77269547fbf7b9c98e8 40832 contrib/binary-arm64/Packages.xz + 62c9557f7df593c922daf5caf765109d6f3624382c9446d20351c1a21183134d 123 contrib/binary-arm64/Release + cf8c193fb462e51c7ec63f6f25cd95e138d56cfd06c43f619f2c624eda526901 163450 contrib/binary-armel/Packages + 40e36af7b5d368a60debd4db875ad71c28a1ccb7dadc3b48eabfe30d296176a8 44618 contrib/binary-armel/Packages.gz + 3fa8647875d490d926729b24ecb0c4983d1c3223a345b8c5aba3ddf9f117477f 37344 contrib/binary-armel/Packages.xz + 22b5f67762d18809f6de4da249d29afb2571e17a414707bb4201bd4868efb034 123 contrib/binary-armel/Release + 4e27bf2cf08d1b6c0b37ff2ff4ba928e919222b65547b17897796815338d71b4 175958 contrib/binary-armhf/Packages + 51e59ac4f6683f3bd170346081531e3e0082dd6e790ee75c9a2b3b0fbd561f8d 47820 contrib/binary-armhf/Packages.gz + ee2f13d61ee994300c0c2a874d40376d77920d73c55dfccf0972f7e91e838fd2 40084 contrib/binary-armhf/Packages.xz + fbef16b450a097561b3ffc77e4a1f06ede4bcbf275a18b04676d6805ece01877 123 contrib/binary-armhf/Release + c1a21605d6fba3ae51c7d6fe219034428d4d32aa322f15e030f04a4b3f9e2952 203906 contrib/binary-i386/Packages + 2e46f9513977275df10e217d55e7b8bea69bfebb4f7add5dd419668afea80b2b 54303 contrib/binary-i386/Packages.gz + 7b85c47bca49b84aa9e2befc37abe36a6b0f2eafb5401c7deb3821ea11807834 45180 contrib/binary-i386/Packages.xz + fc79e9cc2497e6aaaff8ba5240b5f6ed95b2baf78c13f9289eeea438be28ea64 122 contrib/binary-i386/Release + d74a62135dafd8aeb3f99d924619d8446a30e830fdec947e781a6714b7ab1fa5 163915 contrib/binary-mips64el/Packages + 8ade72fbdeb9108227d9386e36acdf4f0c19503fc8a09ba4d478eed112374dda 44741 contrib/binary-mips64el/Packages.gz + b9220b76d33214cd2317e021c589f385af40367914ecbd2c439b777640f861ba 37380 contrib/binary-mips64el/Packages.xz + 1f4cb26b6a540ad01b0c1dc5d17f4d89a98b09e8e867d9da67b0722f56e7ae72 126 contrib/binary-mips64el/Release + 00a497aecb13a837e3daf82843904686dfcc43352f603790661905aef5f824ba 165055 contrib/binary-mipsel/Packages + 686014ae5ce41f0738a4b65d6a2989ea16adc144caebcf2d5b50eb2d51dc45fc 45042 contrib/binary-mipsel/Packages.gz + 806288d0ae1b8863e5a11c4cdf13f66494130ba68f8d0598560a930c566f26a3 37724 contrib/binary-mipsel/Packages.xz + 75a6a20a19ad03a3a2a20f8bf08dd883f3011b39dc5bd2351de3d09c93a10153 124 contrib/binary-mipsel/Release + b510624157855f62e143444f11347fbc5cefea46c016665538ca5d9169a80689 182035 contrib/binary-ppc64el/Packages + 0faf1ebfe4b47a51fbced574df7c20f20876f8524c16dd94c5f5ec090aa217cf 49196 contrib/binary-ppc64el/Packages.gz + a82f418f3cf183e78eabf73dd218be7435078c3b86a35400c448b7f5dc5dee56 40856 contrib/binary-ppc64el/Packages.xz + 0ce970f748d8395848a61df2e9507d7bf3766a42cfaafdf2f1bbe0fc4e6a102d 125 contrib/binary-ppc64el/Release + f4acb775852717de95275ece30e81332132ba02fdad8e59acb83ecab630fad05 162658 contrib/binary-s390x/Packages + 3e18ef6fd4cecd7e39edd3a676af4c870e8f837b4b854ebf02c230c910be4d00 44315 contrib/binary-s390x/Packages.gz + 73cb79c3e73e6f0d538322dded240460e62fb950e05dcb54be22dd0110d03582 37132 contrib/binary-s390x/Packages.xz + b9cca045c916225f687ebb5fe54d9b7f8fc0135166b52f3fd23d472d445f470a 123 contrib/binary-s390x/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-all/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-all/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-all/Packages.xz + 362ad0ebc9b38f752f5eca5b7ec263d41b6d533db47e3f049beff6c2f17f51da 121 contrib/debian-installer/binary-all/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-amd64/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-amd64/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-amd64/Packages.xz + 87c1181b180937cdbf05f348b6e31bae98f3c716f5cb9432f64eb6e0519c3d8e 123 contrib/debian-installer/binary-amd64/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-arm64/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-arm64/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-arm64/Packages.xz + 62c9557f7df593c922daf5caf765109d6f3624382c9446d20351c1a21183134d 123 contrib/debian-installer/binary-arm64/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-armel/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-armel/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-armel/Packages.xz + 22b5f67762d18809f6de4da249d29afb2571e17a414707bb4201bd4868efb034 123 contrib/debian-installer/binary-armel/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-armhf/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-armhf/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-armhf/Packages.xz + fbef16b450a097561b3ffc77e4a1f06ede4bcbf275a18b04676d6805ece01877 123 contrib/debian-installer/binary-armhf/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-i386/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-i386/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-i386/Packages.xz + fc79e9cc2497e6aaaff8ba5240b5f6ed95b2baf78c13f9289eeea438be28ea64 122 contrib/debian-installer/binary-i386/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-mips64el/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-mips64el/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-mips64el/Packages.xz + 1f4cb26b6a540ad01b0c1dc5d17f4d89a98b09e8e867d9da67b0722f56e7ae72 126 contrib/debian-installer/binary-mips64el/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-mipsel/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-mipsel/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-mipsel/Packages.xz + 75a6a20a19ad03a3a2a20f8bf08dd883f3011b39dc5bd2351de3d09c93a10153 124 contrib/debian-installer/binary-mipsel/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-ppc64el/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-ppc64el/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-ppc64el/Packages.xz + 0ce970f748d8395848a61df2e9507d7bf3766a42cfaafdf2f1bbe0fc4e6a102d 125 contrib/debian-installer/binary-ppc64el/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 contrib/debian-installer/binary-s390x/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 contrib/debian-installer/binary-s390x/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 contrib/debian-installer/binary-s390x/Packages.xz + b9cca045c916225f687ebb5fe54d9b7f8fc0135166b52f3fd23d472d445f470a 123 contrib/debian-installer/binary-s390x/Release + f0a51e6d75f883bdecf739b214104a17dba111de8b42022f6b8b053870c83851 119152 contrib/dep11/Components-amd64.yml + e14a1bb3690a18ec7c5b7997fabf4d8d4fa633efdf84a25e071a1f62a2c064b2 15579 contrib/dep11/Components-amd64.yml.gz + 58921318632f77413bee8d9e980689f8f139eb1169b5ce201da06e6f280d485f 13564 contrib/dep11/Components-amd64.yml.xz + 26538634f90cd6f04a6be602151fa6a098075c3013b66a81439a7bbdbfaa40f5 113437 contrib/dep11/Components-arm64.yml + 840908ab753dba952e073216007f93d351577792911dcc09a15a16abfc32c8a7 14251 contrib/dep11/Components-arm64.yml.gz + 3afec5908036aa2d47b9a9a33c13eca12bba1aaf8d8bbb06ffb1627e93f6526f 12480 contrib/dep11/Components-arm64.yml.xz + fb35649f6c32b71b9d85388c2c238011161c250df5c62e2c4d3446e369dced4c 113437 contrib/dep11/Components-armel.yml + c305f1c0826e0414bbf36524d8b0fc2723ffc0fb222275e1e1728914fc334c75 14029 contrib/dep11/Components-armel.yml.gz + fe15a53774801f8d9cb04aa8324cbdb9d741ec75ae0999e033873458bd6160b0 12524 contrib/dep11/Components-armel.yml.xz + 0ed24b6d7ff891c82697497dddfbbbb6818c168c55b41ae710e9cc9240d0d9b2 113437 contrib/dep11/Components-armhf.yml + f5260cdac915ff5eba0a48757c93f8f8b6421a673e641285f43d83f62be3eb8c 14127 contrib/dep11/Components-armhf.yml.gz + db97becd2ab6a05bcef05d824b89080a1e7c03a69735df3bf5945f6989a9e504 12480 contrib/dep11/Components-armhf.yml.xz + 9adf35216113140c31c2e9c169a3eaa465044f41f8803afaac955c467a1e5a49 118972 contrib/dep11/Components-i386.yml + c1d4ea9c0ac26f2b62d45c8c595ec9a5bc1c737b50634d7f86a4bfac17c9b180 15566 contrib/dep11/Components-i386.yml.gz + 51ff60d5f02b46e08acea4054484f5c66d721c19beff4857cb2570f43e881a69 13560 contrib/dep11/Components-i386.yml.xz + 50b6970af7de299a90ac651cceb6cc011e8d165ea0701f7b1c9daf6c1be485f0 113437 contrib/dep11/Components-mips64el.yml + 78aad16ddec6b18d30ce4e20f52008f72efc78ba55688fa462741f4bb514043f 14056 contrib/dep11/Components-mips64el.yml.gz + efb0fb003bbd3997128bef56f12104872604fad320b38fd99bca25e68210d98e 12500 contrib/dep11/Components-mips64el.yml.xz + 05c2268c20e748baf8da20f7169918e2f6dcffb6e4f6dfc22829607cec7ea564 113437 contrib/dep11/Components-ppc64el.yml + 19f600014e245e7d07762b7f07d8de6884b1208a280a19274e56b4174931082a 14219 contrib/dep11/Components-ppc64el.yml.gz + dc8b525d7043ba3a85154ad39d0c809e7215c5b2f3865efbd94ff3daabe54810 12496 contrib/dep11/Components-ppc64el.yml.xz + 5d43b650d261ac23815d98e9a4f644d56f4113e63f8a42b1558ff1c82e925d2f 113437 contrib/dep11/Components-s390x.yml + c1811e0538dad96441a4172e661b9ef7fca9c05d86c4b157a66046bf49aa70e1 14050 contrib/dep11/Components-s390x.yml.gz + 42356b4c04801189947748d6fce6e28e356a114869a7895e4921a3b4901e678c 12488 contrib/dep11/Components-s390x.yml.xz + 641e9a50f98d7e4921102164e7737b095c9faead09f6de4459086b598b3bf0d0 271360 contrib/dep11/icons-128x128.tar + 34b531c5292651ac5a18d0477bb8cf1420f3d969ad73d45fd596641d768b853d 195507 contrib/dep11/icons-128x128.tar.gz + fa3a19603046c258e647b0c1fcdc6110f0b5c1f2801ee950eb1261e8c02e03d6 83968 contrib/dep11/icons-48x48.tar + 28a6f153e56e9b567cc7fc03d6faa6dfb8480ee3f36e0c8d9646e4de3898480b 47168 contrib/dep11/icons-48x48.tar.gz + d882fc33534a8677ed8d3ecf81f7a076fa57e8e8135bf586f8af20371edb195b 138752 contrib/dep11/icons-64x64.tar + 45c8eda64d05f1feee0040809128760f9489665d66bed0502cb179fe0ec79f6e 93294 contrib/dep11/icons-64x64.tar.gz + 094badc305c90db005324c484a55d88f14dfc805aa429856a5863a96518a88e8 192685 contrib/i18n/Translation-en + ce7d3d607194cdfabf421c313030e88876ee899d5cd01f5b023cfdc0c0ed0f40 46929 contrib/i18n/Translation-en.bz2 + 5f3dc68e110af5da79e43d599d2cd25d58f5e6900aca1bf505069a371eb67f1d 124 contrib/source/Release + b64c9f981227bb6bcfeb4e40e305a67e0252ce015337f49fc58ec879d1dea69c 178818 contrib/source/Sources + 94b3cd37b319d4b9be5d20c33688e62dd9d4f8f10491b3aca19a6715a09a8c98 51518 contrib/source/Sources.gz + a98bb40a591dc6082bc2f99a13e0379b37dd3204b51bef6f2dcd616a5761833e 43180 contrib/source/Sources.xz + 9c375caa0fc7539c71153762474e6259745b24c5e503a7ac998d6fd3071f20e1 481615325 main/Contents-all + 11c0802137b894999e67167f8d1900482d68b838a8150162b5fe040f0467f538 31274162 main/Contents-all.gz + 91c97caac7b016358fe62ebd50ac1ecf59f8e35f1a979c0e0d51412ad523a61b 129675985 main/Contents-amd64 + e377fbfb2553f4de6c6bae5f32edb6a0ba3070f0be3e3d7d5eb6fbf190ef9cbf 10326393 main/Contents-amd64.gz + c7ccd1add2a9f0dc5b3281db14df1dc924dba996ba3c3a5413379fc4c9d574c7 123035834 main/Contents-arm64 + 25ec01615c6644fbf69f7b382e299da555ec1b5dc43618911b67947517e343a5 9886926 main/Contents-arm64.gz + 221b74cb99b8461b4d6516db012a056f04d6066a9f990a70f57cb0d62bc1a850 105223777 main/Contents-armel + b62671ed3f8cbdb35e552dc4992dad49fac631a646125fb65b5e94f21518a35c 8750503 main/Contents-armel.gz + 667af3699a25e3ea82e126e3026aeb18d1d5660e60859599041b74a2c6cf8b06 114286689 main/Contents-armhf + d8a27f74857eb6ae1b1f77738fa2a46dcc0538060f2c2189ebc9f00497cd7e69 9360391 main/Contents-armhf.gz + 63d318337b41d97a3265d68fa5fe927c040905448b6702f411f6587e31757614 129640940 main/Contents-i386 + b0317ff818b7c217a448336b5362508e219cb30fd447281c7f875bffc78f40f3 10257245 main/Contents-i386.gz + f3220c7691f5e11d94321ac8fd705c2cfcd5162ea4d7f95b29f6df2b9fd8448e 111617889 main/Contents-mips64el + 4faf5f79a8445d9dac414d4b1dd1e016038dd54b0012f1e86eb197583c972121 9088306 main/Contents-mips64el.gz + f530b1920ad59cb99c5f5cf06f20efbd92ce9c927bd7535dd2923b5a35da418c 112502390 main/Contents-mipsel + 6956cc11811107c9d0983b57654b0609f70510f08ef733925c6aa6e9c2a29892 9170633 main/Contents-mipsel.gz + f041a6d1c19ce5cfc63b33ff91de36b499ac4b4ad284824a77f832de662ae02b 116637320 main/Contents-ppc64el + 2b714f1aed023d9ca0b7f0bf2ccb1f16f9ec22215bc933b6afda7c925e0b5706 9408238 main/Contents-ppc64el.gz + 970157274382c3cc4c8ca8e469ae357b09c2a639a4463b10e9d0753cd4110f04 104233350 main/Contents-s390x + 88fe9bd9d77c2bff037a52247d7ec8c66d942cebdd9a2f90842fadba173b2ef4 8763164 main/Contents-s390x.gz + 81dd2688f84bc862d5ef845c4d3b353d903490b752c3fc2ce0ea0dc129f1031c 708782673 main/Contents-source + 8e22364c4e1c259f79ad7e848a88fdefee37eecae71b6fd42ba9748d57795298 74632984 main/Contents-source.gz + b608454319ef4b0fe4039edfc66a7235ca0e251dae8834f597371bb92df4e9cf 157474 main/Contents-udeb-all + 9d80e0102f0222b994e75c9abc6e162b85447eeb519f9607df385d0bd62e0062 13511 main/Contents-udeb-all.gz + 23c661771f8d0b36b44930dfe9ad3fe069c1e20d51503992e6b729e0f715050a 476840 main/Contents-udeb-amd64 + 41704192a5a5841c00ea3696e9e70a211f71e94ae139ece4754851fcb42e1b53 36123 main/Contents-udeb-amd64.gz + a3742448eba57b5c691f677900da4de18e71573617d73af37a42b3407ee0a896 508817 main/Contents-udeb-arm64 + 557b0cbc499fac365f9d79b12911b3b4280a019c4887b3e333657960322288bf 38016 main/Contents-udeb-arm64.gz + e7e1e62179180f3345d5bb72ee057801c1824f6ab4bf2bfc4ada181a59ade838 323083 main/Contents-udeb-armel + ff109a94fab51922b7b7c63afb579c22249514d5d8dec605c87be67e300457e4 25444 main/Contents-udeb-armel.gz + f30804cc4255117023b1bb4603cd711236b11e63b967d2ec56f00f16f7419a88 579793 main/Contents-udeb-armhf + ff2c63349a0a4aef3592ef3d862030691e9a1445468ba0b25074641e8f2e3b23 43537 main/Contents-udeb-armhf.gz + 9dead65f51741a8cdeb6f3b13efae146aa7723576b22699e58152a02103f1266 750959 main/Contents-udeb-i386 + 76ef6f1958241a869ca171df0be16a137e267ce4fff644d04b7e12e2becd9dc9 54470 main/Contents-udeb-i386.gz + d2cf131b2c07956640869d2e024e4d106e76a6c4b02a5f7e7f8db57b5498dd67 760308 main/Contents-udeb-mips64el + 02af90745e04b5ad0c7b4f0a40446a88b24deb91d05111e87e317a8032ab3d27 52564 main/Contents-udeb-mips64el.gz + 161126a66661f77b2b12b58a549269d805ecd0be12c4be9049b15c23fbf514bf 759984 main/Contents-udeb-mipsel + ca2231442e2bad1939a402fc5a20487c12a6534c86cceb133a7eaa3123225ab6 52894 main/Contents-udeb-mipsel.gz + 0702c3e8adebcb092764a395c3a8e15681e32b96036e186b1f950888932d3035 401645 main/Contents-udeb-ppc64el + 88d6a59130e9a051994153a275f2b60ada8573929ebd2c1690e723c1e27b0768 29327 main/Contents-udeb-ppc64el.gz + aeb0e8df0cc171b03145004c7d627c42e027ace315d6ff5608e9183d3245b26d 258318 main/Contents-udeb-s390x + 66906c689c885981572a3653663be5731535404643d4833085c5112989ccb4da 21034 main/Contents-udeb-s390x.gz + c38906ac79639c3d9a2a5bbd56bf385ed6eabfb3c815a003657c24746ce7c097 20549447 main/binary-all/Packages + 75f0e9e48f1b06f50ef68463bef3ca1394395451a680cf756cb1bae6784026ad 5221543 main/binary-all/Packages.gz + e8a7042e2977292564e883d8d55b845cfaf2afbf71e6c59110e16138ffb7a5d6 3872680 main/binary-all/Packages.xz + 20620429411053e7287c9996f558ff21cdebfc562f74f4e1d58429741da9b242 118 main/binary-all/Release + 090f84be0ab712372b36339b19b509c275c143de8a9964795f6def7c330b4d36 45560272 main/binary-amd64/Packages + 205a086143567ae53436802f62a1d4ca5419c89f368c62a28f92ea421a95263b 11091667 main/binary-amd64/Packages.gz + 86d4b153049aafdbf34def877cba528789ce697951a4c97ebab2ee21c9090b6d 8066552 main/binary-amd64/Packages.xz + b0b4759201a7287de4017b57d138c16fe6bc420af872dbf875be5cb33dd9f466 120 main/binary-amd64/Release + d040fa126638c103fe0fabc176694da6f8db622d016b263734c094bc27c5ae52 44839134 main/binary-arm64/Packages + 8248b0cbb7cf5e29b52ccf1100d152e5c266ad3320502244f91701127af2436d 10936444 main/binary-arm64/Packages.gz + 14fd8848875e988f92d00d0baeb058c068b8352d537d2836eb1f0a6633c7cdd2 7955944 main/binary-arm64/Packages.xz + 71b7d9a8245a77455b793afcc1cf6fa1ca76346e189d0b4c4d524281feaf490d 120 main/binary-arm64/Release + 9207accd9321a3c1a41db8296f11e1c4748c40496c6e932c4549c1749de23ad0 43472829 main/binary-armel/Packages + 34b5355cf486a3716e1def38ea196605ac849159711a75441e6f96925e925225 10686811 main/binary-armel/Packages.gz + 26daca5c8a58f73cb2baae6bfd3244e6fcb6ebf919e911b9438c0e20c552a820 7779872 main/binary-armel/Packages.xz + cec55793456c830f3eb634ad6bc4c11166b5cce1a46476441aeaae864b1cf14d 120 main/binary-armel/Release + 00e4892f50700132282dcea3eb149a60f15f6349ae72cf7fb6e1cdd8bb1aa725 43914977 main/binary-armhf/Packages + 906d6d507c259cb7e8ea8f5d604f3cb042814768eaada1c138670eb1df3d6f93 10777938 main/binary-armhf/Packages.gz + 58813e32ee84abe8989a28fdbf65d2e59e31faa1e9c253227c1dd9618861ff3c 7840448 main/binary-armhf/Packages.xz + d69d03c1719e4b56f95b993eae41824c26a5fc7cad0335634bc206bbf88c9416 120 main/binary-armhf/Release + ea12e48485fb8ae9c1819b572f409b205498a1acc73e1af1258c2883b1fa96d2 45116776 main/binary-i386/Packages + 03f3b3f7508f156d0ee531bf0d1f536fe95cd7c216f8092f6554a360b42af7fd 11009325 main/binary-i386/Packages.gz + 9184e0d7effc0bb22459b4bd52948eb74836de7c6393bb76a7091b7b622288a3 8007036 main/binary-i386/Packages.xz + 6c2b48a5f28e01720d1e75229eaa2986eb32ed579afbd2805d7f04c2f4d73306 119 main/binary-i386/Release + 121e34d080470b5544de72c1daa0c3dad53d816d24d97192a133637f893e8736 43745682 main/binary-mips64el/Packages + 564586ae5df59792174c9b7b95bc5cea40e12668e0c1aca982fe1aec3e4e3cd3 10714581 main/binary-mips64el/Packages.gz + b8fc3aa83253e52f7fe29cd0e90a8ae2836aab97be0a7fedc8acd89bd33b439e 7798860 main/binary-mips64el/Packages.xz + 52d50b7141208298c00ed95fee65115574db887521da043cb7989220b8b58d00 123 main/binary-mips64el/Release + 0f64eb88da62d2c6ad9ae7fcc4d810df35d2201db3e0cc26d83bfaa48a7e3986 43699706 main/binary-mipsel/Packages + 57134b1c232d5bf7a862c7b957f7707ac5fea4a37c233b9f336a0333257a5120 10719540 main/binary-mipsel/Packages.gz + 3a52389dbbc37f2399d6b34ab85f7432a82a069a023f6a802ad009d48fccf200 7800188 main/binary-mipsel/Packages.xz + eeb56a2fa68ab3ebd7c8346411e51d8b292a7b76ff2728901650d54d09447f5b 121 main/binary-mipsel/Release + b17f5eb0a586f44342939eed4d605da1be2891507f75ca58425045bb075bf26a 44694611 main/binary-ppc64el/Packages + 19f9083e52eeba57b8ca526f8cc70feb75592eef4ee237da284b3ce9bb1b2b69 10879131 main/binary-ppc64el/Packages.gz + 805fbfbe82e2de0dfea9850c72db29c234d33d6d7e276ae7f669bc1deb77690f 7919384 main/binary-ppc64el/Packages.xz + 3f5b1773033c13d8fa8957f3acd8a7dffae54861fb34dbd8aed559083ac0635d 122 main/binary-ppc64el/Release + 8caf4f2d6377fe62867a536d18addd2b2df56165ae660debd49d436167b7a4cc 43359927 main/binary-s390x/Packages + 89e53980c77b54458a27be19fe2bdbc0828b34b7bb8645d1ec333a50f951d078 10679541 main/binary-s390x/Packages.gz + fb6f0baffa282bcb30f1b1c41ff135178055437d196759809f47d4f87e7c1140 7772456 main/binary-s390x/Packages.xz + 29a65c72593399d269614a6c4a991f5771e6874e604efccbf18776c4132b7f2a 120 main/binary-s390x/Release + 97618e582621b7d19a58346e8bd45e211da8d3b8a3760bf8901137f6912212af 61176 main/debian-installer/binary-all/Packages + 2973ee5ff4b673d8d1b2802b6ab5230962e5ffd3fd223f4266fae06685a16d75 16521 main/debian-installer/binary-all/Packages.gz + 9c1c769ef1411c3b442f5464d0d25b11a05ef267ede709f1325655a19bebfb5b 14524 main/debian-installer/binary-all/Packages.xz + 20620429411053e7287c9996f558ff21cdebfc562f74f4e1d58429741da9b242 118 main/debian-installer/binary-all/Release + dc606fdc3f0cb923890c6a1586553e2604e084d1bd1056b1933d23c70f87d08c 274532 main/debian-installer/binary-amd64/Packages + cf261faf927807b17cd52502b2f5a2c332fc7e270143258d30fba7d99e0aee13 67562 main/debian-installer/binary-amd64/Packages.gz + 3a96cd19f695266819825748f13c8590144fc5ca0df38f5d677719793b6c551c 55224 main/debian-installer/binary-amd64/Packages.xz + b0b4759201a7287de4017b57d138c16fe6bc420af872dbf875be5cb33dd9f466 120 main/debian-installer/binary-amd64/Release + 5f383679205b0c0368a86f79b3e93bae0b85f6385b193978affba222dc13b9b3 257529 main/debian-installer/binary-arm64/Packages + 2ac5b46193d865bf0306487dbed75c3785e606b6783c83c85fd6db8e4c1e719f 64500 main/debian-installer/binary-arm64/Packages.gz + c5cfc8d73eb1220cc70373c10e58fac3d474c43622f34d70306e11166050672a 53128 main/debian-installer/binary-arm64/Packages.xz + 71b7d9a8245a77455b793afcc1cf6fa1ca76346e189d0b4c4d524281feaf490d 120 main/debian-installer/binary-arm64/Release + 77caae7696f0fd257b48ab1c53728f686433eb6cc3c4cf64812dd8367b816b0e 248543 main/debian-installer/binary-armel/Packages + f2a68b69e14062e3d344d3fae8ab8f5e1914d0f3344ab7ff767dc4412961a587 63326 main/debian-installer/binary-armel/Packages.gz + 1af5fd3e3bbea46d23966749b12d4371dc7b2b5df3ba27e14860959d07abbe93 52408 main/debian-installer/binary-armel/Packages.xz + cec55793456c830f3eb634ad6bc4c11166b5cce1a46476441aeaae864b1cf14d 120 main/debian-installer/binary-armel/Release + 0a460c53777ba4bb40bb013010d979bd4de208a7f84dbb597214b1ffea1205be 251968 main/debian-installer/binary-armhf/Packages + 4300d0d403fe7c9ad19ddbc24b6230a21cc14b38b3a151b3c6c312e1258c7858 64653 main/debian-installer/binary-armhf/Packages.gz + ef2341cef23e31b1221266db7c0357ecfc7552f8a2b54f044f011f1fe9e6cfa3 53088 main/debian-installer/binary-armhf/Packages.xz + d69d03c1719e4b56f95b993eae41824c26a5fc7cad0335634bc206bbf88c9416 120 main/debian-installer/binary-armhf/Release + 89dce83d84cec275c0ac18ec79ec82d78a93081b22a195b140ee962ee6190e99 349625 main/debian-installer/binary-i386/Packages + e2c680206e2edc9a94ecccc3bff2a57b1ec9f05b4e1e582a097dcc938408609f 77105 main/debian-installer/binary-i386/Packages.gz + 4868a0bf36e5ad0b6fbc1a418aef9fa38f757c87f128c851359530585cace9ec 63268 main/debian-installer/binary-i386/Packages.xz + 6c2b48a5f28e01720d1e75229eaa2986eb32ed579afbd2805d7f04c2f4d73306 119 main/debian-installer/binary-i386/Release + b594bd9c48ed413af85ca446d5fb5e7c9e84d720fcb81293a8d1187d391d138a 364896 main/debian-installer/binary-mips64el/Packages + ef9edf3923be37c0c1e535aa67d301c3104b6ccadf83fd461324a9bbf43d8a82 79808 main/debian-installer/binary-mips64el/Packages.gz + 12d030a6a96830e0865bd63f81dc9d125621202530a54e45d3dbc8dd61487224 64948 main/debian-installer/binary-mips64el/Packages.xz + 52d50b7141208298c00ed95fee65115574db887521da043cb7989220b8b58d00 123 main/debian-installer/binary-mips64el/Release + 12017cda650110ab85ab2ff833c0eb9d268855ec6e6337743fb16109be454d97 364382 main/debian-installer/binary-mipsel/Packages + 67976a57a80c161c14e3ebc5d904c29f4a3ff48b4014dcc4f3c17bd588507ccd 79714 main/debian-installer/binary-mipsel/Packages.gz + 69065097a260f83d76d00679f0d0b43c6a387b591e709b529cec04e97d43a1fa 65208 main/debian-installer/binary-mipsel/Packages.xz + eeb56a2fa68ab3ebd7c8346411e51d8b292a7b76ff2728901650d54d09447f5b 121 main/debian-installer/binary-mipsel/Release + 0df0f6540ed2c9a671ad9776a0a9f11792c27a67b876426be3ca74ba384e5f62 257113 main/debian-installer/binary-ppc64el/Packages + 1a0a9266973e0fd758cc8103224e8145ee28e62a57a5171b885710cad3fb8445 65212 main/debian-installer/binary-ppc64el/Packages.gz + 587c3a49a522849616e634d25203d342c399e9af7118bcd7591a3d6cb765981c 53132 main/debian-installer/binary-ppc64el/Packages.xz + 3f5b1773033c13d8fa8957f3acd8a7dffae54861fb34dbd8aed559083ac0635d 122 main/debian-installer/binary-ppc64el/Release + dc3129a8a7d40af9d296d7f8a4bedebd351e410e80def3f19f3054282cc82f61 226455 main/debian-installer/binary-s390x/Packages + 9a2c52cd16a497a134c1cac3d2201f31b8c763a51c23c574f1bed29a309630f4 60253 main/debian-installer/binary-s390x/Packages.gz + 507fc7fa7cf4b1fc16115e43fe894b79564ceabad6412281e2a84c80d2cf69d5 49424 main/debian-installer/binary-s390x/Packages.xz + 29a65c72593399d269614a6c4a991f5771e6874e604efccbf18776c4132b7f2a 120 main/debian-installer/binary-s390x/Release + 99d8d572b0219a7b37addc91ff4e4ff238a33b3452580d4bd2469588a2225cad 18520413 main/dep11/Components-amd64.yml + 9c5522d811abead85a73407f6b56b171207105bb3641e22d76f2146482d4750b 6213469 main/dep11/Components-amd64.yml.gz + 0b517038e27fe4864c35de9459537d91f5d274800a172be69f91e90bb3631589 4048504 main/dep11/Components-amd64.yml.xz + ed767617ad156481cc8948fb72c2d699d6292bfd2d83fb2f24b2b155612dc539 18436837 main/dep11/Components-arm64.yml + 1732a30dff783f891da2245f955becf3a43be40f0400b722087ba626316e980a 6191092 main/dep11/Components-arm64.yml.gz + a02d6259b836d37804838b6de8f40568332a9a78cb4bc7668b32208f6062e782 4033216 main/dep11/Components-arm64.yml.xz + aa3eea13a49b29dba27956d6fb6093817775361e29fef3f751e8e70b7065e54d 17658848 main/dep11/Components-armel.yml + ca3d41da75c25408834b265c9c95f700a1241189f6bf62270e14b85920f5cdc2 5952269 main/dep11/Components-armel.yml.gz + 5c90b5a79fb5cf11b4e822396183bd3b4d3712e5f8e9363c5fce4a3a6c42a58b 3879744 main/dep11/Components-armel.yml.xz + 9d95db48c33d5671c96a2931458a92b6290e9c3f880c7ec7d7aef2b23a681eb3 18205252 main/dep11/Components-armhf.yml + 55c47f2e4607828ad1d875c1ade2aea6565916e9dce3e043f6de2e85b6cd74c4 6110587 main/dep11/Components-armhf.yml.gz + 20797715d417813ddd77d1bf746b8ea9f6353ad0e8be2e67f1700813d992268d 3983180 main/dep11/Components-armhf.yml.xz + 5579083d9a290f05eeb86967fd664c46464b3bafc00c073887560523a1793a64 18485654 main/dep11/Components-i386.yml + ac8dd6c8b9e575785646a7d41adc7783956e22bcc757a60c80f225328c769f08 6201776 main/dep11/Components-i386.yml.gz + 589f93188296c83e394c89ccdaae1565436dc203161958e96f3a5cf2797684ca 4041608 main/dep11/Components-i386.yml.xz + 2b028df6a795c2a4b058b0f239745da363ea0f8b9fb8ce1a7955bedf579cc8cc 17819116 main/dep11/Components-mips64el.yml + 0865e497ec87d5d45f84106166bb035610443e87528aacc1a43f13000542a3f5 5977494 main/dep11/Components-mips64el.yml.gz + 46745049532f14f438f41704b442c157ee0f2990baed5d06da8fda3b41501547 3896708 main/dep11/Components-mips64el.yml.xz + c0e1c64172edc19edcc287b0e617adff28b31354028de4c755cdf1fd077de913 17947079 main/dep11/Components-ppc64el.yml + ba4eb9c1ab3f03a7fd184e5fc47dce250c083a617d9e2ba49a70c920fd957b29 6023058 main/dep11/Components-ppc64el.yml.gz + aa34918432eeb8a82d912d86f69d82e84a4bc0eb48056ebe321b83d2757d1052 3925796 main/dep11/Components-ppc64el.yml.xz + dc222c504c71bbc9ff6b698bf5ef7942e098efff1031861e5eb8670afdd18452 17735785 main/dep11/Components-s390x.yml + 29584e8fd8bc91d9d9099893ae4951601430b1df4f55659e089d34e4525540e5 5976062 main/dep11/Components-s390x.yml.gz + 1f9ca828b916aabab9b41f75950df49f71dc5e8a42f674ff4cb2138f85274314 3894008 main/dep11/Components-s390x.yml.xz + 057f28adb7c2452ab2c810fdfbfce0305ba8143ffe2e24969b2ece077aba7e9f 13048320 main/dep11/icons-128x128.tar + 4f46415e13538a05743752a630c9b8795a9772d0ab4ebe83c9d7e19f0e4bf179 11409337 main/dep11/icons-128x128.tar.gz + e0c306e3293ecdcb8392faa372b00f1fb979c327c3e4370452acf7713ab885a4 4878336 main/dep11/icons-48x48.tar + 93c4366d8b6ef489bb935434d9a2c56d842978922e941dd4ee716ede2a805494 3477622 main/dep11/icons-48x48.tar.gz + 910ec31c85f12f0edefbb43fa2514b9896d105ce7316272a4c55263af864c238 9378816 main/dep11/icons-64x64.tar + a94629c3e4fbe9607fb2921e1c906f88343a7cadc484a1087983181ae6df66a3 7315395 main/dep11/icons-64x64.tar.gz + e061ee16e4478c39875bc3d977fdd5f880a71a3ea97c9f5119ac127a4305579a 6191 main/i18n/Translation-ca + ed06627194c667d774188bcf0d9b859625ec60d2098238ee3c1cd5e1c147c4f7 2673 main/i18n/Translation-ca.bz2 + 857bef6538df7a4e2ae01a6ef40f8a5c9e0512797a769d8813caaa57ca867f29 1205166 main/i18n/Translation-cs + bdd79636af5f08f4c40bb5266a41e4707b7bdc84d5458451df0255b787c380a6 323247 main/i18n/Translation-cs.bz2 + 2c7c6d7013e3d04a62c457525567fac4ac2747ef59f1b2a93cad8c0904c960b9 20240560 main/i18n/Translation-da + 8935ec6ddfeaeb542fe444013ad9fefd6ffd2da2afe818efeb417fb50568b52e 4411163 main/i18n/Translation-da.bz2 + 55e94848df1df7d0963f3cb02cfb4171031350c549e4ae64f6aed517ed08ca6d 7801238 main/i18n/Translation-de + b68fe8718325ebd1e2a8dd30f52b17c003e315f3468f9b7890fe5b1b91c709cd 1717951 main/i18n/Translation-de.bz2 + 284169348b8bd4e0de4cc5641eeb05577e80d2bd736452e454976c052cf3cbe2 1347 main/i18n/Translation-de_DE + 481a435ad350105b74c4972859c44f447b7a8b5edea0d42f6dd635792e00a461 830 main/i18n/Translation-de_DE.bz2 + 9f3b3bc0da0653f0ac8484024a7f77aeda681474907f3a94b8a0a0933775d14d 6257 main/i18n/Translation-el + 807de361285151534654b83681415016d443e4abd1a7ba36e1e78b4ac337b973 1835 main/i18n/Translation-el.bz2 + f7992b84d574c2db87ac12b7b13f9b8518b4f1ce0e4e904a53406c25bc610b8c 30255023 main/i18n/Translation-en + 2e02ee8a85fd869c2cdbed8b7fbca1d212475a8c95ed4787723209cf39a626dc 6236235 main/i18n/Translation-en.bz2 + abccaeb24d409c21b94883b74785053d0f8fad3e94449078ebe92af38861bc5a 2261 main/i18n/Translation-eo + 747ab457a83de3b107e25b9cc5536aea2f19e0fe1f08d5357475acea0d788fae 1196 main/i18n/Translation-eo.bz2 + 38345d246390b3845920937338647a70b1a6a93f354615da725fbf426ac3e332 1325929 main/i18n/Translation-es + d6bd3bb26fb52e553bdaa40a041aa167f8a0c207149ebf626bea65c90ff7e99f 317946 main/i18n/Translation-es.bz2 + 80c3ff00f3b37b64e73c85b11eab47fe88901b6f8d9f189de0e95a387e02ebed 10093 main/i18n/Translation-eu + 7ce6c68ef8a577bd215da5f7a12153bee27268b0b6b9503aaf88244b225f20a1 3914 main/i18n/Translation-eu.bz2 + 54c5db1926c3309513d37990460a51c586ae6f01bcaaf2732e537ae400b6f5f5 269212 main/i18n/Translation-fi + a0c315c9c517ac029e5981f14a3c15fa022c7c0e1e86edf123e05027343974d7 75849 main/i18n/Translation-fi.bz2 + bd258bc1f5bbc6694e24f58fe4dfb5f5636afc86a431795b931225e9e336feb3 11857302 main/i18n/Translation-fr + ef77125783dc8b1125ea85050ba00bfe042e6f38fa1f73613387fe30cae47c5c 2433064 main/i18n/Translation-fr.bz2 + ce1a70b1000909a09166e30d574c717f3d60ba173bb65ad65e768374dc73232d 1427 main/i18n/Translation-gl + fa1eb924fc1473b81f7790ccd909de1dc274f4f266df8af544261f03e1d21079 824 main/i18n/Translation-gl.bz2 + 22e19c218655a9a4d09e9930a66715aeb5d0b02bdc4d147e5816067873e71861 21069 main/i18n/Translation-hr + 04e538e90503a9238d071bba89039e563d4c03ee038c217708a4f8c8672c28d6 4695 main/i18n/Translation-hr.bz2 + a275d9da1b509fc6c1d8307ff33daea14669cec8b8f89bb4c4fdf4d50ff48135 65236 main/i18n/Translation-hu + 94827a9f6e251237fb3b093360f88ba469d2be8d4a7c2c02c84298c94faceaa5 22243 main/i18n/Translation-hu.bz2 + 0f4bfaba954ffa37332a34df69c8844b7334cc0b61515e9510513e2c43e140b1 3983 main/i18n/Translation-id + 11aebe26133b1249ebc06ec6d1a8b76f5975b9a3630daf71ecb7e2f6521a2fd2 1780 main/i18n/Translation-id.bz2 + d965461960f14ff1f614bcd0ba757874e098cd460b8ae0e018fb4aba254ce641 24489940 main/i18n/Translation-it + 451a92cd21dc98889f43a39223dc8863284bd1a8e515bc58633bdb7bf96dd37c 4844227 main/i18n/Translation-it.bz2 + 1cb8cbfe8b502cc64639b02150e6f805bdeebedae3eb69273146c03ca6c9287c 4511401 main/i18n/Translation-ja + 0c00e0a8cff6fb13bdc4ed3387e3faf4f9db94f3ed4ca8e72d324c0a03d8f018 803966 main/i18n/Translation-ja.bz2 + 7238152be74233d91630f7100ef7ff2bb8a95598b5fbc11c21c7afeecfc0fecd 11879 main/i18n/Translation-km + 01577e06c8e41b3a914ae539147af0fcdc7a0f883f50d82b57b263cf62fe1bf8 2371 main/i18n/Translation-km.bz2 + 232cb289feae187cf94ad451662d7ce36be8014c40b69e645d19b9534dd586df 2606190 main/i18n/Translation-ko + 894aba3a34a47f3d59deca3bda07f8aa288e9f4ed6ae92422eab3fd9dd370ad5 584643 main/i18n/Translation-ko.bz2 + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 main/i18n/Translation-ml + d3dda84eb03b9738d118eb2be78e246106900493c0ae07819ad60815134a8058 14 main/i18n/Translation-ml.bz2 + 16be336bba03786450a43321709eca2fce7fa7b50a135a97da71e16eb5e7d60b 1193 main/i18n/Translation-nb + fdec5fc00fe2d0e3c7730462f95273492d278eb8a6957c1b437969833366c217 738 main/i18n/Translation-nb.bz2 + ce65092fbb0a09286644912bfaf3a9535921705519e16d07617ad85ec44ccf3a 174332 main/i18n/Translation-nl + e12b923a6f3f83636a31e6e1b2503d8a213e1e4112586a27700fc17bb48ce564 47973 main/i18n/Translation-nl.bz2 + 8999184566c11a42f9a31c810d9252316dc4f52ba738db43e0be2cd760c823a1 2051811 main/i18n/Translation-pl + 17fe48deb79b044bdf5894d0d129823b1d86653b9759f848667a74b563625379 491993 main/i18n/Translation-pl.bz2 + 2dbf3c4316bba32571abc589b177be93c8e72885131940c9993d3fb6b8d58cb4 1074959 main/i18n/Translation-pt + 991a66952f6395d7588f38e68e1032f4dcc72da61322a59460c34a24d7713400 272186 main/i18n/Translation-pt.bz2 + 5d7ec6fe173a67789c445369b7ebf8709cbc9ce4f3e06a75cf36c562a16580a1 3306707 main/i18n/Translation-pt_BR + 1583cdd6a71e29b6eaea0d29dee9ce903fc8ced1f9f57e5ad4de154938799bd0 802734 main/i18n/Translation-pt_BR.bz2 + c90708ca8975ced4acf4be98a4ac1f5c8092fd826b4d928e35c3650e705553d4 1717 main/i18n/Translation-ro + 35f2449dba7bd93e0aece908f4c4de53cc864a48c8f7aeaa5a64f67384e1bcda 982 main/i18n/Translation-ro.bz2 + f8b907289a1970413a47a3450c59b04e166c08cb387ee3ae4f6c0d2e4774c379 3058931 main/i18n/Translation-ru + 8685feba7a33fef7ad8d7fe5db5f59e837eba69134deb87610742cf564e47258 494782 main/i18n/Translation-ru.bz2 + ee2a1713ba3ccf4aa7ef3ee1b5786874c38ecc15db012bc15c3efbf5ad8facd2 5984088 main/i18n/Translation-sk + 0dfec1c42d581b3fe8f95bbe26f649f45234d419c7e709dc881f1994bfb20974 1304539 main/i18n/Translation-sk.bz2 + 5ff9c60997a547f07d212476a8f50b4942f012d7952765c6c1925c52495711d1 323953 main/i18n/Translation-sr + b4608fc3c0c7f6aefe0f6e5e19d0fbe0d5035333e74044e29358b3e3efa99536 58385 main/i18n/Translation-sr.bz2 + 5656d4e913760691e99cd4805e76c8f18c4441fe707a02e621a2a172da756d5b 85612 main/i18n/Translation-sv + fbad8c083b9985e53a2a82d7e32f26f683bd5b8e2f1bf09a3e0fc3f8f7abf6da 27320 main/i18n/Translation-sv.bz2 + 2e50dd5fdf1dd6157c0db51afb4457fcfbd427ebb6d1268aeeea1daf50da78f0 14670 main/i18n/Translation-tr + 401a0f8d754d92c562bafe54aa0cb2dd7686ca015425513b666b50b8c9dc36a7 5362 main/i18n/Translation-tr.bz2 + 6c66f49d6c9df7ef28f92aaab2620a2151fa16f74bf96deb3b74987183e43b86 3740343 main/i18n/Translation-uk + bd760427bda1a65895dd7b3bd6a3e2b2a0ee6b4060ce726ec4b7c02b89a72204 576766 main/i18n/Translation-uk.bz2 + c2207dfa8d62c7e2a31851842dd928739bc147515f69fb7a28db93196dd1a601 21882 main/i18n/Translation-vi + e3eab47e1acdc01ee2d774dba5b0f9d29c98ff48b25a57d469eeecf60d3035ca 6510 main/i18n/Translation-vi.bz2 + 7133134d1b1b6c869b4b700fed9778e93a0b774391402ad3399e9ff46984efff 2007 main/i18n/Translation-zh + 8cbeadbbcec613b8476f8e2aa40b15772909109e10a83317c111fcf7c28d0219 1215 main/i18n/Translation-zh.bz2 + d88628c7a7a16a042234daf91a709daa6d5f9de15406ec78530891354fa25c75 425199 main/i18n/Translation-zh_CN + 1ef87b145198090deb2d037bc16b5b940c0e757a2511f4ff84a7c750720b2723 113621 main/i18n/Translation-zh_CN.bz2 + 564fdb3059cffbe78dde61697e77edd7bc94005a358cc4b5dffb436776d1b2b0 39965 main/i18n/Translation-zh_TW + 0a4d5ecccec7069a32b30de129018034b2f6f2b318f1530e1edc239182442cf8 14859 main/i18n/Translation-zh_TW.bz2 + 162365f8292f0d461c3bc30895ca33ee679ce00030143a8468e734df2bb21502 58277 main/installer-amd64/20210731+deb11u11/images/MD5SUMS + 2c436aaaace0203117bbb4e8df3e9cebb7c24a39f6ad2b9956f57eee57b22510 78097 main/installer-amd64/20210731+deb11u11/images/SHA256SUMS + 91e63d03c43f9feaed6c255a510c30c35c547c517f395c2574900b0119fad790 57705 main/installer-amd64/20210731/images/MD5SUMS + a3a16cc4af2d688613ce8df4d224974629ad3383a1969350c24ea68bfdd5f1e5 77333 main/installer-amd64/20210731/images/SHA256SUMS + 162365f8292f0d461c3bc30895ca33ee679ce00030143a8468e734df2bb21502 58277 main/installer-amd64/current/images/MD5SUMS + 2c436aaaace0203117bbb4e8df3e9cebb7c24a39f6ad2b9956f57eee57b22510 78097 main/installer-amd64/current/images/SHA256SUMS + 88e8a5e12b51d6f6a9995182bd54846ac3fbc004c7a6eeec98f8b6503c361402 69049 main/installer-arm64/20210731+deb11u11/images/MD5SUMS + c0afa24e7f0b885eef40c35dc1c1d683be8512c3e2ea0a5c2a2f7d05d1df907d 94149 main/installer-arm64/20210731+deb11u11/images/SHA256SUMS + 291e81049aa85b147063ec1aa5bec87da60d3196c06c3098de5210c3346837eb 68403 main/installer-arm64/20210731/images/MD5SUMS + 5dfc89487fc8717ab9a9b75cdaaf01a295ab3021cc3310d3fe9dd3e78fc1f666 93279 main/installer-arm64/20210731/images/SHA256SUMS + 88e8a5e12b51d6f6a9995182bd54846ac3fbc004c7a6eeec98f8b6503c361402 69049 main/installer-arm64/current/images/MD5SUMS + c0afa24e7f0b885eef40c35dc1c1d683be8512c3e2ea0a5c2a2f7d05d1df907d 94149 main/installer-arm64/current/images/SHA256SUMS + 6d0c45429d567502ecb5e8b7581e1ba2bb853013dd53d1fe34ce0aa67811b88c 20678 main/installer-armel/20210731+deb11u11/images/MD5SUMS + 2e177e863325b045f9afa9d3d7e12cf1f9289e0ad0a9584f11ae98803247892a 28882 main/installer-armel/20210731+deb11u11/images/SHA256SUMS + ee9f639b7a0304207f23c84f5396284720a6fc6c638ee7be6873944a0f224c95 20182 main/installer-armel/20210731/images/MD5SUMS + 07353d4c378ea579803ed8c1aca3fe6df2cbc89788736c7d01102a7b3ebad859 28194 main/installer-armel/20210731/images/SHA256SUMS + 6d0c45429d567502ecb5e8b7581e1ba2bb853013dd53d1fe34ce0aa67811b88c 20678 main/installer-armel/current/images/MD5SUMS + 2e177e863325b045f9afa9d3d7e12cf1f9289e0ad0a9584f11ae98803247892a 28882 main/installer-armel/current/images/SHA256SUMS + e92ecc98e9599b7d9211ff0a65a5b111e99eb1d2a74e043c1e490a95f65f4c35 64380 main/installer-armhf/20210731+deb11u11/images/MD5SUMS + 13bf16d1c44d7d1972c98e7c425a6301aca06b73eb0b1b3191f77445b32db450 92680 main/installer-armhf/20210731+deb11u11/images/SHA256SUMS + 8c1f810a60fc7daf099e608b763cec563f59c82203a07bbf4469a6213a8946eb 64240 main/installer-armhf/20210731/images/MD5SUMS + 67c5b636e3fc02747ca9593e6fc7e906a3ec95d4947740fec81b1e942f0643ae 92476 main/installer-armhf/20210731/images/SHA256SUMS + e92ecc98e9599b7d9211ff0a65a5b111e99eb1d2a74e043c1e490a95f65f4c35 64380 main/installer-armhf/current/images/MD5SUMS + 13bf16d1c44d7d1972c98e7c425a6301aca06b73eb0b1b3191f77445b32db450 92680 main/installer-armhf/current/images/SHA256SUMS + 539bf72aaade930be8c2c6ecf5d5b5b30c0fc76d0f74805037a79993857c15b4 56840 main/installer-i386/20210731+deb11u11/images/MD5SUMS + b2793ddc5525c2fb939aba918d2a183febe9a3784bf0681aab89efaf8fceb9f7 76724 main/installer-i386/20210731+deb11u11/images/SHA256SUMS + 96e8acb8eb827ce7032587400fbe848b6f53921c661d52e1b16fd243cb8e57aa 56286 main/installer-i386/20210731/images/MD5SUMS + bced74c95a3688a9a2a28abb8190cb7efd7e1f6372dc8989e260771752ef571b 75978 main/installer-i386/20210731/images/SHA256SUMS + 539bf72aaade930be8c2c6ecf5d5b5b30c0fc76d0f74805037a79993857c15b4 56840 main/installer-i386/current/images/MD5SUMS + b2793ddc5525c2fb939aba918d2a183febe9a3784bf0681aab89efaf8fceb9f7 76724 main/installer-i386/current/images/SHA256SUMS + f5f84b89af68f05627471f3a3bd835459696fb262d06ec4eb2ddef5cabfe593d 630 main/installer-mips64el/20210731+deb11u11/images/MD5SUMS + abe5c5bb88b24baca51ce62a29988c6299a9645062164600c513445fa3aeaef7 1026 main/installer-mips64el/20210731+deb11u11/images/SHA256SUMS + af3b55dea76e91f1565bd54bc1af76a6a0bb4991eef9abe281a22d9fd8d54a7b 627 main/installer-mips64el/20210731/images/MD5SUMS + 995cda8278b101eb25849d56f3ef33290fb57a940fa1c6837f19df00ceafaaff 1023 main/installer-mips64el/20210731/images/SHA256SUMS + f5f84b89af68f05627471f3a3bd835459696fb262d06ec4eb2ddef5cabfe593d 630 main/installer-mips64el/current/images/MD5SUMS + abe5c5bb88b24baca51ce62a29988c6299a9645062164600c513445fa3aeaef7 1026 main/installer-mips64el/current/images/SHA256SUMS + 82bc43e4c9a525ff006806a644bcdf3391fb9f06089040604b198b8eb4ae7f2e 630 main/installer-mipsel/20210731+deb11u11/images/MD5SUMS + 900022b3f78971be0d474d4ec7ba98f75746a3cd6006322df6c5460b931300f9 1026 main/installer-mipsel/20210731+deb11u11/images/SHA256SUMS + ca77bbc823d1bf6999e141cd42c1bb4c18179cbe4a3fbb6da3e40e1055848ed7 627 main/installer-mipsel/20210731/images/MD5SUMS + 28589449e1b3ac9a73bdf6f266edc83e70ebbbca587a228b15b0dbe5e1a634fa 1023 main/installer-mipsel/20210731/images/SHA256SUMS + 82bc43e4c9a525ff006806a644bcdf3391fb9f06089040604b198b8eb4ae7f2e 630 main/installer-mipsel/current/images/MD5SUMS + 900022b3f78971be0d474d4ec7ba98f75746a3cd6006322df6c5460b931300f9 1026 main/installer-mipsel/current/images/SHA256SUMS + e1287311079f8317185fef1d71c78e6686ff62806897c705f0db8ee517435845 576 main/installer-ppc64el/20210731+deb11u11/images/MD5SUMS + 833cd00f67959a4b18c7e1415bb37727450781266055f5ec65e1858dd27543b7 972 main/installer-ppc64el/20210731+deb11u11/images/SHA256SUMS + d162b2da6777c1ea0643921cc1a3dde78ae48cf022711eb98c7e9dd030b89a44 576 main/installer-ppc64el/20210731/images/MD5SUMS + 73e281bce56df3c7512ffa1a1cb13886064759a461621db4acf9b1f71965c676 972 main/installer-ppc64el/20210731/images/SHA256SUMS + e1287311079f8317185fef1d71c78e6686ff62806897c705f0db8ee517435845 576 main/installer-ppc64el/current/images/MD5SUMS + 833cd00f67959a4b18c7e1415bb37727450781266055f5ec65e1858dd27543b7 972 main/installer-ppc64el/current/images/SHA256SUMS + 3b8792d3a4d106308dd7e0d5dd33b50c018f9e300ef5f67ec90f23d9ecb1cf6e 374 main/installer-s390x/20210731+deb11u11/images/MD5SUMS + 44af8efdeb64fc93c8f879f85cbcb2fbe5e5f2c679f3a93ba41f2f735576a73a 674 main/installer-s390x/20210731+deb11u11/images/SHA256SUMS + b2c58a9c5b97a59742a8056e3e9d7f4f22d4d11e51c71d7a0051dc4649a717b9 374 main/installer-s390x/20210731/images/MD5SUMS + 61447263ea7318c444fde199afc718a8498fe67bc0e7116f2e1103cc65ef672b 674 main/installer-s390x/20210731/images/SHA256SUMS + 3b8792d3a4d106308dd7e0d5dd33b50c018f9e300ef5f67ec90f23d9ecb1cf6e 374 main/installer-s390x/current/images/MD5SUMS + 44af8efdeb64fc93c8f879f85cbcb2fbe5e5f2c679f3a93ba41f2f735576a73a 674 main/installer-s390x/current/images/SHA256SUMS + 76ea84afb69cbcd816c3cb7977758c448091ce0de018ab3072a62f2e7dfa40e6 121 main/source/Release + a3816bb0f651ff9a0e73fafade11ceb7d203c9338e465dc00a7122c1af0959bd 44710563 main/source/Sources + 89880dc039475776b5c5209ddafdd548fe2825fa1d070dc785d174be715e024f 11432302 main/source/Sources.gz + efba366d510c38d64c31f72e8dc8de42925d666ce1dc54aea1de3e0fc77d0878 8502160 main/source/Sources.xz + 29cac69ab0fd86e224587eea8e2ed2fb9b1b2e3c936fb1dc7165b8ed8d00528a 17347341 non-free/Contents-all + 3b87590d0360ae141f3688fbafb5fdad35d4dd4b1a239888c911743c4357862d 888157 non-free/Contents-all.gz + 7c565c8d3f4fb90fadcd3f9e80e5324bd0a6cb128672ff309432a6abcaed48dc 1100508 non-free/Contents-amd64 + 3418e19214dfd2e0b24d1ec67991e2c9e0e1bb3a75fda92f08872896cddd0fc3 80075 non-free/Contents-amd64.gz + 6e4bed3fc85c4242dd18af50b210ea0f2514c44f85c7e26095e88e962f28a013 503217 non-free/Contents-arm64 + 2447d3c4abf70281c0b2fd1dc643d928646f7895acb8bbccddc957313bb78c76 37848 non-free/Contents-arm64.gz + 827b79c179b6453c3caa195b8b03e8e42f374ed046fa063bdb2460f8e7541d19 95417 non-free/Contents-armel + d2afa3f0ae88d3ca2d9aac440af0f2d6301dd9fa977163a03e90f43e1cfdbaa1 9294 non-free/Contents-armel.gz + 48f3fb1e6ba01719f7627cac528bda99d533218baf7a1c24fe2e0cbca5fb8ebe 146124 non-free/Contents-armhf + 1c1e790580d7fdbc5f8d178d5b462ff7245f8e8ff7221b0b2ce7f0c11d380c99 13508 non-free/Contents-armhf.gz + a8ef263e31efad21338a0c886cf7b256df7ee205b4f39133e2df332ee544dba9 344734 non-free/Contents-i386 + f432171c7869b70f1dd720988c0d21bbcd07f4b1b139ba911547b1cd4b65b712 29303 non-free/Contents-i386.gz + 70ec451abc9e5aded1a1de9449af583d1948b35e2ee86acd0e3e4337b9e4e0f9 91215 non-free/Contents-mips64el + c96ff0828f1e42f75280162e04af630d83f47080facaeacac8df0f7d271e3c0f 8661 non-free/Contents-mips64el.gz + a501c1f687d88db10d64f8e2caee68b54d37ac08fd167c0c1246918286441ff0 92244 non-free/Contents-mipsel + f31e9d5e871897abc6330f817cdbcac34a055f4124efb1a97bbff9df79656991 9013 non-free/Contents-mipsel.gz + 784465fda98f973a293f21bb7c75d5924b5c577e86740b4a6d10e283470a1506 784438 non-free/Contents-ppc64el + 6d96cbfc02860ef2254e0860025b8d9060ec937d44259597b42e9d1e02755f67 54577 non-free/Contents-ppc64el.gz + 4c88ec8e5f41a669462d2b24de862d0efd892e7b01da1c188144e893ac8cff9b 74537 non-free/Contents-s390x + 42d81a546af19c3f1b50cea30018b24792d93f2526bea3aec173eef6eb11d1b2 7367 non-free/Contents-s390x.gz + bb5c811e77f0edaec0501d603b1d3ec2637f288a113c19f24e254beb161ac53b 10810052 non-free/Contents-source + d989f3c3aac1d7483405e4f3dbcea3017fe3c8267d2a74cbc83bdd9a2cb441c2 1064267 non-free/Contents-source.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-all + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-all.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-amd64 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-amd64.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-arm64 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-arm64.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-armel + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-armel.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-armhf + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-armhf.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-i386 + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-i386.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-mips64el + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-mips64el.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-mipsel + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-mipsel.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-ppc64el + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-ppc64el.gz + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/Contents-udeb-s390x + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/Contents-udeb-s390x.gz + f3a5bcf13b06e7207336d5c8a8543870f6b6948183875ddce24fa495bca6a2c8 189039 non-free/binary-all/Packages + a0cc2811f77e98c9b54cbc592c0431aa6cfe6bd8ced656705ed3b156a1e738fb 50997 non-free/binary-all/Packages.gz + e09bb8b0ebb7fd3fcb86ffb491772f5af19dc0a1fd5ae6d1d7167239750ab99b 42824 non-free/binary-all/Packages.xz + e884e00f94f68c555ffc8197b6cb1c8b73b1ca8aebacf9aa4d3ae94fed7f9276 122 non-free/binary-all/Release + e7e7da559cdbdffa3f6dce1d47526894c4dbfd492293b58f75d135fbace9090e 542476 non-free/binary-amd64/Packages + e1fe2fcce0ad5eddf3bad933bf0b090be94a5c9c825323f9cb9278ce33b56846 121003 non-free/binary-amd64/Packages.gz + 6b829985c60626dc54c6fe71ee556592f1f1ead31c7bf10297cdb6daabd91d14 96176 non-free/binary-amd64/Packages.xz + 3e4ed4030a469e43e51c9c72aa2dde3afeb96e3a8c388e254b3c1f14ab706324 124 non-free/binary-amd64/Release + 01fd2619a6ef5d3022d53ad1df8355c7a13ef0f8b1591af50e8ece256c7df2d5 379784 non-free/binary-arm64/Packages + 68b4c819c8cfa81651a552c74ce2addc7831ecf1b8f42f99102ced5b1895d20f 88157 non-free/binary-arm64/Packages.gz + 245a771c3e3f80d89a06a3ebf90e978059d73c1b3a79517c014f03d7baf23088 72276 non-free/binary-arm64/Packages.xz + 6498d2f4351b192f4e270a0ca748e286a4c2f3a394e39ff5da98910b39bcd253 124 non-free/binary-arm64/Release + cb3e6042046a0225441ddc29b1139ee7c9f19759862db0a96f155b895d463d50 227975 non-free/binary-armel/Packages + 7db928abd12f9c500763bcf5b9a6b04d1121cec205cba72cd48dcd5ff59800d2 61785 non-free/binary-armel/Packages.gz + 07e8c8e6215413afb696561a42c5df3e5ee602b70957cfbe667ef4148e6ef086 51588 non-free/binary-armel/Packages.xz + ab83f9121d708740b07f30cd007178cb15f5c37a974142a6d44b80308fe123f0 124 non-free/binary-armel/Release + 50233bb694c65820372d2c5ae0f39f943ec165c2e039ffbc107f634a92119565 259198 non-free/binary-armhf/Packages + 75d3adc283173bc2c1250a8bc8e713445709e8667e0e6d172c9b6e5dc6b21d31 67349 non-free/binary-armhf/Packages.gz + 9a82fef0361f96f968608a0cb76071eb38f6e2ee561e159a1217ab04a0da6f77 56004 non-free/binary-armhf/Packages.xz + a33d7b199fb3c3e0e46a23431111d9a0fd17a0906b0034a08455127e7f7a7af5 124 non-free/binary-armhf/Release + 70c49a43962588e666ebbff505b9d3fbcf7c65f1908576f6d144f7bc2e54a4e5 421250 non-free/binary-i386/Packages + 24528e8b442d3a8f7a46d0bccbaac0dd3580f4403ebabe52defed46999a3fed1 96578 non-free/binary-i386/Packages.gz + fe2704dac200f9a91700bf1bfba6c727302a0a50bff079b4bb6a958c4dfb2f78 78372 non-free/binary-i386/Packages.xz + 378115a70c97feeeec83072dc6cc5573a85dc3bf920c3e5ac4a7ddc721cde965 123 non-free/binary-i386/Release + 88df870440f52bbbcfc0ef92c821431478bfe5d1f5924936030528bb98928241 225548 non-free/binary-mips64el/Packages + 7324b2265c85fb4565ac30c39ca93d8b6bf422183f6c6b770694fe37567a8eae 61129 non-free/binary-mips64el/Packages.gz + 20ca042a3b29bef20051bbe06b0aae1f176774a17d60f6d7e13f114149ee6a68 50900 non-free/binary-mips64el/Packages.xz + 93dd8c161d160e788ddf5d4d8f4d36d30da679c8c95f086c37be56b1dc261137 127 non-free/binary-mips64el/Release + 50500082be43416df6835de6c005815f03fbbc3c09073a93eda643db05fba164 226204 non-free/binary-mipsel/Packages + 847e27a8142c3da343e9315d30b4f026ee7470266f3e1f7823d49fbb56d7a28f 61198 non-free/binary-mipsel/Packages.gz + 791377237227caa9578ce253823d01ce9dc1c831c2b9ab77ea5937a2d7889710 51152 non-free/binary-mipsel/Packages.xz + d0502dea4f79532b57f5462285988a2d7916ebf467bb42cc686210cca755f409 125 non-free/binary-mipsel/Release + b396d7398f1076b1cb71f366e316e52fe719fdec400f5c8c588c3f104ef292d3 405033 non-free/binary-ppc64el/Packages + d6b7732fff1ef6c409a55f8cac5f1fa23e2a8d52c467025e67dfc3029894d5cc 91696 non-free/binary-ppc64el/Packages.gz + 51f0d74141047dd21f727255c5c11e00a02c270b7df2ad8baff512e1c5cf25ba 74316 non-free/binary-ppc64el/Packages.xz + 15c1695f9b8ecb48c513840816c64241033619a23a401356c1c00286621097e5 126 non-free/binary-ppc64el/Release + 2750f1422d430e65cff2133657e5bc53b86f675d796dca4db57740c53cefc13f 220612 non-free/binary-s390x/Packages + 6157d83f9311fb54e5479c69516684df68da74dc0c95752761b1b72104b56668 59881 non-free/binary-s390x/Packages.gz + c7567d3c57ad8b21af9b36b4e75f7d0ca1359908348abe13a504822e4e67d940 49980 non-free/binary-s390x/Packages.xz + 533f4b32dd648b1322e3a8b90bbe62791574d6e985277790cf5df5c9e3b9fb0e 124 non-free/binary-s390x/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-all/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-all/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-all/Packages.xz + e884e00f94f68c555ffc8197b6cb1c8b73b1ca8aebacf9aa4d3ae94fed7f9276 122 non-free/debian-installer/binary-all/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-amd64/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-amd64/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-amd64/Packages.xz + 3e4ed4030a469e43e51c9c72aa2dde3afeb96e3a8c388e254b3c1f14ab706324 124 non-free/debian-installer/binary-amd64/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-arm64/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-arm64/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-arm64/Packages.xz + 6498d2f4351b192f4e270a0ca748e286a4c2f3a394e39ff5da98910b39bcd253 124 non-free/debian-installer/binary-arm64/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-armel/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-armel/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-armel/Packages.xz + ab83f9121d708740b07f30cd007178cb15f5c37a974142a6d44b80308fe123f0 124 non-free/debian-installer/binary-armel/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-armhf/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-armhf/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-armhf/Packages.xz + a33d7b199fb3c3e0e46a23431111d9a0fd17a0906b0034a08455127e7f7a7af5 124 non-free/debian-installer/binary-armhf/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-i386/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-i386/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-i386/Packages.xz + 378115a70c97feeeec83072dc6cc5573a85dc3bf920c3e5ac4a7ddc721cde965 123 non-free/debian-installer/binary-i386/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-mips64el/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-mips64el/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-mips64el/Packages.xz + 93dd8c161d160e788ddf5d4d8f4d36d30da679c8c95f086c37be56b1dc261137 127 non-free/debian-installer/binary-mips64el/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-mipsel/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-mipsel/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-mipsel/Packages.xz + d0502dea4f79532b57f5462285988a2d7916ebf467bb42cc686210cca755f409 125 non-free/debian-installer/binary-mipsel/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-ppc64el/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-ppc64el/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-ppc64el/Packages.xz + 15c1695f9b8ecb48c513840816c64241033619a23a401356c1c00286621097e5 126 non-free/debian-installer/binary-ppc64el/Release + e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 0 non-free/debian-installer/binary-s390x/Packages + f61f27bd17de546264aa58f40f3aafaac7021e0ef69c17f6b1b4cd7664a037ec 20 non-free/debian-installer/binary-s390x/Packages.gz + 0040f94d11d0039505328a90b2ff48968db873e9e7967307631bf40ef5679275 32 non-free/debian-installer/binary-s390x/Packages.xz + 533f4b32dd648b1322e3a8b90bbe62791574d6e985277790cf5df5c9e3b9fb0e 124 non-free/debian-installer/binary-s390x/Release + e13d055f233a81a77666f0ff8dd9d748917b2829740756e1dc2b8a350309bcb0 278293 non-free/dep11/Components-amd64.yml + f51b1a07cd72a36b2a9f36742ab26819a7808aa7765cbf3e2ff4abe6be66b50c 29634 non-free/dep11/Components-amd64.yml.gz + e113163e116c137577fc9d3a4f7c95e0934ddbae7bdae5e083aaa1ce095435b6 17904 non-free/dep11/Components-amd64.yml.xz + 6177cb908c067306c11bd8728a5b65a205d999be63930c079e3ff4250a24ce8e 271451 non-free/dep11/Components-arm64.yml + 1b6107a1fa771a8fff50e0b182362fd679dc01f58f7a1f3fe9fe0183daf3be0d 27686 non-free/dep11/Components-arm64.yml.gz + 7ff5eda9a37e07b9bcfa479c89863d7b2b1aafbedbe4b37ea6c32a16f2eaa241 16392 non-free/dep11/Components-arm64.yml.xz + f54eccd2dbf23fa45cab9e9e7abfafeb667397ea70b6197a3653e8499ffea8bf 271451 non-free/dep11/Components-armel.yml + 5581d7f4c159a5cbd33927294f7fc9918e7deaf04b313001965c83412b6a81f7 27606 non-free/dep11/Components-armel.yml.gz + 0830d150400c82255a52a74f6af9f1a11007bf4b92fc814513f9e13cfac0b22c 16448 non-free/dep11/Components-armel.yml.xz + 15d1524c660c8fb1ee911775a9b59cebbc66843eb97cc0a15a361009f153e6ff 271451 non-free/dep11/Components-armhf.yml + 3fa04d7715c8955987742dc376d10327a975f9583cf656da055d13895e460a67 27691 non-free/dep11/Components-armhf.yml.gz + bbf5a05de96a53c0e10af6019cb7b053b83b0f5def488cde4d8359475adb08da 16364 non-free/dep11/Components-armhf.yml.xz + 716cec6e00d8303375812c8c9be7cbfa5fc858fdb3d9af3f0c72a696d8f7cb2d 280613 non-free/dep11/Components-i386.yml + 40f189b3b3a74bc85652829d0c67b21aad7e60ce389f26fe1959db1e1e8ec48c 31098 non-free/dep11/Components-i386.yml.gz + 18507e0a03c74ed39b9bec853eb9216b458f2fe2b7535c2622c126b9cd35301e 19156 non-free/dep11/Components-i386.yml.xz + d82d6fadb06b6a1f0d36c155b70a02eb2281838aee3ce1b9bf51b7ae06136721 271451 non-free/dep11/Components-mips64el.yml + 25d788e157070218396bafba65ff087551830ba0d0ba3e3cec5342bb150aec57 27765 non-free/dep11/Components-mips64el.yml.gz + 2d0aa3979fd6093dc6de8ba902166a985235c8c4926e07cab7aa2a9b4ad0c11d 16380 non-free/dep11/Components-mips64el.yml.xz + c55445f6f87fd566212bb018f9fae1a4eb43c1a66fe1b0e198b1c7d7e500b009 271451 non-free/dep11/Components-ppc64el.yml + f525af23f1a1eb26ee786c36e2afd4aa5e4102b646f33f8c6788aee395b752bf 27592 non-free/dep11/Components-ppc64el.yml.gz + 0ee03164cca5098ec7c6f98a469818b40b61da7846451cc223d0b9e01585c57c 16576 non-free/dep11/Components-ppc64el.yml.xz + 359af9af71c00d90265395225b75313966435729cf1f6cfb1085fe1721b01e72 271451 non-free/dep11/Components-s390x.yml + 47ef508dff3dfdf17ceeed229d98a2e3992c1a26f28eb328a2d1958d2ddfe070 27558 non-free/dep11/Components-s390x.yml.gz + 181db8b5130910114256e8809ff9a1637efac55b1f33d1f516983521b8d51e7b 16356 non-free/dep11/Components-s390x.yml.xz + 601045de5331d63b7ef2a24f8f74a7452d7be785f94ae6c46002c5dc2608188f 8192 non-free/dep11/icons-128x128.tar + 4fb59feb5d5afe99980ea36c3d7c14577a4b5f11705e7d16524767708666ed54 2394 non-free/dep11/icons-128x128.tar.gz + 977a5470a45ec30f5e230361a446f4692f9cf9bc2abccf6eabac2df0291f1ee4 4096 non-free/dep11/icons-48x48.tar + 07a401f7b03554c2d8ab32dea5885c43b7da7badeea0569b9ce5c9dbbb7cf66f 741 non-free/dep11/icons-48x48.tar.gz + 159551b3012db94a70261cb8f88619a6bb148318da051479ade6df7211c41a34 36864 non-free/dep11/icons-64x64.tar + 872b7437de6fb938db8b26d9de9a3113bc722cd6ed682973151722e2b7a190be 27667 non-free/dep11/icons-64x64.tar.gz + 91e2ce8918b735ed6366ae37eac48af1bc72b6a8639240b7c99d08f8f113fc49 565058 non-free/i18n/Translation-en + f99192bd382fb40eab1ee3d4edc9d6a76e88fef1860617036d1b4d979a9783d6 92546 non-free/i18n/Translation-en.bz2 + e4c0701086e2e2a9492bce9d37de7570b6c1673a5622bc0dfffe80fe2de38985 125 non-free/source/Release + 26b13a0a1125e30beb6d6cf82846f27b16804e1b3f041d5ed12876849a9a31b2 360713 non-free/source/Sources + 6ca8563f5e3dab02cd29607dd487529bc81f5428d1f5e30c5c0f7ff171a1c757 98365 non-free/source/Sources.gz + 2d1811ceac2816f6714190d711637029b7a96b0437fbf628813d543bde1f5a4c 81088 non-free/source/Sources.xz +-----BEGIN PGP SIGNATURE----- + +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 +uxR9LBOV6/LNonq/VtptEQkw +=goR9 +-----END PGP SIGNATURE----- diff --git a/lib/modules/datasource/deb/checksum.spec.ts b/lib/modules/datasource/deb/checksum.spec.ts new file mode 100644 index 00000000000000..6cb46823934a0f --- /dev/null +++ b/lib/modules/datasource/deb/checksum.spec.ts @@ -0,0 +1,57 @@ +import { dir } from 'tmp-promise'; +import { Fixtures } from '../../../../test/fixtures'; +import { GlobalConfig } from '../../../config/global'; +import { createCacheWriteStream } from '../../../util/fs'; +import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; + +const fixtureInRelease = Fixtures.getBinary(`InRelease`).toString(); + +describe('modules/datasource/deb/checksum', () => { + beforeEach(async () => { + const cacheDirResult = await dir({ unsafeCleanup: true }); + const cacheDir = cacheDirResult.path; + + GlobalConfig.set({ cacheDir }); + }); + + describe('parseChecksumsFromInRelease', () => { + it('parses the checksum for the specified package', () => { + const expectedHash = + 'bf77b15e68c5bfd7267c76a34172021de8f10f861f41ebda7b39d1390dd4bf9a'; + expect( + parseChecksumsFromInRelease( + fixtureInRelease, + 'contrib/binary-amd64/Packages.gz', + ), + ).toBe(expectedHash); + + expect( + parseChecksumsFromInRelease( + fixtureInRelease, + 'non-existing/binary-amd64/Packages.gz', + ), + ).toBeUndefined(); + }); + }); + + describe('computeFileChecksum', () => { + it('computes the checksum of a file', async () => { + const stream = createCacheWriteStream('file.txt'); + + const write = new Promise((resolve, reject) => { + stream.write('bar'); + stream.close(resolve); + }); + await write; + + const expectedHash = + 'fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9'; + + expect(await computeFileChecksum('file.txt')).toBe(expectedHash); + }); + + it('should fail if there is an error in the stream', async () => { + await expect(computeFileChecksum('file.txt')).rejects.toThrow(); + }); + }); +}); diff --git a/lib/modules/datasource/deb/checksum.ts b/lib/modules/datasource/deb/checksum.ts new file mode 100644 index 00000000000000..1b95aca1a2aaa2 --- /dev/null +++ b/lib/modules/datasource/deb/checksum.ts @@ -0,0 +1,46 @@ +import crypto from 'crypto'; +import { createCacheReadStream } from '../../../util/fs'; +import { escapeRegExp } from '../../../util/regex'; + +/** + * Parses the SHA256 checksum for a specified package path from the InRelease content. + * + * @param inReleaseContent - content of the InRelease file + * @param packagePath - path of the package file (e.g., 'contrib/binary-amd64/Packages.gz') + * @returns The SHA256 checksum if found, otherwise undefined + */ +export function parseChecksumsFromInRelease( + inReleaseContent: string, + packagePath: string, +): string | undefined { + const lines = inReleaseContent.split('\n'); + const regex = new RegExp( + `([a-f0-9]{64})\\s+\\d+\\s+${escapeRegExp(packagePath)}$`, + ); + + for (const line of lines) { + const match = line.match(regex); + if (match) { + return match[1]; + } + } + + return undefined; +} + +/** + * Computes the SHA256 checksum of a specified file. + * + * @param filePath - path of the file + * @returns resolves to the SHA256 checksum + */ +export function computeFileChecksum(filePath: string): Promise { + return new Promise((resolve, reject) => { + const hash = crypto.createHash('sha256'); + const stream = createCacheReadStream(filePath); + + stream.on('data', (data) => hash.update(data)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', (error) => reject(error)); + }); +} diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 998eca8fb2fcc1..f09563de8a5549 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -1,4 +1,5 @@ import { createHash } from 'crypto'; +import { createReadStream } from 'fs'; import { copyFile, stat } from 'fs-extra'; import { DirectoryResult, dir } from 'tmp-promise'; import upath from 'upath'; @@ -8,6 +9,7 @@ import * as httpMock from '../../../../test/http-mock'; import { fs } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import type { GetPkgReleasesConfig } from '../types'; +import { getBaseReleaseUrl } from './url'; import { DebDatasource } from '.'; const debBaseUrl = 'http://ftp.debian.org'; @@ -22,6 +24,8 @@ describe('modules/datasource/deb/index', () => { let cfg: GetPkgReleasesConfig; let extractionFolder: string; let extractedPackageFile: string; + let fixturePackagesArchiveHash: string; + let fixturePackagesArchiveHash2: string; beforeEach(async () => { jest.resetAllMocks(); @@ -44,6 +48,13 @@ describe('modules/datasource/deb/index', () => { getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'), ], }; + + fixturePackagesArchiveHash = await computeFileChecksum( + fixturePackagesArchivePath, + ); + fixturePackagesArchiveHash2 = await computeFileChecksum( + fixturePackagesArchivePath2, + ); }); describe('getReleases', () => { @@ -98,6 +109,13 @@ describe('modules/datasource/deb/index', () => { .scope(debBaseUrl) .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath); + + mockFetchInReleaseContent( + fixturePackagesArchiveHash, + 'stable', + 'non-free', + 'amd64', + ); }); it('returns a valid version for the package `album`', async () => { @@ -128,6 +146,13 @@ describe('modules/datasource/deb/index', () => { .get(getPackageUrl('', 'stable', 'non-free-second', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath2); + mockFetchInReleaseContent( + fixturePackagesArchiveHash2, + 'stable', + 'non-free-second', + 'amd64', + ); + cfg.registryUrls = [ getRegistryUrl( debBaseUrl, @@ -158,6 +183,13 @@ describe('modules/datasource/deb/index', () => { .scope(debBaseUrl) .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(404); + + mockFetchInReleaseContent( + fixturePackagesArchiveHash, + 'stable', + 'non-free', + 'riscv', + ); }); it('returns null for the package', async () => { @@ -173,6 +205,13 @@ describe('modules/datasource/deb/index', () => { .get(getPackageUrl('', 'stable', 'non-free', 'riscv')) .replyWithFile(200, fixturePackagesArchivePath); + mockFetchInReleaseContent( + fixturePackagesArchiveHash, + 'stable', + 'non-free', + 'riscv', + ); + cfg.registryUrls = [ getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'riscv'), ]; @@ -214,14 +253,47 @@ describe('modules/datasource/deb/index', () => { }); describe('downloadAndExtractPackage', () => { - beforeEach(() => { + it('should throw error when fetching the InRelease content fails', async () => { + mockFetchInReleaseContent( + 'wrong-hash', + 'bullseye', + 'main', + 'amd64', + true, + ); + await expect( + debDatasource.downloadAndExtractPackage( + getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), + ), + ).rejects.toThrow(`No compression standard worked for `); + }); + + it('should throw error when checksum validation fails', async () => { httpMock .scope(debBaseUrl) .get(getPackageUrl('', 'bullseye', 'main', 'amd64')) .replyWithFile(200, fixturePackagesArchivePath2); + mockFetchInReleaseContent('wrong-hash', 'bullseye', 'main', 'amd64'); + + await expect( + debDatasource.downloadAndExtractPackage( + getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), + ), + ).rejects.toThrow(`No compression standard worked for `); }); it('should throw error for unsupported compression', async () => { + httpMock + .scope(debBaseUrl) + .get(getPackageUrl('', 'bullseye', 'main', 'amd64')) + .replyWithFile(200, fixturePackagesArchivePath2); + mockFetchInReleaseContent( + fixturePackagesArchiveHash2, + 'bullseye', + 'main', + 'amd64', + ); + DebDatasource.extract = jest.fn().mockRejectedValueOnce(new Error()); await expect( debDatasource.downloadAndExtractPackage( @@ -262,6 +334,46 @@ describe('modules/datasource/deb/index', () => { }); }); +/** + * Mocks the response for fetching the InRelease file content. + * + * This function sets up a mock HTTP response for a specific InRelease file request. The content includes a SHA256 checksum + * entry for a package index file. It allows simulating both successful and error responses. + * + * @param packageIndexHash - The SHA256 checksum hash of the package index file. + * @param release - The release name (e.g., 'bullseye'). + * @param component - The component name (e.g., 'main'). + * @param arch - The architecture (e.g., 'amd64'). + * @param error - Optional flag to simulate an error response (default is false). + */ +function mockFetchInReleaseContent( + packageIndexHash: string, + release: string, + component: string, + arch: string, + error: boolean = false, +) { + const packageIndexPath = `${component}/binary-${arch}/Packages.gz`; + + const content = `SHA256: + 3957f28db16e3f28c7b34ae84f1c929c567de6970f3f1b95dac9b498dd80fe63 738242 contrib/Contents-all + ${packageIndexHash} 1234 ${packageIndexPath} +`; + + const mockCall = httpMock + .scope(debBaseUrl) + .get( + getBaseReleaseUrl(getComponentUrl('', release, component, arch)) + + '/InRelease', + ); + + if (error) { + mockCall.replyWithError('Unexpected Error'); + } else { + mockCall.reply(200, content); + } +} + /** * Constructs a URL for accessing the component directory for a specific release and architecture. * @@ -315,3 +427,20 @@ function getRegistryUrl( ) { return `${baseUrl}/debian?suite=${release}&components=${components.join(',')}&binaryArch=${arch}`; } + +/** + * Computes the SHA256 checksum of a specified file. + * + * @param filePath - path of the file + * @returns resolves to the SHA256 checksum + */ +function computeFileChecksum(filePath: string): Promise { + return new Promise((resolve, reject) => { + const hash = createHash('sha256'); + const stream = createReadStream(filePath); + + stream.on('data', (data) => hash.update(data)); + stream.on('end', () => resolve(hash.digest('hex'))); + stream.on('error', (error) => reject(error)); + }); +} diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 9f0e999010ca61..fd709d1318228b 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -8,9 +8,10 @@ import * as fs from '../../../util/fs'; import type { HttpOptions } from '../../../util/http/types'; import { Datasource } from '../datasource'; import type { GetReleasesConfig, ReleaseResult } from '../types'; +import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; -import { constructComponentUrls } from './url'; +import { constructComponentUrls, getBaseReleaseUrl } from './url'; export class DebDatasource extends Datasource { static readonly id = 'deb'; @@ -119,12 +120,27 @@ export class DebDatasource extends Datasource { `${packageUrlHash}.${compression}`, ); - const wasUpdated = await this.downloadPackageFile( - componentUrl, - compression, - compressedFile, - lastTimestamp, - ); + let wasUpdated = false; + + try { + wasUpdated = await this.downloadPackageFile( + componentUrl, + compression, + compressedFile, + lastTimestamp, + ); + } catch (error) { + logger.error( + { + compression, + error: error.message, + }, + `Failed to download package file from ${componentUrl}`, + ); + + continue; + } + if (wasUpdated || !lastTimestamp) { try { await DebDatasource.extract( @@ -173,6 +189,7 @@ export class DebDatasource extends Datasource { compressedFile: string, lastDownloadTimestamp?: Date, ): Promise { + const baseReleaseUrl = getBaseReleaseUrl(basePackageUrl); const packageUrl = `${basePackageUrl}/Packages.${compression}`; let needsToDownload = true; @@ -184,19 +201,26 @@ export class DebDatasource extends Datasource { } if (needsToDownload) { - try { - const readStream = this.http.stream(packageUrl); - const writeStream = fs.createCacheWriteStream(compressedFile); - await fs.pipeline(readStream, writeStream); - logger.debug( - { url: packageUrl, targetFile: compressedFile }, - 'Downloading Debian package file', - ); - } catch (error) { - logger.error( - `Failed to download package file from ${packageUrl}: ${error.message}`, - ); - needsToDownload = false; + const inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); + const expectedChecksum = parseChecksumsFromInRelease( + inReleaseContent, + // path to the Package.gz file + packageUrl.replace(`${baseReleaseUrl}/`, ''), + ); + + const readStream = this.http.stream(packageUrl); + const writeStream = fs.createCacheWriteStream(compressedFile); + await fs.pipeline(readStream, writeStream); + logger.debug( + { url: packageUrl, targetFile: compressedFile }, + 'Downloading Debian package file', + ); + + const actualChecksum = await computeFileChecksum(compressedFile); + + if (actualChecksum !== expectedChecksum) { + await fs.rmCache(compressedFile); + throw new Error('SHA256 checksum validation failed'); } } else { logger.debug(`No need to download ${packageUrl}, file is up to date.`); @@ -205,6 +229,26 @@ export class DebDatasource extends Datasource { return needsToDownload; } + /** + * Fetches the content of the InRelease file from the given base release URL. + * + * @param baseReleaseUrl - The base URL of the release (e.g., 'https://ftp.debian.org/debian/dists/bullseye'). + * @returns resolves to the content of the InRelease file. + * @throws An error if the InRelease file could not be downloaded. + */ + async fetchInReleaseFile(baseReleaseUrl: string): Promise { + const inReleaseUrl = `${baseReleaseUrl}/InRelease`; + try { + const response = await this.http.get(inReleaseUrl); + return response.body; + } catch (error) { + logger.error( + `Failed to download InRelease file from ${inReleaseUrl}: ${error.message}`, + ); + throw new Error('InRelease download failed'); + } + } + /** * Checks if a packageUrl content has been modified since the specified timestamp. * diff --git a/lib/modules/datasource/deb/url.spec.ts b/lib/modules/datasource/deb/url.spec.ts index 3d3aa1386272f5..d56b4cc630adcd 100644 --- a/lib/modules/datasource/deb/url.spec.ts +++ b/lib/modules/datasource/deb/url.spec.ts @@ -1,4 +1,4 @@ -import { constructComponentUrls } from './url'; +import { constructComponentUrls, getBaseReleaseUrl } from './url'; describe('modules/datasource/deb/url', () => { it('constructs URLs correctly from registry URL with suite', () => { @@ -29,4 +29,12 @@ describe('modules/datasource/deb/url', () => { 'Missing required query parameter', ); }); + + it('returns the correct release url', () => { + const basePackageUrl = + 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64'; + const expectedUrl = 'https://ftp.debian.org/debian/dists/bullseye'; + + expect(getBaseReleaseUrl(basePackageUrl)).toBe(expectedUrl); + }); }); diff --git a/lib/modules/datasource/deb/url.ts b/lib/modules/datasource/deb/url.ts index 37af1859794d4e..01fe73ea116c10 100644 --- a/lib/modules/datasource/deb/url.ts +++ b/lib/modules/datasource/deb/url.ts @@ -1,5 +1,20 @@ import { joinUrlParts } from '../../../util/url'; +/** + * Extracts the base release URL from a package URL by removing the last two path segments. + * + * @param basePackageUrl - The base URL of the package. + * @returns The base release URL. + * + * @example + * // Returns 'https://ftp.debian.org/debian/dists/bullseye' + * getBaseReleaseUrl('https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64'); + */ +export function getBaseReleaseUrl(basePackageUrl: string): string { + const urlParts = basePackageUrl.split('/'); + return urlParts.slice(0, urlParts.length - 2).join('/'); +} + /** * Constructs the component URLs from the given registry URL. * From af252ea9dd1c9bdf1c31851e5276ed64626968e8 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:23:40 +0200 Subject: [PATCH 20/62] Apply suggestions from code review Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/checksum.ts | 8 ++++---- lib/modules/datasource/deb/index.ts | 22 ++++++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/lib/modules/datasource/deb/checksum.ts b/lib/modules/datasource/deb/checksum.ts index 1b95aca1a2aaa2..ffcdb7777e610f 100644 --- a/lib/modules/datasource/deb/checksum.ts +++ b/lib/modules/datasource/deb/checksum.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { createCacheReadStream } from '../../../util/fs'; -import { escapeRegExp } from '../../../util/regex'; +import { escapeRegExp, regEx } from '../../../util/regex'; /** * Parses the SHA256 checksum for a specified package path from the InRelease content. @@ -12,9 +12,9 @@ import { escapeRegExp } from '../../../util/regex'; export function parseChecksumsFromInRelease( inReleaseContent: string, packagePath: string, -): string | undefined { +): string | null { const lines = inReleaseContent.split('\n'); - const regex = new RegExp( + const regex = regEx( `([a-f0-9]{64})\\s+\\d+\\s+${escapeRegExp(packagePath)}$`, ); @@ -25,7 +25,7 @@ export function parseChecksumsFromInRelease( } } - return undefined; + return null; } /** diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index fd709d1318228b..a9f2430607b43e 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -294,7 +294,7 @@ export class DebDatasource extends Datasource { async parseExtractedPackageIndex( extractedFile: string, lastTimestamp: Date, - ): Promise { + ): Promise> { // read line by line to avoid high memory consumption as the extracted Packages // files can be multiple MBs in size const rl = readline.createInterface({ @@ -303,13 +303,13 @@ export class DebDatasource extends Datasource { }); let currentPackage: PackageDescription = {}; - const allPackages: PackageDescription[] = []; + const allPackages: Record = {} for await (const line of rl) { if (line === '') { // All information of the package are available, add to the list of packages if (Object.keys(currentPackage).length > 0) { - allPackages.push(currentPackage); + allPackages[currentPackage.Package!] = currentPackage; currentPackage = {}; } } else { @@ -324,12 +324,26 @@ export class DebDatasource extends Datasource { // Check the last package after file reading is complete if (Object.keys(currentPackage).length > 0) { - allPackages.push(currentPackage); + allPackages[currentPackage.Package!] = currentPackage; } return allPackages; } + @cache({ + namespace: `datasource-${DebDatasource.id}-package`, + key: (componentUrl: string) => componentUrl, + }) + async getPackageIndex(componentUrl: string): Promise> { + const { extractedFile, lastTimestamp } = + await this.downloadAndExtractPackage(componentUrl); + return await this.parseExtractedPackageIndex( + extractedFile, + lastTimestamp, + ); + } +} + /** * Fetches the release information for a given package from the registry URL. * From 035179fe05810bde68c60b9b89c1013c8f4643c0 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:26:06 +0200 Subject: [PATCH 21/62] Apply suggestions from code review Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index a9f2430607b43e..d792155feb4e49 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -308,7 +308,7 @@ export class DebDatasource extends Datasource { for await (const line of rl) { if (line === '') { // All information of the package are available, add to the list of packages - if (Object.keys(currentPackage).length > 0) { + if (!DebDatasource.requiredPackageKeys.some((key) => !(key in currentPackage))) { allPackages[currentPackage.Package!] = currentPackage; currentPackage = {}; } @@ -364,15 +364,8 @@ export class DebDatasource extends Datasource { for (const componentUrl of componentUrls) { try { - const { extractedFile, lastTimestamp } = - await this.downloadAndExtractPackage(componentUrl); - const parsedPackages = await this.parseExtractedPackageIndex( - extractedFile, - lastTimestamp, - ); - const parsedPackage = parsedPackages.find( - (p) => p.Package === packageName, - ); + const packageIndex = await this.getPackageIndex(componentUrl) + const parsedPackage = packageIndex[packageName]; if (parsedPackage) { const newRelease = formatReleaseResult(parsedPackage); From 9ea71602360f0318dfba25d42fb562695175965b Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:35:08 +0200 Subject: [PATCH 22/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index f09563de8a5549..e1ae74277c80c5 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -69,12 +69,19 @@ describe('modules/datasource/deb/index', () => { .reply(304); const res = await getPkgReleases(cfg); - expect(res).toBeObject(); - expect(res!.releases).toHaveLength(1); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + releases: [ + { + version: '4.15-1', + }, + ], + }); const modifiedTs = httpMock.getTrace()[0].headers['if-modified-since']; - expect(modifiedTs).toBeDefined(); - expect(modifiedTs).toEqual(ts.toUTCString()); + expect(modifiedTs).toBe(ts.toUTCString()); }); describe('parsing of registry url', () => { From d2fdad7aedaa2c0dd79a90a66ed45ad2d885db14 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:35:28 +0200 Subject: [PATCH 23/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index e1ae74277c80c5..3d46fa91bc81f5 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -127,8 +127,16 @@ describe('modules/datasource/deb/index', () => { it('returns a valid version for the package `album`', async () => { const res = await getPkgReleases(cfg); - expect(res).toBeObject(); - expect(res!.releases).toHaveLength(1); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + releases: [ + { + version: '4.15-1', + }, + ], + }); }); it('returns a valid version for the package `album` if release is used in the registryUrl', async () => { From f97ace5ad52ec97e12aaf86cda5fe9af97423488 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:35:42 +0200 Subject: [PATCH 24/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 3d46fa91bc81f5..4c9c85ef96d7b3 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -144,8 +144,16 @@ describe('modules/datasource/deb/index', () => { getRegistryUrl(debBaseUrl, 'stable', ['non-free'], 'amd64'), ]; const res = await getPkgReleases(cfg); - expect(res).toBeObject(); - expect(res!.releases).toHaveLength(1); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + releases: [ + { + version: '4.15-1', + }, + ], + }); }); it('returns null for an unknown package', async () => { From 3110cb9660b788a0e3765b5b7905637246819085 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:36:00 +0200 Subject: [PATCH 25/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 4c9c85ef96d7b3..5c138832ccbb25 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -188,8 +188,19 @@ describe('modules/datasource/deb/index', () => { it('returns two releases for `album` which is the same across the components', async () => { const res = await getPkgReleases(cfg); - expect(res).toBeObject(); - expect(res!.releases).toHaveLength(2); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + releases: [ + { + version: '4.14-1', + }, + { + version: '4.15-1', + }, + ], + }); }); it('returns two releases for `album` which has different metadata across the components', async () => { From 44e87be84bcf1f9fd897473f484ce70379033e1f Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:36:13 +0200 Subject: [PATCH 26/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 5c138832ccbb25..f34e98834b5e4d 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -206,7 +206,19 @@ describe('modules/datasource/deb/index', () => { it('returns two releases for `album` which has different metadata across the components', async () => { cfg.packageName = 'album'; const res = await getPkgReleases(cfg); - expect(res?.releases).toHaveLength(2); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + releases: [ + { + version: '4.14-1', + }, + { + version: '4.15-1', + }, + ], + }); }); }); }); From 8aea12682e384bd1c451b8876c01105aa45d8a60 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:36:26 +0200 Subject: [PATCH 27/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index f34e98834b5e4d..fbf89f4cabe012 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -263,8 +263,16 @@ describe('modules/datasource/deb/index', () => { ]; const res = await getPkgReleases(cfg); - expect(res).toBeObject(); - expect(res!.releases).toHaveLength(1); + expect(res).toEqual({ + homepage: 'http://marginalhacks.com/Hacks/album', + registryUrl: + 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv', + releases: [ + { + version: '4.15-1', + }, + ], + }); }); }); From f07f390a98cf8d681cd8038813546a8dce8478d4 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:36:48 +0200 Subject: [PATCH 28/62] Update lib/modules/datasource/deb/index.spec.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.spec.ts | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index fbf89f4cabe012..81552bc931633f 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -285,12 +285,23 @@ describe('modules/datasource/deb/index', () => { new Date(), ); - const firstPackage = parsedPackages.find((p) => p.Package === 'album'); - const lastPackage = parsedPackages.find( - (p) => p.Package === 'alien-arena-data', - ); - expect(firstPackage?.Version).toBe('4.15-1'); - expect(lastPackage?.Version).toBe('7.71.3+ds-1'); + expect(parsedPackages).toEqual({ + album: { + Homepage: 'http://marginalhacks.com/Hacks/album', + Package: 'album', + Version: '4.15-1', + }, + 'album-data': { + Homepage: 'http://marginalhacks.com/Hacks/album', + Package: 'album-data', + Version: '4.05-7.2', + }, + 'alien-arena-data': { + Homepage: 'https://martianbackup.com', + Package: 'alien-arena-data', + Version: '7.71.3+ds-1', + }, + }); }); }); From df5198cb2c6a0305c3897ee7166daa7e62895a4d Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 10:47:34 +0200 Subject: [PATCH 29/62] fix tests --- lib/modules/datasource/deb/checksum.spec.ts | 2 +- lib/modules/datasource/deb/index.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/modules/datasource/deb/checksum.spec.ts b/lib/modules/datasource/deb/checksum.spec.ts index 6cb46823934a0f..4c33aba5abd190 100644 --- a/lib/modules/datasource/deb/checksum.spec.ts +++ b/lib/modules/datasource/deb/checksum.spec.ts @@ -30,7 +30,7 @@ describe('modules/datasource/deb/checksum', () => { fixtureInRelease, 'non-existing/binary-amd64/Packages.gz', ), - ).toBeUndefined(); + ).toBeNull(); }); }); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index d792155feb4e49..4ec1a6e8c74fd9 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -303,12 +303,16 @@ export class DebDatasource extends Datasource { }); let currentPackage: PackageDescription = {}; - const allPackages: Record = {} + const allPackages: Record = {}; for await (const line of rl) { if (line === '') { // All information of the package are available, add to the list of packages - if (!DebDatasource.requiredPackageKeys.some((key) => !(key in currentPackage))) { + if ( + !DebDatasource.requiredPackageKeys.some( + (key) => !(key in currentPackage), + ) + ) { allPackages[currentPackage.Package!] = currentPackage; currentPackage = {}; } @@ -334,15 +338,13 @@ export class DebDatasource extends Datasource { namespace: `datasource-${DebDatasource.id}-package`, key: (componentUrl: string) => componentUrl, }) - async getPackageIndex(componentUrl: string): Promise> { + async getPackageIndex( + componentUrl: string, + ): Promise> { const { extractedFile, lastTimestamp } = await this.downloadAndExtractPackage(componentUrl); - return await this.parseExtractedPackageIndex( - extractedFile, - lastTimestamp, - ); + return await this.parseExtractedPackageIndex(extractedFile, lastTimestamp); } -} /** * Fetches the release information for a given package from the registry URL. @@ -364,7 +366,7 @@ export class DebDatasource extends Datasource { for (const componentUrl of componentUrls) { try { - const packageIndex = await this.getPackageIndex(componentUrl) + const packageIndex = await this.getPackageIndex(componentUrl); const parsedPackage = packageIndex[packageName]; if (parsedPackage) { From 3297e335d09561c49799859ccb12eee610a4b0a2 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 10 Aug 2024 11:05:33 +0200 Subject: [PATCH 30/62] improve description --- lib/modules/datasource/deb/types.ts | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/modules/datasource/deb/types.ts b/lib/modules/datasource/deb/types.ts index b2e4082050ab73..5b404c31ef8e9e 100644 --- a/lib/modules/datasource/deb/types.ts +++ b/lib/modules/datasource/deb/types.ts @@ -1,11 +1,30 @@ /** - * A package file contains multiple package descriptions which are each separated by an completely empty line. - * A package description contains meta data properties in the form of + * Represents the structure of a package description extracted from a Package Index file. * + * A Package Index file contains multiple package descriptions, with each package description + * separated by a completely empty line. Each package description provides meta-data + * about a specific package, with properties in the following format: + * + * ``` * PropertyName: value + * ``` + * + * @example + * + * ``` + * Package: album + * Version: 4.15-1 + * Homepage: http://marginalhacks.com/Hacks/album + * + * Package: album-data + * Version: 4.05-7.2 + * Homepage: http://marginalhacks.com/Hacks/album + * ``` + * + * Some property are optional and may not be present in a package description. */ export interface PackageDescription { - Package?: string; // Package - Version?: string; // Version - Homepage?: string; // Homepage + Package?: string; + Version?: string; + Homepage?: string; } From 1f1d4cc2bca12a83b27c5279cf3568770f1dcaf5 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:41:02 +0200 Subject: [PATCH 31/62] Apply suggestions from code review Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/index.ts | 51 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 4ec1a6e8c74fd9..36d6d8b91f9ace 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -190,7 +190,7 @@ export class DebDatasource extends Datasource { lastDownloadTimestamp?: Date, ): Promise { const baseReleaseUrl = getBaseReleaseUrl(basePackageUrl); - const packageUrl = `${basePackageUrl}/Packages.${compression}`; + const packageUrl = joinUrlParts(basePackageUrl, `Packages.${compression}`); let needsToDownload = true; if (lastDownloadTimestamp) { @@ -200,14 +200,10 @@ export class DebDatasource extends Datasource { ); } - if (needsToDownload) { - const inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); - const expectedChecksum = parseChecksumsFromInRelease( - inReleaseContent, - // path to the Package.gz file - packageUrl.replace(`${baseReleaseUrl}/`, ''), - ); - + if (!needsToDownload) { + logger.debug(`No need to download ${packageUrl}, file is up to date.`); + return false; + } const readStream = this.http.stream(packageUrl); const writeStream = fs.createCacheWriteStream(compressedFile); await fs.pipeline(readStream, writeStream); @@ -218,14 +214,24 @@ export class DebDatasource extends Datasource { const actualChecksum = await computeFileChecksum(compressedFile); - if (actualChecksum !== expectedChecksum) { - await fs.rmCache(compressedFile); - throw new Error('SHA256 checksum validation failed'); + try { + const inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); + const expectedChecksum = parseChecksumsFromInRelease( + inReleaseContent, + // path to the Package.gz file + packageUrl.replace(`${baseReleaseUrl}/`, ''), + ); + if (actualChecksum !== expectedChecksum) { + logger.debug( + { url: baseReleaseUrl }, + 'SHA256 checksum validation failed', + ); + return false; + } + } catch (error) { + // This is expected to fail for Artifactory if GPG verification is not enabled + logger.debug({ url: baseReleaseUrl }, 'Could not fetch InRelease file'); } - } else { - logger.debug(`No need to download ${packageUrl}, file is up to date.`); - } - return needsToDownload; } @@ -237,16 +243,9 @@ export class DebDatasource extends Datasource { * @throws An error if the InRelease file could not be downloaded. */ async fetchInReleaseFile(baseReleaseUrl: string): Promise { - const inReleaseUrl = `${baseReleaseUrl}/InRelease`; - try { - const response = await this.http.get(inReleaseUrl); - return response.body; - } catch (error) { - logger.error( - `Failed to download InRelease file from ${inReleaseUrl}: ${error.message}`, - ); - throw new Error('InRelease download failed'); - } + const inReleaseUrl = joinUrlParts(baseReleaseUrl, 'InRelease'); + const response = await this.http.get(inReleaseUrl); + return response.body; } /** From b816ecdca9c27ebda5995e0c1207130a19eeda53 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:20:46 +0200 Subject: [PATCH 32/62] fix suggestion --- lib/modules/datasource/deb/index.spec.ts | 35 ++++++++------- lib/modules/datasource/deb/index.ts | 54 +++++++++++++----------- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 81552bc931633f..dddb3325a611c7 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -229,13 +229,6 @@ describe('modules/datasource/deb/index', () => { .scope(debBaseUrl) .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(404); - - mockFetchInReleaseContent( - fixturePackagesArchiveHash, - 'stable', - 'non-free', - 'riscv', - ); }); it('returns null for the package', async () => { @@ -318,19 +311,29 @@ describe('modules/datasource/deb/index', () => { }); describe('downloadAndExtractPackage', () => { - it('should throw error when fetching the InRelease content fails', async () => { - mockFetchInReleaseContent( - 'wrong-hash', - 'bullseye', - 'main', + it('should ignore error when fetching the InRelease content fails', async () => { + const packageArgs: [release: string, component: string, arch: string] = [ + 'stable', + 'non-free', 'amd64', - true, - ); + ]; + + httpMock + .scope(debBaseUrl) + .get(getPackageUrl('', ...packageArgs)) + .replyWithFile(200, fixturePackagesArchivePath2); + mockFetchInReleaseContent('wrong-hash', ...packageArgs, true); + await expect( debDatasource.downloadAndExtractPackage( - getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), + getComponentUrl(debBaseUrl, ...packageArgs), ), - ).rejects.toThrow(`No compression standard worked for `); + ).resolves.toEqual( + expect.objectContaining({ + extractedFile: extractedPackageFile, + lastTimestamp: expect.anything(), + }), + ); }); it('should throw error when checksum validation fails', async () => { diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 36d6d8b91f9ace..5734f6212a841e 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -6,6 +6,7 @@ import { logger } from '../../../logger'; import { cache } from '../../../util/cache/package/decorator'; import * as fs from '../../../util/fs'; import type { HttpOptions } from '../../../util/http/types'; +import { joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; @@ -204,34 +205,37 @@ export class DebDatasource extends Datasource { logger.debug(`No need to download ${packageUrl}, file is up to date.`); return false; } - const readStream = this.http.stream(packageUrl); - const writeStream = fs.createCacheWriteStream(compressedFile); - await fs.pipeline(readStream, writeStream); - logger.debug( - { url: packageUrl, targetFile: compressedFile }, - 'Downloading Debian package file', - ); + const readStream = this.http.stream(packageUrl); + const writeStream = fs.createCacheWriteStream(compressedFile); + await fs.pipeline(readStream, writeStream); + logger.debug( + { url: packageUrl, targetFile: compressedFile }, + 'Downloading Debian package file', + ); - const actualChecksum = await computeFileChecksum(compressedFile); + const actualChecksum = await computeFileChecksum(compressedFile); - try { - const inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); - const expectedChecksum = parseChecksumsFromInRelease( - inReleaseContent, - // path to the Package.gz file - packageUrl.replace(`${baseReleaseUrl}/`, ''), - ); - if (actualChecksum !== expectedChecksum) { - logger.debug( - { url: baseReleaseUrl }, - 'SHA256 checksum validation failed', - ); - return false; - } - } catch (error) { - // This is expected to fail for Artifactory if GPG verification is not enabled - logger.debug({ url: baseReleaseUrl }, 'Could not fetch InRelease file'); + let inReleaseContent = ''; + + try { + inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); + } catch (error) { + // This is expected to fail for Artifactory if GPG verification is not enabled + logger.debug({ url: baseReleaseUrl }, 'Could not fetch InRelease file'); + } + + if (inReleaseContent) { + const expectedChecksum = parseChecksumsFromInRelease( + inReleaseContent, + // path to the Package.gz file + packageUrl.replace(`${baseReleaseUrl}/`, ''), + ); + if (actualChecksum !== expectedChecksum) { + await fs.rmCache(compressedFile); + throw new Error('SHA256 checksum validation failed'); } + } + return needsToDownload; } From 2f6fb802d0d9843a0152675de1d5761ef3f7d20a Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 17 Aug 2024 13:03:49 +0200 Subject: [PATCH 33/62] use deb cdn instead --- lib/modules/datasource/deb/index.spec.ts | 14 +++++++------- lib/modules/datasource/deb/index.ts | 6 +++--- lib/modules/datasource/deb/readme.md | 2 +- lib/modules/datasource/deb/url.spec.ts | 18 +++++++++--------- lib/modules/datasource/deb/url.ts | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index dddb3325a611c7..07687b2b43fc77 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -12,7 +12,7 @@ import type { GetPkgReleasesConfig } from '../types'; import { getBaseReleaseUrl } from './url'; import { DebDatasource } from '.'; -const debBaseUrl = 'http://ftp.debian.org'; +const debBaseUrl = 'http://deb.debian.org'; const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); @@ -72,7 +72,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + 'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', releases: [ { version: '4.15-1', @@ -130,7 +130,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + 'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', releases: [ { version: '4.15-1', @@ -147,7 +147,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', + 'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=amd64', releases: [ { version: '4.15-1', @@ -191,7 +191,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + 'http://deb.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', releases: [ { version: '4.14-1', @@ -209,7 +209,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', + 'http://deb.debian.org/debian?suite=stable&components=non-free,non-free-second&binaryArch=amd64', releases: [ { version: '4.14-1', @@ -259,7 +259,7 @@ describe('modules/datasource/deb/index', () => { expect(res).toEqual({ homepage: 'http://marginalhacks.com/Hacks/album', registryUrl: - 'http://ftp.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv', + 'http://deb.debian.org/debian?suite=stable&components=non-free&binaryArch=riscv', releases: [ { version: '4.15-1', diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 5734f6212a841e..e95ca69386ed34 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -51,10 +51,10 @@ export class DebDatasource extends Datasource { * - components: comma separated list of components * - suite: stable, oldstable or other alias for a release, either this or release must be given * - release: buster, etc. - * - binaryArch: e.g. amd64 resolves to http://ftp.debian.org/debian/dists/stable/non-free/binary-amd64/ + * - binaryArch: e.g. amd64 resolves to http://deb.debian.org/debian/dists/stable/non-free/binary-amd64/ */ override readonly defaultRegistryUrls = [ - 'https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64', + 'https://deb.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64', ]; override readonly defaultVersioning = 'deb'; @@ -242,7 +242,7 @@ export class DebDatasource extends Datasource { /** * Fetches the content of the InRelease file from the given base release URL. * - * @param baseReleaseUrl - The base URL of the release (e.g., 'https://ftp.debian.org/debian/dists/bullseye'). + * @param baseReleaseUrl - The base URL of the release (e.g., 'https://deb.debian.org/debian/dists/bullseye'). * @returns resolves to the content of the InRelease file. * @throws An error if the InRelease file could not be downloaded. */ diff --git a/lib/modules/datasource/deb/readme.md b/lib/modules/datasource/deb/readme.md index cf216dd31f175e..d46c68e48a0318 100644 --- a/lib/modules/datasource/deb/readme.md +++ b/lib/modules/datasource/deb/readme.md @@ -12,7 +12,7 @@ To use a Debian repository with the datasource, you need a properly formatted UR **Example**: ``` -https://ftp.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64 +https://deb.debian.org/debian?suite=stable&components=main,contrib,non-free&binaryArch=amd64 ``` This URL points to the `stable` suite of the Debian repository for `amd64` architecture, including `main`, `contrib`, and `non-free` components. diff --git a/lib/modules/datasource/deb/url.spec.ts b/lib/modules/datasource/deb/url.spec.ts index d56b4cc630adcd..f0ca42eb5caff7 100644 --- a/lib/modules/datasource/deb/url.spec.ts +++ b/lib/modules/datasource/deb/url.spec.ts @@ -3,10 +3,10 @@ import { constructComponentUrls, getBaseReleaseUrl } from './url'; describe('modules/datasource/deb/url', () => { it('constructs URLs correctly from registry URL with suite', () => { const registryUrl = - 'https://ftp.debian.org/debian?suite=stable&components=main,contrib&binaryArch=amd64'; + 'https://deb.debian.org/debian?suite=stable&components=main,contrib&binaryArch=amd64'; const expectedUrls = [ - 'https://ftp.debian.org/debian/dists/stable/main/binary-amd64', - 'https://ftp.debian.org/debian/dists/stable/contrib/binary-amd64', + 'https://deb.debian.org/debian/dists/stable/main/binary-amd64', + 'https://deb.debian.org/debian/dists/stable/contrib/binary-amd64', ]; const componentUrls = constructComponentUrls(registryUrl); expect(componentUrls).toEqual(expectedUrls); @@ -14,17 +14,17 @@ describe('modules/datasource/deb/url', () => { it('constructs URLs correctly from registry URL with release', () => { const registryUrl = - 'https://ftp.debian.org/debian?release=bullseye&components=main,contrib&binaryArch=amd64'; + 'https://deb.debian.org/debian?release=bullseye&components=main,contrib&binaryArch=amd64'; const expectedUrls = [ - 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64', - 'https://ftp.debian.org/debian/dists/bullseye/contrib/binary-amd64', + 'https://deb.debian.org/debian/dists/bullseye/main/binary-amd64', + 'https://deb.debian.org/debian/dists/bullseye/contrib/binary-amd64', ]; const componentUrls = constructComponentUrls(registryUrl); expect(componentUrls).toEqual(expectedUrls); }); it('throws an error if required parameters are missing', () => { - const registryUrl = 'https://ftp.debian.org/debian?components=main,contrib'; + const registryUrl = 'https://deb.debian.org/debian?components=main,contrib'; expect(() => constructComponentUrls(registryUrl)).toThrow( 'Missing required query parameter', ); @@ -32,8 +32,8 @@ describe('modules/datasource/deb/url', () => { it('returns the correct release url', () => { const basePackageUrl = - 'https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64'; - const expectedUrl = 'https://ftp.debian.org/debian/dists/bullseye'; + 'https://deb.debian.org/debian/dists/bullseye/main/binary-amd64'; + const expectedUrl = 'https://deb.debian.org/debian/dists/bullseye'; expect(getBaseReleaseUrl(basePackageUrl)).toBe(expectedUrl); }); diff --git a/lib/modules/datasource/deb/url.ts b/lib/modules/datasource/deb/url.ts index 01fe73ea116c10..87a6891d16efa5 100644 --- a/lib/modules/datasource/deb/url.ts +++ b/lib/modules/datasource/deb/url.ts @@ -7,8 +7,8 @@ import { joinUrlParts } from '../../../util/url'; * @returns The base release URL. * * @example - * // Returns 'https://ftp.debian.org/debian/dists/bullseye' - * getBaseReleaseUrl('https://ftp.debian.org/debian/dists/bullseye/main/binary-amd64'); + * // Returns 'https://deb.debian.org/debian/dists/bullseye' + * getBaseReleaseUrl('https://deb.debian.org/debian/dists/bullseye/main/binary-amd64'); */ export function getBaseReleaseUrl(basePackageUrl: string): string { const urlParts = basePackageUrl.split('/'); From f0765c4984722ebaedbfa4376d2b7523ca392d61 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:25:49 +0200 Subject: [PATCH 34/62] fix linting issue, add test for parallel lookup, fix lookup for packages without homepage --- lib/modules/datasource/deb/index.spec.ts | 81 ++++++++++++++++++++---- lib/modules/datasource/deb/index.ts | 18 ++++-- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 07687b2b43fc77..40f925c254fdd8 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -17,6 +17,8 @@ const debBaseUrl = 'http://deb.debian.org'; const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); const fixturePackagesPath = Fixtures.getPath(`Packages`); +let fixturePackagesArchiveHash: string; +let fixturePackagesArchiveHash2: string; describe('modules/datasource/deb/index', () => { let debDatasource: DebDatasource; @@ -24,8 +26,6 @@ describe('modules/datasource/deb/index', () => { let cfg: GetPkgReleasesConfig; let extractionFolder: string; let extractedPackageFile: string; - let fixturePackagesArchiveHash: string; - let fixturePackagesArchiveHash2: string; beforeEach(async () => { jest.resetAllMocks(); @@ -112,17 +112,7 @@ describe('modules/datasource/deb/index', () => { describe('without local version', () => { beforeEach(() => { - httpMock - .scope(debBaseUrl) - .get(getPackageUrl('', 'stable', 'non-free', 'amd64')) - .replyWithFile(200, fixturePackagesArchivePath); - - mockFetchInReleaseContent( - fixturePackagesArchiveHash, - 'stable', - 'non-free', - 'amd64', - ); + mockHttpCalls('stable', 'non-free', 'amd64', false); }); it('returns a valid version for the package `album`', async () => { @@ -267,6 +257,33 @@ describe('modules/datasource/deb/index', () => { ], }); }); + + it('should not lead to a race condition on parallel lookups', async () => { + const packages = [ + 'album', + 'album-data', + 'alien-arena-data', + 'amiwm', + 'arb', + 'arb-common', + 'libfaac-dev', + 'amoeba-data', + ]; + + for (let i = 0; i < packages.length; i++) { + // first call doesn't include a http head call, since the file doesn't exists locally yet + // the package index is downloaded every time since the http head call returns 200 + mockHttpCalls('stable', 'non-free', 'amd64', !!i); + } + + const results = await Promise.all( + packages.map((packageName) => getPkgReleases({ ...cfg, packageName })), + ); + + for (const result of results) { + expect(result?.releases?.length).toBe(1); + } + }); }); describe('parseExtractedPackageIndex', () => { @@ -402,6 +419,44 @@ describe('modules/datasource/deb/index', () => { }); }); +/** + * Mocks several http calls for the in parallel lookup test + * + * - Mocks the http get call for the Package Index file + * - Mocks the http get call for the InRelease file + * - Mocks the http head call for Package Index file (returns 200) + * + * @param release - The release name (e.g., 'bullseye'). + * @param component - The component name (e.g., 'main'). + * @param arch - The architecture (e.g., 'amd64'). + * @param checkIfModified - whether it should mock the http head call of the Package Index file + */ +function mockHttpCalls( + release: string, + component: string, + arch: string, + checkIfModified: boolean, +) { + httpMock + .scope(debBaseUrl) + .get(getPackageUrl('', release, component, arch)) + .replyWithFile(200, fixturePackagesArchivePath); + + mockFetchInReleaseContent( + fixturePackagesArchiveHash, + release, + component, + arch, + ); + + if (checkIfModified) { + httpMock + .scope(debBaseUrl) + .head(getPackageUrl('', release, component, arch)) + .reply(200); + } +} + /** * Mocks the response for fetching the InRelease file content. * diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index e95ca69386ed34..b17c428f4a577a 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -13,6 +13,7 @@ import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; import { constructComponentUrls, getBaseReleaseUrl } from './url'; +import { nanoid } from 'nanoid'; export class DebDatasource extends Datasource { static readonly id = 'deb'; @@ -62,6 +63,10 @@ export class DebDatasource extends Datasource { static requiredPackageKeys: Array = [ 'Package', 'Version', + ]; + + static packagesKeys: Array = [ + ...DebDatasource.requiredPackageKeys, 'Homepage', ]; @@ -118,7 +123,7 @@ export class DebDatasource extends Datasource { for (const compression of DebDatasource.compressions) { const compressedFile = upath.join( fullCacheDir, - `${packageUrlHash}.${compression}`, + `${nanoid()}_${packageUrlHash}.${compression}`, ); let wasUpdated = false; @@ -221,7 +226,10 @@ export class DebDatasource extends Datasource { inReleaseContent = await this.fetchInReleaseFile(baseReleaseUrl); } catch (error) { // This is expected to fail for Artifactory if GPG verification is not enabled - logger.debug({ url: baseReleaseUrl }, 'Could not fetch InRelease file'); + logger.debug( + { url: baseReleaseUrl, error }, + 'Could not fetch InRelease file', + ); } if (inReleaseContent) { @@ -312,15 +320,15 @@ export class DebDatasource extends Datasource { if (line === '') { // All information of the package are available, add to the list of packages if ( - !DebDatasource.requiredPackageKeys.some( - (key) => !(key in currentPackage), + DebDatasource.requiredPackageKeys.every( + (key) => key in currentPackage, ) ) { allPackages[currentPackage.Package!] = currentPackage; currentPackage = {}; } } else { - for (const key of DebDatasource.requiredPackageKeys) { + for (const key of DebDatasource.packagesKeys) { if (line.startsWith(`${key}:`)) { currentPackage[key] = line.substring(key.length + 1).trim(); break; From 0e4707c1c3838baa9eaa1f41cf6a8f39ff217c56 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 17 Aug 2024 16:37:17 +0200 Subject: [PATCH 35/62] fix import order --- lib/modules/datasource/deb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index b17c428f4a577a..504238c5672c88 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -1,6 +1,7 @@ import { createHash } from 'crypto'; import readline from 'readline'; import { createUnzip } from 'zlib'; +import { nanoid } from 'nanoid'; import upath from 'upath'; import { logger } from '../../../logger'; import { cache } from '../../../util/cache/package/decorator'; @@ -13,7 +14,6 @@ import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; import { constructComponentUrls, getBaseReleaseUrl } from './url'; -import { nanoid } from 'nanoid'; export class DebDatasource extends Datasource { static readonly id = 'deb'; From 02f98af43ac9420ad647c48ceb5ee4c15d232227 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 19 Aug 2024 22:30:50 +0200 Subject: [PATCH 36/62] use type import instead --- lib/modules/datasource/deb/index.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 40f925c254fdd8..03a1cbd958dba7 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -1,7 +1,8 @@ import { createHash } from 'crypto'; import { createReadStream } from 'fs'; import { copyFile, stat } from 'fs-extra'; -import { DirectoryResult, dir } from 'tmp-promise'; +import type { DirectoryResult } from 'tmp-promise'; +import { dir } from 'tmp-promise'; import upath from 'upath'; import { getPkgReleases } from '..'; import { Fixtures } from '../../../../test/fixtures'; From 7f1447840aec346fee8c82089add18acebe486e2 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:36:04 +0000 Subject: [PATCH 37/62] it should also parse same package with other versions --- .../datasource/deb/__fixtures__/Packages | 19 ++++++++- lib/modules/datasource/deb/index.spec.ts | 41 ++++++++++++------- lib/modules/datasource/deb/index.ts | 27 ++++++++---- lib/modules/datasource/deb/release.ts | 8 ++-- 4 files changed, 66 insertions(+), 29 deletions(-) diff --git a/lib/modules/datasource/deb/__fixtures__/Packages b/lib/modules/datasource/deb/__fixtures__/Packages index 67716b40d9635c..c1874fbb5c528f 100644 --- a/lib/modules/datasource/deb/__fixtures__/Packages +++ b/lib/modules/datasource/deb/__fixtures__/Packages @@ -37,6 +37,23 @@ Size: 5469888 MD5sum: bfecda545171260a755ab5b4acd97aa6 SHA256: 8e16c340d46e53752d5916c7f8aca665aa81a78614bcae0ad7b9decf555c114e +Package: album-data +Version: 4.05-7.3 +Installed-Size: 7936 +Maintainer: Salvo 'LtWorf' Tomaselli +Architecture: all +Depends: album +Description: themes, plugins and translations for album +Homepage: http://marginalhacks.com/Hacks/album +Description-md5: 34adea76df6b2c02712e3838461edbb2 +Tag: role::app-data +Section: non-free/web +Priority: optional +Filename: pool/non-free/a/album-data/album-data_4.05-7.2_all.deb +Size: 5469888 +MD5sum: bfecda545171260a755ab5b4acd97aa6 +SHA256: 8e16c340d46e53752d5916c7f8aca665aa81a78614bcae0ad7b9decf555c114e + Package: alien-arena-data Version: 7.71.3+ds-1 Installed-Size: 1918877 @@ -53,4 +70,4 @@ Priority: optional Filename: pool/non-free/a/alien-arena-data/alien-arena-data_7.71.3+ds-1_all.deb Size: 766655844 MD5sum: 447e0d3a42973dbf3d87df28359501d2 -SHA256: a3fdaf2b0b9e969149642300f0781f7cfdd3ed33f49be38afe58e89531a21b70 \ No newline at end of file +SHA256: a3fdaf2b0b9e969149642300f0781f7cfdd3ed33f49be38afe58e89531a21b70 diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 03a1cbd958dba7..c90bc30b4de5df 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -297,21 +297,32 @@ describe('modules/datasource/deb/index', () => { ); expect(parsedPackages).toEqual({ - album: { - Homepage: 'http://marginalhacks.com/Hacks/album', - Package: 'album', - Version: '4.15-1', - }, - 'album-data': { - Homepage: 'http://marginalhacks.com/Hacks/album', - Package: 'album-data', - Version: '4.05-7.2', - }, - 'alien-arena-data': { - Homepage: 'https://martianbackup.com', - Package: 'alien-arena-data', - Version: '7.71.3+ds-1', - }, + album: [ + { + Homepage: 'http://marginalhacks.com/Hacks/album', + Package: 'album', + Version: '4.15-1', + }, + ], + 'album-data': [ + { + Homepage: 'http://marginalhacks.com/Hacks/album', + Package: 'album-data', + Version: '4.05-7.2', + }, + { + Homepage: 'http://marginalhacks.com/Hacks/album', + Package: 'album-data', + Version: '4.05-7.3', + }, + ], + 'alien-arena-data': [ + { + Homepage: 'https://martianbackup.com', + Package: 'alien-arena-data', + Version: '7.71.3+ds-1', + }, + ], }); }); }); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 504238c5672c88..71bcc18ba5544b 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -305,7 +305,7 @@ export class DebDatasource extends Datasource { async parseExtractedPackageIndex( extractedFile: string, lastTimestamp: Date, - ): Promise> { + ): Promise> { // read line by line to avoid high memory consumption as the extracted Packages // files can be multiple MBs in size const rl = readline.createInterface({ @@ -314,7 +314,8 @@ export class DebDatasource extends Datasource { }); let currentPackage: PackageDescription = {}; - const allPackages: Record = {}; + // A Package Index can contain multiple Versions of the package on private Artifactory (e.g. Jfrog) + const allPackages: Record = {}; for await (const line of rl) { if (line === '') { @@ -324,7 +325,10 @@ export class DebDatasource extends Datasource { (key) => key in currentPackage, ) ) { - allPackages[currentPackage.Package!] = currentPackage; + if (!allPackages[currentPackage.Package!]) { + allPackages[currentPackage.Package!] = []; + } + allPackages[currentPackage.Package!].push(currentPackage); currentPackage = {}; } } else { @@ -338,8 +342,13 @@ export class DebDatasource extends Datasource { } // Check the last package after file reading is complete - if (Object.keys(currentPackage).length > 0) { - allPackages[currentPackage.Package!] = currentPackage; + if ( + DebDatasource.requiredPackageKeys.every((key) => key in currentPackage) + ) { + if (!allPackages[currentPackage.Package!]) { + allPackages[currentPackage.Package!] = []; + } + allPackages[currentPackage.Package!].push(currentPackage); } return allPackages; @@ -351,7 +360,7 @@ export class DebDatasource extends Datasource { }) async getPackageIndex( componentUrl: string, - ): Promise> { + ): Promise> { const { extractedFile, lastTimestamp } = await this.downloadAndExtractPackage(componentUrl); return await this.parseExtractedPackageIndex(extractedFile, lastTimestamp); @@ -378,10 +387,10 @@ export class DebDatasource extends Datasource { for (const componentUrl of componentUrls) { try { const packageIndex = await this.getPackageIndex(componentUrl); - const parsedPackage = packageIndex[packageName]; + const parsedPackages = packageIndex[packageName]; - if (parsedPackage) { - const newRelease = formatReleaseResult(parsedPackage); + if (parsedPackages) { + const newRelease = formatReleaseResult(parsedPackages); if (aggregatedRelease === null) { aggregatedRelease = newRelease; } else { diff --git a/lib/modules/datasource/deb/release.ts b/lib/modules/datasource/deb/release.ts index 4f4ccf72377b3d..7ed3e114423da5 100644 --- a/lib/modules/datasource/deb/release.ts +++ b/lib/modules/datasource/deb/release.ts @@ -18,14 +18,14 @@ export function releaseMetaInformationMatches( /** * Formats the package description into a ReleaseResult. * - * @param packageDesc - The package description object. + * @param packagesDesc - list of package description objects. * @returns A formatted ReleaseResult. */ export function formatReleaseResult( - packageDesc: PackageDescription, + packagesDesc: PackageDescription[], ): ReleaseResult { return { - releases: [{ version: packageDesc.Version! }], - homepage: packageDesc.Homepage, + releases: packagesDesc.map((p) => ({ version: p.Version! })), + homepage: packagesDesc[0].Homepage, }; } From d1202eb8ec922d3a8dc253a164fe2a28f815b72b Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Tue, 20 Aug 2024 22:03:12 +0200 Subject: [PATCH 38/62] Update lib/modules/datasource/deb/release.ts Co-authored-by: Sebastian Poxhofer --- lib/modules/datasource/deb/release.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/release.ts b/lib/modules/datasource/deb/release.ts index 7ed3e114423da5..29dda15c585e53 100644 --- a/lib/modules/datasource/deb/release.ts +++ b/lib/modules/datasource/deb/release.ts @@ -26,6 +26,6 @@ export function formatReleaseResult( ): ReleaseResult { return { releases: packagesDesc.map((p) => ({ version: p.Version! })), - homepage: packagesDesc[0].Homepage, + homepage: packagesDesc[0]?.Homepage, }; } From b778f25ba284f9b698658bf7e3adfaa94b90be4c Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 06:02:21 +0000 Subject: [PATCH 39/62] set registryStrategy to merge --- lib/modules/datasource/deb/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 71bcc18ba5544b..aa1131de9aa49a 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -40,6 +40,13 @@ export class DebDatasource extends Datasource { */ override readonly customRegistrySupport = true; + /** + * Users can specify multiple upstream repositories and the datasource will aggregate the release + * @example + * When specifying multiple dependencies both internal and external dependencies from internal/external artifactory + */ + override readonly registryStrategy = 'merge'; + /** * The original apt source list file format is * deb uri distribution [component1] [component2] [...] From b91178471b5ed84afa16f7db252f2323c2b52822 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:25:56 +0000 Subject: [PATCH 40/62] fix minor review findings --- lib/modules/datasource/deb/checksum.ts | 12 +++--------- lib/modules/datasource/deb/index.ts | 15 ++++++--------- lib/util/cache/package/types.ts | 2 +- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/lib/modules/datasource/deb/checksum.ts b/lib/modules/datasource/deb/checksum.ts index ffcdb7777e610f..720803709836db 100644 --- a/lib/modules/datasource/deb/checksum.ts +++ b/lib/modules/datasource/deb/checksum.ts @@ -1,5 +1,5 @@ -import crypto from 'crypto'; import { createCacheReadStream } from '../../../util/fs'; +import { hashStream } from '../../../util/hash'; import { escapeRegExp, regEx } from '../../../util/regex'; /** @@ -35,12 +35,6 @@ export function parseChecksumsFromInRelease( * @returns resolves to the SHA256 checksum */ export function computeFileChecksum(filePath: string): Promise { - return new Promise((resolve, reject) => { - const hash = crypto.createHash('sha256'); - const stream = createCacheReadStream(filePath); - - stream.on('data', (data) => hash.update(data)); - stream.on('end', () => resolve(hash.digest('hex'))); - stream.on('error', (error) => reject(error)); - }); + const stream = createCacheReadStream(filePath); + return hashStream(stream, 'sha256'); } diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index aa1131de9aa49a..4c2707f31fa755 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -1,4 +1,3 @@ -import { createHash } from 'crypto'; import readline from 'readline'; import { createUnzip } from 'zlib'; import { nanoid } from 'nanoid'; @@ -6,6 +5,7 @@ import upath from 'upath'; import { logger } from '../../../logger'; import { cache } from '../../../util/cache/package/decorator'; import * as fs from '../../../util/fs'; +import { toSha256 } from '../../../util/hash'; import type { HttpOptions } from '../../../util/http/types'; import { joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; @@ -120,9 +120,7 @@ export class DebDatasource extends Datasource { async downloadAndExtractPackage( componentUrl: string, ): Promise<{ extractedFile: string; lastTimestamp: Date }> { - const packageUrlHash = createHash('sha256') - .update(componentUrl) - .digest('hex'); + const packageUrlHash = toSha256(componentUrl); const fullCacheDir = await fs.ensureCacheDir(DebDatasource.cacheSubDir); const extractedFile = upath.join(fullCacheDir, `${packageUrlHash}.txt`); let lastTimestamp = await this.getFileCreationTime(extractedFile); @@ -225,8 +223,6 @@ export class DebDatasource extends Datasource { 'Downloading Debian package file', ); - const actualChecksum = await computeFileChecksum(compressedFile); - let inReleaseContent = ''; try { @@ -234,12 +230,13 @@ export class DebDatasource extends Datasource { } catch (error) { // This is expected to fail for Artifactory if GPG verification is not enabled logger.debug( - { url: baseReleaseUrl, error }, + { url: baseReleaseUrl, err: error }, 'Could not fetch InRelease file', ); } if (inReleaseContent) { + const actualChecksum = await computeFileChecksum(compressedFile); const expectedChecksum = parseChecksumsFromInRelease( inReleaseContent, // path to the Package.gz file @@ -304,7 +301,7 @@ export class DebDatasource extends Datasource { * @returns a list of packages with minimal Metadata. */ @cache({ - namespace: `datasource-${DebDatasource.id}-package`, + namespace: `datasource-${DebDatasource.id}`, key: (extractedFile: string, lastTimestamp: Date) => `${extractedFile}:${lastTimestamp.getTime()}`, ttlMinutes: 24 * 60, @@ -362,7 +359,7 @@ export class DebDatasource extends Datasource { } @cache({ - namespace: `datasource-${DebDatasource.id}-package`, + namespace: `datasource-${DebDatasource.id}`, key: (componentUrl: string) => componentUrl, }) async getPackageIndex( diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index 77217430e45eab..eb3868d5d298bc 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -42,7 +42,7 @@ export type PackageCacheNamespace = | 'datasource-cpan' | 'datasource-crate-metadata' | 'datasource-crate' - | 'datasource-deb-package' + | 'datasource-deb' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' From 7664cc7785cde671b60000a9a029602c845c476e Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 13:40:21 +0000 Subject: [PATCH 41/62] use util function --- lib/modules/datasource/deb/index.spec.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index c90bc30b4de5df..510930c69df0fb 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -1,4 +1,3 @@ -import { createHash } from 'crypto'; import { createReadStream } from 'fs'; import { copyFile, stat } from 'fs-extra'; import type { DirectoryResult } from 'tmp-promise'; @@ -9,6 +8,7 @@ import { Fixtures } from '../../../../test/fixtures'; import * as httpMock from '../../../../test/http-mock'; import { fs } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; +import { hashStream, toSha256 } from '../../../util/hash'; import type { GetPkgReleasesConfig } from '../types'; import { getBaseReleaseUrl } from './url'; import { DebDatasource } from '.'; @@ -37,9 +37,7 @@ describe('modules/datasource/deb/index', () => { extractionFolder = await fs.ensureCacheDir(DebDatasource.cacheSubDir); extractedPackageFile = upath.join( extractionFolder, - `${createHash('sha256') - .update(getComponentUrl(debBaseUrl, 'stable', 'non-free', 'amd64')) - .digest('hex')}.txt`, + `${toSha256(getComponentUrl(debBaseUrl, 'stable', 'non-free', 'amd64'))}.txt`, ); cfg = { @@ -570,12 +568,6 @@ function getRegistryUrl( * @returns resolves to the SHA256 checksum */ function computeFileChecksum(filePath: string): Promise { - return new Promise((resolve, reject) => { - const hash = createHash('sha256'); - const stream = createReadStream(filePath); - - stream.on('data', (data) => hash.update(data)); - stream.on('end', () => resolve(hash.digest('hex'))); - stream.on('error', (error) => reject(error)); - }); + const stream = createReadStream(filePath); + return hashStream(stream, 'sha256'); } From d1b37ec98768ad2778f98c920cd0be1b67a8879c Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:07:12 +0200 Subject: [PATCH 42/62] set cache decorator --- lib/modules/datasource/deb/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 4c2707f31fa755..afebd283d2a52d 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -376,6 +376,11 @@ export class DebDatasource extends Datasource { * @param config - Configuration for fetching releases. * @returns The release result if the package is found, otherwise null. */ + @cache({ + namespace: `datasource-${DebDatasource.id}`, + key: (registryUrl: string, packageName: string) => + `${toSha256(registryUrl)}_${packageName}`, + }) async getReleases({ registryUrl, packageName, From d5f18c195d084a4a7b8225d208c1706f962459f3 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:46:30 +0200 Subject: [PATCH 43/62] fix minor review findings --- lib/modules/datasource/deb/common.ts | 22 ++++++ lib/modules/datasource/deb/file.spec.ts | 28 ++++++++ lib/modules/datasource/deb/file.ts | 37 ++++++++++ lib/modules/datasource/deb/index.spec.ts | 19 ++--- lib/modules/datasource/deb/index.ts | 92 +++++------------------- lib/util/cache/package/types.ts | 1 + 6 files changed, 110 insertions(+), 89 deletions(-) create mode 100644 lib/modules/datasource/deb/common.ts create mode 100644 lib/modules/datasource/deb/file.spec.ts create mode 100644 lib/modules/datasource/deb/file.ts diff --git a/lib/modules/datasource/deb/common.ts b/lib/modules/datasource/deb/common.ts new file mode 100644 index 00000000000000..7c33cc06f9ff6c --- /dev/null +++ b/lib/modules/datasource/deb/common.ts @@ -0,0 +1,22 @@ +import type { PackageDescription } from './types'; + +/** + * This is just an internal list of compressions that are supported and tried to be downloaded from the remote + */ +export const supportedPackageCompressions = ['gz']; + +/** + * This specifies the directory where the extracted and downloaded packages files are stored relative to cacheDir. + * The folder will be created automatically if it doesn't exist. + */ +export const cacheSubDir: string = 'deb'; + +export const requiredPackageKeys: Array = [ + 'Package', + 'Version', +]; + +export const packageKeys: Array = [ + ...requiredPackageKeys, + 'Homepage', +]; diff --git a/lib/modules/datasource/deb/file.spec.ts b/lib/modules/datasource/deb/file.spec.ts new file mode 100644 index 00000000000000..ed1bcaec39da20 --- /dev/null +++ b/lib/modules/datasource/deb/file.spec.ts @@ -0,0 +1,28 @@ +import { dir } from 'tmp-promise'; +import upath from 'upath'; +import { Fixtures } from '../../../../test/fixtures'; +import { fs } from '../../../../test/util'; +import { GlobalConfig } from '../../../config/global'; +import { extract } from './file'; + +const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); + +describe('modules/datasource/deb/file', () => { + let extractedPackageFile: string; + + beforeEach(async () => { + const cacheDir = await dir({ unsafeCleanup: true }); + GlobalConfig.set({ cacheDir: cacheDir.path }); + + const extractionFolder = await fs.ensureCacheDir('file'); + extractedPackageFile = upath.join(extractionFolder, `package.txt`); + }); + + describe('extract', () => { + it('should throw error for unsupported compression', async () => { + await expect( + extract(fixturePackagesArchivePath, 'xz', extractedPackageFile), + ).rejects.toThrow('Unsupported compression standard'); + }); + }); +}); diff --git a/lib/modules/datasource/deb/file.ts b/lib/modules/datasource/deb/file.ts new file mode 100644 index 00000000000000..675c4a42a5f154 --- /dev/null +++ b/lib/modules/datasource/deb/file.ts @@ -0,0 +1,37 @@ +import { createUnzip } from 'zlib'; +import * as fs from '../../../util/fs'; + +/** + * Extracts the specified compressed file to the output file. + * + * @param compressedFile - The path to the compressed file. + * @param compression - The compression method used (currently only 'gz' is supported). + * @param outputFile - The path where the extracted content will be stored. + * @throws Will throw an error if the compression method is unknown. + */ +export async function extract( + compressedFile: string, + compression: string, + outputFile: string, +): Promise { + if (compression === 'gz') { + const source = fs.createCacheReadStream(compressedFile); + const destination = fs.createCacheWriteStream(outputFile); + await fs.pipeline(source, createUnzip(), destination); + } else { + throw new Error(`Unsupported compression standard '${compression}'`); + } +} + +/** + * Checks if the file exists and retrieves its creation time. + * + * @param filePath - The path to the file. + * @returns The creation time if the file exists, otherwise undefined. + */ +export async function getFileCreationTime( + filePath: string, +): Promise { + const stats = await fs.statCacheFile(filePath); + return stats?.ctime; +} diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 510930c69df0fb..a48a3f5d890a9f 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -10,6 +10,8 @@ import { fs } from '../../../../test/util'; import { GlobalConfig } from '../../../config/global'; import { hashStream, toSha256 } from '../../../util/hash'; import type { GetPkgReleasesConfig } from '../types'; +import { cacheSubDir } from './common'; +import * as fileUtils from './file'; import { getBaseReleaseUrl } from './url'; import { DebDatasource } from '.'; @@ -34,7 +36,7 @@ describe('modules/datasource/deb/index', () => { cacheDir = await dir({ unsafeCleanup: true }); GlobalConfig.set({ cacheDir: cacheDir.path }); - extractionFolder = await fs.ensureCacheDir(DebDatasource.cacheSubDir); + extractionFolder = await fs.ensureCacheDir(cacheSubDir); extractedPackageFile = upath.join( extractionFolder, `${toSha256(getComponentUrl(debBaseUrl, 'stable', 'non-free', 'amd64'))}.txt`, @@ -325,18 +327,6 @@ describe('modules/datasource/deb/index', () => { }); }); - describe('extract', () => { - it('should throw error for unsupported compression', async () => { - await expect( - DebDatasource.extract( - fixturePackagesArchivePath, - 'xz', - extractedPackageFile, - ), - ).rejects.toThrow('Unsupported compression standard'); - }); - }); - describe('downloadAndExtractPackage', () => { it('should ignore error when fetching the InRelease content fails', async () => { const packageArgs: [release: string, component: string, arch: string] = [ @@ -378,6 +368,8 @@ describe('modules/datasource/deb/index', () => { }); it('should throw error for unsupported compression', async () => { + jest.spyOn(fileUtils, 'extract').mockRejectedValueOnce(new Error()); + httpMock .scope(debBaseUrl) .get(getPackageUrl('', 'bullseye', 'main', 'amd64')) @@ -389,7 +381,6 @@ describe('modules/datasource/deb/index', () => { 'amd64', ); - DebDatasource.extract = jest.fn().mockRejectedValueOnce(new Error()); await expect( debDatasource.downloadAndExtractPackage( getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index afebd283d2a52d..a41b3f4987bfb2 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -1,5 +1,4 @@ import readline from 'readline'; -import { createUnzip } from 'zlib'; import { nanoid } from 'nanoid'; import upath from 'upath'; import { logger } from '../../../logger'; @@ -11,6 +10,13 @@ import { joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; +import { + cacheSubDir, + packageKeys, + requiredPackageKeys, + supportedPackageCompressions, +} from './common'; +import { extract, getFileCreationTime } from './file'; import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; import { constructComponentUrls, getBaseReleaseUrl } from './url'; @@ -22,17 +28,6 @@ export class DebDatasource extends Datasource { super(DebDatasource.id); } - /** - * This is just an internal list of compressions that are supported and tried to be downloaded from the remote - */ - static readonly compressions = ['gz']; - - /** - * This specifies the directory where the extracted and downloaded packages files are stored relative to cacheDir. - * The folder will be created automatically if it doesn't exist. - */ - static readonly cacheSubDir: string = 'deb'; - /** * Users are able to specify custom Debian repositories as long as they follow * the Debian package repository format as specified here @@ -67,49 +62,6 @@ export class DebDatasource extends Datasource { override readonly defaultVersioning = 'deb'; - static requiredPackageKeys: Array = [ - 'Package', - 'Version', - ]; - - static packagesKeys: Array = [ - ...DebDatasource.requiredPackageKeys, - 'Homepage', - ]; - - /** - * Extracts the specified compressed file to the output file. - * - * @param compressedFile - The path to the compressed file. - * @param compression - The compression method used (currently only 'gz' is supported). - * @param outputFile - The path where the extracted content will be stored. - * @throws Will throw an error if the compression method is unknown. - */ - static async extract( - compressedFile: string, - compression: string, - outputFile: string, - ): Promise { - if (compression === 'gz') { - const source = fs.createCacheReadStream(compressedFile); - const destination = fs.createCacheWriteStream(outputFile); - await fs.pipeline(source, createUnzip(), destination); - } else { - throw new Error(`Unsupported compression standard '${compression}'`); - } - } - - /** - * Checks if the file exists and retrieves its creation time. - * - * @param filePath - The path to the file. - * @returns The creation time if the file exists, otherwise undefined. - */ - async getFileCreationTime(filePath: string): Promise { - const stats = await fs.statCacheFile(filePath); - return stats?.ctime; - } - /** * Downloads and extracts a package file from a component URL. * @@ -121,11 +73,11 @@ export class DebDatasource extends Datasource { componentUrl: string, ): Promise<{ extractedFile: string; lastTimestamp: Date }> { const packageUrlHash = toSha256(componentUrl); - const fullCacheDir = await fs.ensureCacheDir(DebDatasource.cacheSubDir); + const fullCacheDir = await fs.ensureCacheDir(cacheSubDir); const extractedFile = upath.join(fullCacheDir, `${packageUrlHash}.txt`); - let lastTimestamp = await this.getFileCreationTime(extractedFile); + let lastTimestamp = await getFileCreationTime(extractedFile); - for (const compression of DebDatasource.compressions) { + for (const compression of supportedPackageCompressions) { const compressedFile = upath.join( fullCacheDir, `${nanoid()}_${packageUrlHash}.${compression}`, @@ -154,12 +106,8 @@ export class DebDatasource extends Datasource { if (wasUpdated || !lastTimestamp) { try { - await DebDatasource.extract( - compressedFile, - compression, - extractedFile, - ); - lastTimestamp = await this.getFileCreationTime(extractedFile); + await extract(compressedFile, compression, extractedFile); + lastTimestamp = await getFileCreationTime(extractedFile); } catch (error) { logger.error( { @@ -324,11 +272,7 @@ export class DebDatasource extends Datasource { for await (const line of rl) { if (line === '') { // All information of the package are available, add to the list of packages - if ( - DebDatasource.requiredPackageKeys.every( - (key) => key in currentPackage, - ) - ) { + if (requiredPackageKeys.every((key) => key in currentPackage)) { if (!allPackages[currentPackage.Package!]) { allPackages[currentPackage.Package!] = []; } @@ -336,7 +280,7 @@ export class DebDatasource extends Datasource { currentPackage = {}; } } else { - for (const key of DebDatasource.packagesKeys) { + for (const key of packageKeys) { if (line.startsWith(`${key}:`)) { currentPackage[key] = line.substring(key.length + 1).trim(); break; @@ -346,9 +290,7 @@ export class DebDatasource extends Datasource { } // Check the last package after file reading is complete - if ( - DebDatasource.requiredPackageKeys.every((key) => key in currentPackage) - ) { + if (requiredPackageKeys.every((key) => key in currentPackage)) { if (!allPackages[currentPackage.Package!]) { allPackages[currentPackage.Package!] = []; } @@ -377,9 +319,9 @@ export class DebDatasource extends Datasource { * @returns The release result if the package is found, otherwise null. */ @cache({ - namespace: `datasource-${DebDatasource.id}`, + namespace: `datasource-${DebDatasource.id}-releases`, key: (registryUrl: string, packageName: string) => - `${toSha256(registryUrl)}_${packageName}`, + `${registryUrl}-${packageName}`, }) async getReleases({ registryUrl, diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index eb3868d5d298bc..153f76cf955dd1 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -43,6 +43,7 @@ export type PackageCacheNamespace = | 'datasource-crate-metadata' | 'datasource-crate' | 'datasource-deb' + | 'datasource-deb-releases' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' From b911c42c2b45cdbd11745b59777ecac01badcf3c Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:55:08 +0200 Subject: [PATCH 44/62] remove support for multiple compression as currently only one exists --- lib/modules/datasource/deb/common.ts | 5 -- lib/modules/datasource/deb/index.spec.ts | 6 +- lib/modules/datasource/deb/index.ts | 70 ++++++++---------------- 3 files changed, 27 insertions(+), 54 deletions(-) diff --git a/lib/modules/datasource/deb/common.ts b/lib/modules/datasource/deb/common.ts index 7c33cc06f9ff6c..304f7809038bb9 100644 --- a/lib/modules/datasource/deb/common.ts +++ b/lib/modules/datasource/deb/common.ts @@ -1,10 +1,5 @@ import type { PackageDescription } from './types'; -/** - * This is just an internal list of compressions that are supported and tried to be downloaded from the remote - */ -export const supportedPackageCompressions = ['gz']; - /** * This specifies the directory where the extracted and downloaded packages files are stored relative to cacheDir. * The folder will be created automatically if it doesn't exist. diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index a48a3f5d890a9f..8acd8d6157b867 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -364,10 +364,10 @@ describe('modules/datasource/deb/index', () => { debDatasource.downloadAndExtractPackage( getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), ), - ).rejects.toThrow(`No compression standard worked for `); + ).rejects.toThrow(`SHA256 checksum validation failed`); }); - it('should throw error for unsupported compression', async () => { + it('should throw error for when extracting fails', async () => { jest.spyOn(fileUtils, 'extract').mockRejectedValueOnce(new Error()); httpMock @@ -385,7 +385,7 @@ describe('modules/datasource/deb/index', () => { debDatasource.downloadAndExtractPackage( getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), ), - ).rejects.toThrow(`No compression standard worked for `); + ).rejects.toThrow(`Missing metadata in extracted package index file!`); }); }); diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index a41b3f4987bfb2..1531c97e5e843c 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -10,12 +10,7 @@ import { joinUrlParts } from '../../../util/url'; import { Datasource } from '../datasource'; import type { GetReleasesConfig, ReleaseResult } from '../types'; import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; -import { - cacheSubDir, - packageKeys, - requiredPackageKeys, - supportedPackageCompressions, -} from './common'; +import { cacheSubDir, packageKeys, requiredPackageKeys } from './common'; import { extract, getFileCreationTime } from './file'; import { formatReleaseResult, releaseMetaInformationMatches } from './release'; import type { PackageDescription } from './types'; @@ -77,60 +72,43 @@ export class DebDatasource extends Datasource { const extractedFile = upath.join(fullCacheDir, `${packageUrlHash}.txt`); let lastTimestamp = await getFileCreationTime(extractedFile); - for (const compression of supportedPackageCompressions) { - const compressedFile = upath.join( - fullCacheDir, - `${nanoid()}_${packageUrlHash}.${compression}`, - ); + const compression = 'gz'; + const compressedFile = upath.join( + fullCacheDir, + `${nanoid()}_${packageUrlHash}.${compression}`, + ); - let wasUpdated = false; + const wasUpdated = await this.downloadPackageFile( + componentUrl, + compression, + compressedFile, + lastTimestamp, + ); + if (wasUpdated || !lastTimestamp) { try { - wasUpdated = await this.downloadPackageFile( - componentUrl, - compression, - compressedFile, - lastTimestamp, - ); + await extract(compressedFile, compression, extractedFile); + lastTimestamp = await getFileCreationTime(extractedFile); } catch (error) { logger.error( { + componentUrl, compression, error: error.message, }, - `Failed to download package file from ${componentUrl}`, + `Failed to extract package file from ${compressedFile}`, ); - - continue; - } - - if (wasUpdated || !lastTimestamp) { - try { - await extract(compressedFile, compression, extractedFile); - lastTimestamp = await getFileCreationTime(extractedFile); - } catch (error) { - logger.error( - { - componentUrl, - compression, - error: error.message, - }, - `Failed to extract package file from ${compressedFile}`, - ); - } finally { - await fs.rmCache(compressedFile); - } - } - - if (!lastTimestamp) { - //extracting went wrong - continue; + } finally { + await fs.rmCache(compressedFile); } + } - return { extractedFile, lastTimestamp }; + if (!lastTimestamp) { + //extracting went wrong + throw new Error('Missing metadata in extracted package index file!'); } - throw new Error(`No compression standard worked for ${componentUrl}`); + return { extractedFile, lastTimestamp }; } /** From 92ff9190427842b33d401adde29befb6fb674cd5 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:04:23 +0200 Subject: [PATCH 45/62] remove redundant cache decorator as already covered in getPackageIndex --- lib/modules/datasource/deb/index.ts | 5 ----- lib/util/cache/package/types.ts | 1 - 2 files changed, 6 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 1531c97e5e843c..83a02c9d0eeddf 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -296,11 +296,6 @@ export class DebDatasource extends Datasource { * @param config - Configuration for fetching releases. * @returns The release result if the package is found, otherwise null. */ - @cache({ - namespace: `datasource-${DebDatasource.id}-releases`, - key: (registryUrl: string, packageName: string) => - `${registryUrl}-${packageName}`, - }) async getReleases({ registryUrl, packageName, diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index 153f76cf955dd1..eb3868d5d298bc 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -43,7 +43,6 @@ export type PackageCacheNamespace = | 'datasource-crate-metadata' | 'datasource-crate' | 'datasource-deb' - | 'datasource-deb-releases' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' From 87e67f3b7adb18d3c75a3be0db8fad2bf7aadc35 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:30:32 +0000 Subject: [PATCH 46/62] Revert "remove redundant cache decorator as already covered in getPackageIndex" --- lib/modules/datasource/deb/index.ts | 5 +++++ lib/util/cache/package/types.ts | 1 + 2 files changed, 6 insertions(+) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 83a02c9d0eeddf..1531c97e5e843c 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -296,6 +296,11 @@ export class DebDatasource extends Datasource { * @param config - Configuration for fetching releases. * @returns The release result if the package is found, otherwise null. */ + @cache({ + namespace: `datasource-${DebDatasource.id}-releases`, + key: (registryUrl: string, packageName: string) => + `${registryUrl}-${packageName}`, + }) async getReleases({ registryUrl, packageName, diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index eb3868d5d298bc..153f76cf955dd1 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -43,6 +43,7 @@ export type PackageCacheNamespace = | 'datasource-crate-metadata' | 'datasource-crate' | 'datasource-deb' + | 'datasource-deb-releases' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' From 8e1463b5d16dbdee1bce3882a3a3e8f1d274756f Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:32:55 +0200 Subject: [PATCH 47/62] Apply suggestions from code review Co-authored-by: Michael Kriese --- lib/modules/datasource/deb/checksum.spec.ts | 10 ++-------- lib/modules/datasource/deb/checksum.ts | 6 +++--- lib/modules/datasource/deb/index.ts | 8 ++++---- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/modules/datasource/deb/checksum.spec.ts b/lib/modules/datasource/deb/checksum.spec.ts index 4c33aba5abd190..6e6a9be738e31a 100644 --- a/lib/modules/datasource/deb/checksum.spec.ts +++ b/lib/modules/datasource/deb/checksum.spec.ts @@ -1,7 +1,7 @@ import { dir } from 'tmp-promise'; import { Fixtures } from '../../../../test/fixtures'; import { GlobalConfig } from '../../../config/global'; -import { createCacheWriteStream } from '../../../util/fs'; +import { outputCacheFile } from '../../../util/fs'; import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; const fixtureInRelease = Fixtures.getBinary(`InRelease`).toString(); @@ -36,13 +36,7 @@ describe('modules/datasource/deb/checksum', () => { describe('computeFileChecksum', () => { it('computes the checksum of a file', async () => { - const stream = createCacheWriteStream('file.txt'); - - const write = new Promise((resolve, reject) => { - stream.write('bar'); - stream.close(resolve); - }); - await write; + await outputCacheFile('file.txt', 'bar'); const expectedHash = 'fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9'; diff --git a/lib/modules/datasource/deb/checksum.ts b/lib/modules/datasource/deb/checksum.ts index 720803709836db..19ead4a3ed5945 100644 --- a/lib/modules/datasource/deb/checksum.ts +++ b/lib/modules/datasource/deb/checksum.ts @@ -1,6 +1,6 @@ import { createCacheReadStream } from '../../../util/fs'; import { hashStream } from '../../../util/hash'; -import { escapeRegExp, regEx } from '../../../util/regex'; +import { escapeRegExp, newlineRegex, regEx } from '../../../util/regex'; /** * Parses the SHA256 checksum for a specified package path from the InRelease content. @@ -13,13 +13,13 @@ export function parseChecksumsFromInRelease( inReleaseContent: string, packagePath: string, ): string | null { - const lines = inReleaseContent.split('\n'); + const lines = inReleaseContent.split(newlineRegex); const regex = regEx( `([a-f0-9]{64})\\s+\\d+\\s+${escapeRegExp(packagePath)}$`, ); for (const line of lines) { - const match = line.match(regex); + const match = regex.exec(line); if (match) { return match[1]; } diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 1531c97e5e843c..3b5d1773a0904e 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -64,7 +64,7 @@ export class DebDatasource extends Datasource { * @returns The path to the extracted file and the last modification timestamp. * @throws Will throw an error if no valid compression method is found. */ - async downloadAndExtractPackage( + private async downloadAndExtractPackage( componentUrl: string, ): Promise<{ extractedFile: string; lastTimestamp: Date }> { const packageUrlHash = toSha256(componentUrl); @@ -120,7 +120,7 @@ export class DebDatasource extends Datasource { * @param lastDownloadTimestamp - The timestamp of the last download. * @returns True if the file was downloaded, otherwise false. */ - async downloadPackageFile( + private async downloadPackageFile( basePackageUrl: string, compression: string, compressedFile: string, @@ -184,7 +184,7 @@ export class DebDatasource extends Datasource { * @returns resolves to the content of the InRelease file. * @throws An error if the InRelease file could not be downloaded. */ - async fetchInReleaseFile(baseReleaseUrl: string): Promise { + private async fetchInReleaseFile(baseReleaseUrl: string): Promise { const inReleaseUrl = joinUrlParts(baseReleaseUrl, 'InRelease'); const response = await this.http.get(inReleaseUrl); return response.body; @@ -198,7 +198,7 @@ export class DebDatasource extends Datasource { * @returns True if the content has been modified, otherwise false. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since */ - async checkIfModified( + private async checkIfModified( packageUrl: string, lastDownloadTimestamp: Date, ): Promise { From 97018b1cf8ad50a7568fe740c7b131e82666f68b Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:43:17 +0000 Subject: [PATCH 48/62] move vars to describe block --- lib/modules/datasource/deb/file.spec.ts | 8 ++++- lib/modules/datasource/deb/index.spec.ts | 45 +++++++++++++++--------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/lib/modules/datasource/deb/file.spec.ts b/lib/modules/datasource/deb/file.spec.ts index ed1bcaec39da20..3a7f111588e3b6 100644 --- a/lib/modules/datasource/deb/file.spec.ts +++ b/lib/modules/datasource/deb/file.spec.ts @@ -8,16 +8,22 @@ import { extract } from './file'; const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); describe('modules/datasource/deb/file', () => { + let cacheDir; let extractedPackageFile: string; beforeEach(async () => { - const cacheDir = await dir({ unsafeCleanup: true }); + cacheDir = await dir({ unsafeCleanup: true }); GlobalConfig.set({ cacheDir: cacheDir.path }); const extractionFolder = await fs.ensureCacheDir('file'); extractedPackageFile = upath.join(extractionFolder, `package.txt`); }); + afterEach(async () => { + await cacheDir?.cleanup(); + cacheDir = null; + }); + describe('extract', () => { it('should throw error for unsupported compression', async () => { await expect( diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 8acd8d6157b867..6bca4766b54c60 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -17,13 +17,13 @@ import { DebDatasource } from '.'; const debBaseUrl = 'http://deb.debian.org'; -const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); -const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); -const fixturePackagesPath = Fixtures.getPath(`Packages`); -let fixturePackagesArchiveHash: string; -let fixturePackagesArchiveHash2: string; - describe('modules/datasource/deb/index', () => { + const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); + const fixturePackagesArchivePath2 = Fixtures.getPath(`Packages2.gz`); + const fixturePackagesPath = Fixtures.getPath(`Packages`); + let fixturePackagesArchiveHash: string; + let fixturePackagesArchiveHash2: string; + let debDatasource: DebDatasource; let cacheDir: DirectoryResult; let cfg: GetPkgReleasesConfig; @@ -113,7 +113,14 @@ describe('modules/datasource/deb/index', () => { describe('without local version', () => { beforeEach(() => { - mockHttpCalls('stable', 'non-free', 'amd64', false); + mockHttpCalls( + 'stable', + 'non-free', + 'amd64', + false, + fixturePackagesArchivePath, + fixturePackagesArchiveHash, + ); }); it('returns a valid version for the package `album`', async () => { @@ -274,7 +281,14 @@ describe('modules/datasource/deb/index', () => { for (let i = 0; i < packages.length; i++) { // first call doesn't include a http head call, since the file doesn't exists locally yet // the package index is downloaded every time since the http head call returns 200 - mockHttpCalls('stable', 'non-free', 'amd64', !!i); + mockHttpCalls( + 'stable', + 'non-free', + 'amd64', + !!i, + fixturePackagesArchivePath, + fixturePackagesArchiveHash, + ); } const results = await Promise.all( @@ -431,24 +445,23 @@ describe('modules/datasource/deb/index', () => { * @param component - The component name (e.g., 'main'). * @param arch - The architecture (e.g., 'amd64'). * @param checkIfModified - whether it should mock the http head call of the Package Index file + * @param packageArchivePath - path to package index + * @param packagesArchiveHash - sha256 hash of package */ function mockHttpCalls( release: string, component: string, arch: string, checkIfModified: boolean, + packageArchivePath: string, + packagesArchiveHash: string, ) { httpMock .scope(debBaseUrl) .get(getPackageUrl('', release, component, arch)) - .replyWithFile(200, fixturePackagesArchivePath); - - mockFetchInReleaseContent( - fixturePackagesArchiveHash, - release, - component, - arch, - ); + .replyWithFile(200, packageArchivePath); + + mockFetchInReleaseContent(packagesArchiveHash, release, component, arch); if (checkIfModified) { httpMock From bb9f35250afbde1fae45ed06803b5338f3d999d0 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:49:54 +0000 Subject: [PATCH 49/62] cleanup cache dir --- lib/modules/datasource/deb/checksum.spec.ts | 12 +++++++++--- lib/modules/datasource/deb/file.spec.ts | 3 ++- lib/modules/datasource/deb/index.spec.ts | 17 +++++++++++------ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/modules/datasource/deb/checksum.spec.ts b/lib/modules/datasource/deb/checksum.spec.ts index 6e6a9be738e31a..6f934b642e6634 100644 --- a/lib/modules/datasource/deb/checksum.spec.ts +++ b/lib/modules/datasource/deb/checksum.spec.ts @@ -1,3 +1,4 @@ +import type { DirectoryResult } from 'tmp-promise'; import { dir } from 'tmp-promise'; import { Fixtures } from '../../../../test/fixtures'; import { GlobalConfig } from '../../../config/global'; @@ -7,11 +8,16 @@ import { computeFileChecksum, parseChecksumsFromInRelease } from './checksum'; const fixtureInRelease = Fixtures.getBinary(`InRelease`).toString(); describe('modules/datasource/deb/checksum', () => { + let cacheDir: DirectoryResult | null; + beforeEach(async () => { - const cacheDirResult = await dir({ unsafeCleanup: true }); - const cacheDir = cacheDirResult.path; + const cacheDir = await dir({ unsafeCleanup: true }); + GlobalConfig.set({ cacheDir: cacheDir.path }); + }); - GlobalConfig.set({ cacheDir }); + afterEach(async () => { + await cacheDir?.cleanup(); + cacheDir = null; }); describe('parseChecksumsFromInRelease', () => { diff --git a/lib/modules/datasource/deb/file.spec.ts b/lib/modules/datasource/deb/file.spec.ts index 3a7f111588e3b6..62f60cf7231fa4 100644 --- a/lib/modules/datasource/deb/file.spec.ts +++ b/lib/modules/datasource/deb/file.spec.ts @@ -1,3 +1,4 @@ +import type { DirectoryResult } from 'tmp-promise'; import { dir } from 'tmp-promise'; import upath from 'upath'; import { Fixtures } from '../../../../test/fixtures'; @@ -8,7 +9,7 @@ import { extract } from './file'; const fixturePackagesArchivePath = Fixtures.getPath(`Packages.gz`); describe('modules/datasource/deb/file', () => { - let cacheDir; + let cacheDir: DirectoryResult | null; let extractedPackageFile: string; beforeEach(async () => { diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index 6bca4766b54c60..f720fef697bdbb 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -25,7 +25,7 @@ describe('modules/datasource/deb/index', () => { let fixturePackagesArchiveHash2: string; let debDatasource: DebDatasource; - let cacheDir: DirectoryResult; + let cacheDir: DirectoryResult | null; let cfg: GetPkgReleasesConfig; let extractionFolder: string; let extractedPackageFile: string; @@ -58,6 +58,11 @@ describe('modules/datasource/deb/index', () => { ); }); + afterEach(async () => { + await cacheDir?.cleanup(); + cacheDir = null; + }); + describe('getReleases', () => { it('returns a valid version for the package `album` and does not require redownload', async () => { await copyFile(fixturePackagesPath, extractedPackageFile); @@ -356,7 +361,7 @@ describe('modules/datasource/deb/index', () => { mockFetchInReleaseContent('wrong-hash', ...packageArgs, true); await expect( - debDatasource.downloadAndExtractPackage( + debDatasource['downloadAndExtractPackage']( getComponentUrl(debBaseUrl, ...packageArgs), ), ).resolves.toEqual( @@ -375,7 +380,7 @@ describe('modules/datasource/deb/index', () => { mockFetchInReleaseContent('wrong-hash', 'bullseye', 'main', 'amd64'); await expect( - debDatasource.downloadAndExtractPackage( + debDatasource['downloadAndExtractPackage']( getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), ), ).rejects.toThrow(`SHA256 checksum validation failed`); @@ -396,7 +401,7 @@ describe('modules/datasource/deb/index', () => { ); await expect( - debDatasource.downloadAndExtractPackage( + debDatasource['downloadAndExtractPackage']( getComponentUrl(debBaseUrl, 'bullseye', 'main', 'amd64'), ), ).rejects.toThrow(`Missing metadata in extracted package index file!`); @@ -411,7 +416,7 @@ describe('modules/datasource/deb/index', () => { .reply(200); await expect( - debDatasource.checkIfModified( + debDatasource['checkIfModified']( getPackageUrl(debBaseUrl, 'stable', 'non-free', 'amd64'), new Date(), ), @@ -425,7 +430,7 @@ describe('modules/datasource/deb/index', () => { .replyWithError('Unexpected Error'); await expect( - debDatasource.checkIfModified( + debDatasource['checkIfModified']( getPackageUrl(debBaseUrl, 'stable', 'non-free', 'amd64'), new Date(), ), From 56988960f11fbdb1090a466e1c190b98fecf37f6 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Sat, 24 Aug 2024 08:55:33 +0000 Subject: [PATCH 50/62] validate http head call using nock options --- lib/modules/datasource/deb/index.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/modules/datasource/deb/index.spec.ts b/lib/modules/datasource/deb/index.spec.ts index f720fef697bdbb..e43320da4e04db 100644 --- a/lib/modules/datasource/deb/index.spec.ts +++ b/lib/modules/datasource/deb/index.spec.ts @@ -70,7 +70,12 @@ describe('modules/datasource/deb/index', () => { const ts = stats.ctime; httpMock - .scope(debBaseUrl) + .scope(debBaseUrl, { + // ensure the rest call sets the correct request headers + reqheaders: { + 'if-modified-since': ts.toUTCString(), + }, + }) .head(getPackageUrl('', 'stable', 'non-free', 'amd64')) .reply(304); @@ -85,9 +90,6 @@ describe('modules/datasource/deb/index', () => { }, ], }); - - const modifiedTs = httpMock.getTrace()[0].headers['if-modified-since']; - expect(modifiedTs).toBe(ts.toUTCString()); }); describe('parsing of registry url', () => { From ee03bdf37e9560ec576f18ea1ce47efcee1fcae3 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Thu, 22 Aug 2024 17:05:30 -0300 Subject: [PATCH 51/62] test(regex-manager): Separate arrange/act/assert with spaces (#30972) --- .../manager/custom/regex/index.spec.ts | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/modules/manager/custom/regex/index.spec.ts b/lib/modules/manager/custom/regex/index.spec.ts index 671883e1c6d820..560e4c02d58522 100644 --- a/lib/modules/manager/custom/regex/index.spec.ts +++ b/lib/modules/manager/custom/regex/index.spec.ts @@ -29,11 +29,13 @@ describe('modules/manager/custom/regex/index', () => { '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', depTypeTemplate: 'final', }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(8); expect(res?.deps.find((dep) => dep.depName === 'yarn')?.versioning).toBe( @@ -53,7 +55,9 @@ describe('modules/manager/custom/regex/index', () => { versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', }; + const res = await extractPackageFile('', 'Dockerfile', config); + expect(res).toBeNull(); }); @@ -64,11 +68,13 @@ describe('modules/manager/custom/regex/index', () => { ], versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver', }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toBeNull(); }); @@ -78,11 +84,13 @@ describe('modules/manager/custom/regex/index', () => { 'ENV NGINX_MODULE_HEADERS_MORE_VERSION=(?.*) # (?.*?)/(?.*?)(\\&versioning=(?.*?))?(\\&extractVersion=(?.*?))?\\s', ], }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); expect( @@ -99,6 +107,7 @@ describe('modules/manager/custom/regex/index', () => { ], datasourceTemplate: 'helm', }; + const res = await extractPackageFile( ` apiVersion: helm.fluxcd.io/v1 @@ -116,6 +125,7 @@ describe('modules/manager/custom/regex/index', () => { 'Dockerfile', config, ); + expect(res).toMatchSnapshot({ deps: [ { @@ -135,11 +145,13 @@ describe('modules/manager/custom/regex/index', () => { ], registryUrlTemplate: 'http://registry.{{depName}}.com/', }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); expect( @@ -154,11 +166,13 @@ describe('modules/manager/custom/regex/index', () => { ], registryUrlTemplate: 'this-is-not-a-valid-url-{{depName}}', }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toMatchSnapshot({ deps: [ { @@ -186,11 +200,13 @@ describe('modules/manager/custom/regex/index', () => { versioningTemplate: '{{#if versioning}}{{versioning}}{{else}}semver{{/if}}', }; + const res = await extractPackageFile( dockerfileContent, 'Dockerfile', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(2); expect( @@ -210,11 +226,13 @@ describe('modules/manager/custom/regex/index', () => { autoReplaceStringTemplate: 'image: {{{depName}}}:{{{newValue}}}', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( 'image: my.old.registry/aRepository/andImage:1.18-alpine', 'values.yaml', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -228,11 +246,13 @@ describe('modules/manager/custom/regex/index', () => { 'image:\n{{{indentation}}} name: {{{depName}}}:{{{newValue}}}', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( ' image: eclipse-temurin:17.0.0-alpine', 'bitbucket-pipelines.yml', config, ); + expect(res).toMatchObject({ deps: [ { @@ -255,11 +275,13 @@ describe('modules/manager/custom/regex/index', () => { 'image:\n{{{indentation}}} name: {{{depName}}}:{{{newValue}}}', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( 'name: image: eclipse-temurin:17.0.0-alpine', 'bitbucket-pipelines.yml', config, ); + expect(res).toMatchObject({ deps: [ { @@ -282,11 +304,13 @@ describe('modules/manager/custom/regex/index', () => { matchStringsStrategy: 'combination', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -303,11 +327,13 @@ describe('modules/manager/custom/regex/index', () => { datasourceTemplate: 'docker', depNameTemplate: '{{{ registry }}}/{{{ repository }}}', }; + const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); + expect(res?.deps).toHaveLength(1); expect(res?.deps[0].depName).toBe('docker.io/prom/prometheus'); expect(res).toMatchSnapshot(); @@ -322,11 +348,13 @@ describe('modules/manager/custom/regex/index', () => { matchStringsStrategy: 'combination', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( ansibleYamlContent, 'ansible.yml', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -341,11 +369,13 @@ describe('modules/manager/custom/regex/index', () => { ], datasourceTemplate: 'helm', }; + const res = await extractPackageFile( exampleGitlabCiYml, '.gitlab-ci.yml', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -360,11 +390,13 @@ describe('modules/manager/custom/regex/index', () => { 'image:\n name: {{{depName}}}{{#if newValue}}:{{{newValue}}}{{/if}}{{#if newDigest}}@{{{newDigest}}}{{/if}}', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( 'image: eclipse-temurin:17.0.0-alpine', 'bitbucket-pipelines.yml', config, ); + expect(res).toMatchObject({ deps: [ { @@ -387,11 +419,13 @@ describe('modules/manager/custom/regex/index', () => { 'image:\n name: {{{depName}}}{{#if newValue}}:{{{newValue}}}{{/if}}{{#if newDigest}}@{{{newDigest}}}{{/if}}', datasourceTemplate: 'docker', }; + const res = await extractPackageFile( 'image: eclipse-temurin@sha256:1234567890abcdef', 'bitbucket-pipelines.yml', config, ); + expect(res).toMatchObject({ deps: [ { @@ -414,11 +448,13 @@ describe('modules/manager/custom/regex/index', () => { datasourceTemplate: 'helm', depNameTemplate: 'helm_repo/{{{ depName }}}', }; + const res = await extractPackageFile( exampleGitlabCiYml, '.gitlab-ci.yml', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -433,7 +469,9 @@ describe('modules/manager/custom/regex/index', () => { datasourceTemplate: 'helm', depNameTemplate: 'helm_repo/{{{ depName }}}', }; + const res = await extractPackageFile('', '.gitlab-ci.yml', config); + expect(res).toBeNull(); }); @@ -445,11 +483,13 @@ describe('modules/manager/custom/regex/index', () => { ], matchStringsStrategy: 'recursive', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -462,11 +502,13 @@ describe('modules/manager/custom/regex/index', () => { ], matchStringsStrategy: 'recursive', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(2); }); @@ -480,11 +522,13 @@ describe('modules/manager/custom/regex/index', () => { ], matchStringsStrategy: 'recursive', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(1); }); @@ -494,11 +538,13 @@ describe('modules/manager/custom/regex/index', () => { matchStrings: ['"group.{1}":\\s*\\{[^}]*}'], matchStringsStrategy: 'recursive', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toBeNull(); }); @@ -507,11 +553,13 @@ describe('modules/manager/custom/regex/index', () => { matchStrings: ['"trunk.{1}":\\s*\\{[^}]*}'], matchStringsStrategy: 'recursive', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toBeNull(); }); @@ -525,11 +573,13 @@ describe('modules/manager/custom/regex/index', () => { matchStringsStrategy: 'recursive', depNameTemplate: '{{{ first }}}/{{{ second }}}/{{{ depName }}}', }; + const res = await extractPackageFile( exampleJsonContent, 'example.json', config, ); + expect(res).toMatchSnapshot(); expect(res?.deps).toHaveLength(4); }); @@ -544,6 +594,7 @@ describe('modules/manager/custom/regex/index', () => { depNameTemplate: 'org.jacoco:jacoco', datasourceTemplate: 'maven', }; + const res = await extractPackageFile( ` jacoco { @@ -553,6 +604,7 @@ describe('modules/manager/custom/regex/index', () => { 'build.gradle.kts', config, ); + expect(res).toMatchObject({ deps: [ { From c3ea33a9158799490cc5320796f63c1732bfc714 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 20:17:21 +0000 Subject: [PATCH 52/62] fix(deps): update ghcr.io/renovatebot/base-image docker tag to v7.16.1 (#30974) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tools/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index 4a610b8991bbd7..bf5adc719160da 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -5,17 +5,17 @@ ARG BASE_IMAGE_TYPE=slim # -------------------------------------- # slim image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:7.16.0@sha256:afa4d0461877e0cc3869fcf2189174a9ba7deb796174497311c8cd59f747ae2f AS slim-base +FROM ghcr.io/renovatebot/base-image:7.16.1@sha256:2618099cc16710c2d06e210a41a3c6f192eae04544b1409e5e57c97a0f449a0e AS slim-base # -------------------------------------- # full image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:7.16.0-full@sha256:93e7c16dac3e9b23bbb4cf4d3cf5b466397a1c88b306d906ce9ec1004eaa4375 AS full-base +FROM ghcr.io/renovatebot/base-image:7.16.1-full@sha256:9e9078c5b5731fafd86624b637e14412c2cd870969f966175491ede2ea4f860b AS full-base # -------------------------------------- # build image # -------------------------------------- -FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:7.16.0@sha256:afa4d0461877e0cc3869fcf2189174a9ba7deb796174497311c8cd59f747ae2f AS build +FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:7.16.1@sha256:2618099cc16710c2d06e210a41a3c6f192eae04544b1409e5e57c97a0f449a0e AS build # We want a specific node version here # renovate: datasource=node-version From 2923d8a1170b3c1af7dda55056d1544a53f5eef0 Mon Sep 17 00:00:00 2001 From: Aleksandr Mezin Date: Fri, 23 Aug 2024 06:39:56 +0300 Subject: [PATCH 53/62] fix(manager/git-submodules): get recorded submodule commit (#30976) --- lib/modules/manager/git-submodules/extract.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/git-submodules/extract.ts b/lib/modules/manager/git-submodules/extract.ts index fce3e496a77339..a762aa5d463aa1 100644 --- a/lib/modules/manager/git-submodules/extract.ts +++ b/lib/modules/manager/git-submodules/extract.ts @@ -123,7 +123,9 @@ export default async function extractPackageFile( const deps = []; for (const { name, path } of depNames) { try { - const [currentDigest] = (await git.subModule(['status', path])) + const [currentDigest] = ( + await git.subModule(['status', '--cached', path]) + ) .trim() .replace(regEx(/^[-+]/), '') .split(regEx(/\s/)); From 62d80b1044283b362cf3c40e520c4e19319e2a29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:02:07 +0000 Subject: [PATCH 54/62] chore(deps): update dependency mkdocs-material to v9.5.33 (#30981) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- pdm.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pdm.lock b/pdm.lock index 00040cf7efc9d1..bc1ed2d47d954b 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default"] strategy = ["inherit_metadata"] lock_version = "4.5.0" -content_hash = "sha256:a78e6d4ea1bc2b40d3aca326609cbc341e5362c1a1673e2e57ca604a24fdecb1" +content_hash = "sha256:4b3e787c8be67cc0c573185de7f932adc72183f9c1f3c7c16c2536c916440022" [[metadata.targets]] requires_python = ">=3.11" @@ -268,7 +268,7 @@ files = [ [[package]] name = "mkdocs-material" -version = "9.5.32" +version = "9.5.33" requires_python = ">=3.8" summary = "Documentation that simply works" groups = ["default"] @@ -286,8 +286,8 @@ dependencies = [ "requests~=2.26", ] files = [ - {file = "mkdocs_material-9.5.32-py3-none-any.whl", hash = "sha256:f3704f46b63d31b3cd35c0055a72280bed825786eccaf19c655b44e0cd2c6b3f"}, - {file = "mkdocs_material-9.5.32.tar.gz", hash = "sha256:38ed66e6d6768dde4edde022554553e48b2db0d26d1320b19e2e2b9da0be1120"}, + {file = "mkdocs_material-9.5.33-py3-none-any.whl", hash = "sha256:dbc79cf0fdc6e2c366aa987de8b0c9d4e2bb9f156e7466786ba2fd0f9bf7ffca"}, + {file = "mkdocs_material-9.5.33.tar.gz", hash = "sha256:d23a8b5e3243c9b2f29cdfe83051104a8024b767312dc8fde05ebe91ad55d89d"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 0eb40dcb2b5fee..8561f62e522303 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] dependencies = [ - "mkdocs-material==9.5.32", + "mkdocs-material==9.5.33", "mkdocs-awesome-pages-plugin==2.9.3", ] requires-python = ">=3.11" From e914d9a33ba0fbb3d3232191edec5b3b215ed1fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:54:04 +0000 Subject: [PATCH 55/62] feat(deps): update ghcr.io/renovatebot/base-image docker tag to v7.17.0 (#30983) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- tools/docker/Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/docker/Dockerfile b/tools/docker/Dockerfile index bf5adc719160da..61e63f4ba2ccd8 100644 --- a/tools/docker/Dockerfile +++ b/tools/docker/Dockerfile @@ -5,17 +5,17 @@ ARG BASE_IMAGE_TYPE=slim # -------------------------------------- # slim image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:7.16.1@sha256:2618099cc16710c2d06e210a41a3c6f192eae04544b1409e5e57c97a0f449a0e AS slim-base +FROM ghcr.io/renovatebot/base-image:7.17.0@sha256:2f95402b323ee9baf582dd662c2919d957f37b8c5ed471102f63f2e9abfe6c22 AS slim-base # -------------------------------------- # full image # -------------------------------------- -FROM ghcr.io/renovatebot/base-image:7.16.1-full@sha256:9e9078c5b5731fafd86624b637e14412c2cd870969f966175491ede2ea4f860b AS full-base +FROM ghcr.io/renovatebot/base-image:7.17.0-full@sha256:83ea557d0f9996a442491aebe1a393c8793d0df1fa6f695d22e69dd0060dcc3d AS full-base # -------------------------------------- # build image # -------------------------------------- -FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:7.16.1@sha256:2618099cc16710c2d06e210a41a3c6f192eae04544b1409e5e57c97a0f449a0e AS build +FROM --platform=$BUILDPLATFORM ghcr.io/renovatebot/base-image:7.17.0@sha256:2f95402b323ee9baf582dd662c2919d957f37b8c5ed471102f63f2e9abfe6c22 AS build # We want a specific node version here # renovate: datasource=node-version From 5f648f323e0135453a2ebe7e905e92e748d5a1fb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Aug 2024 22:37:44 +0000 Subject: [PATCH 56/62] chore(deps): update github/codeql-action action to v3.26.5 (#30986) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/scorecard.yml | 2 +- .github/workflows/trivy.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7d5ac50f605d6c..dfba64091bb837 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -41,7 +41,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: languages: javascript @@ -51,7 +51,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -65,4 +65,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 561aea0688cd07..f36296b14dc873 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -51,6 +51,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: 'Upload to code-scanning' - uses: github/codeql-action/upload-sarif@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: results.sarif diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index ba0480c640e159..78684af795d21c 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -31,7 +31,7 @@ jobs: format: 'sarif' output: 'trivy-results.sarif' - - uses: github/codeql-action/upload-sarif@f0f3afee809481da311ca3a6ff1ff51d81dbeb24 # v3.26.4 + - uses: github/codeql-action/upload-sarif@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3.26.5 with: sarif_file: trivy-results.sarif category: 'docker-image-${{ matrix.tag }}' From 80f51c5ba55e0a074ae4f833bb771395ec789cb6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 01:53:20 +0000 Subject: [PATCH 57/62] chore(deps): update dependency type-fest to v4.25.0 (#30987) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 553c8df164dcf6..0aafde8cc35ca1 100644 --- a/package.json +++ b/package.json @@ -340,7 +340,7 @@ "tmp-promise": "3.0.3", "ts-jest": "29.2.4", "ts-node": "10.9.2", - "type-fest": "4.24.0", + "type-fest": "4.25.0", "typescript": "5.5.4", "unified": "9.2.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26d58241641efb..2a6d3f23796fb0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -605,8 +605,8 @@ importers: specifier: 10.9.2 version: 10.9.2(@swc/core@1.7.11)(@types/node@20.16.1)(typescript@5.5.4) type-fest: - specifier: 4.24.0 - version: 4.24.0 + specifier: 4.25.0 + version: 4.25.0 typescript: specifier: 5.5.4 version: 5.5.4 @@ -5883,8 +5883,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - type-fest@4.24.0: - resolution: {integrity: sha512-spAaHzc6qre0TlZQQ2aA/nGMe+2Z/wyGk5Z+Ru2VUfdNwT6kWO6TjevOlpebsATEG1EIQ2sOiDszud3lO5mt/Q==} + type-fest@4.25.0: + resolution: {integrity: sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==} engines: {node: '>=16'} typed-array-buffer@1.0.2: @@ -11972,7 +11972,7 @@ snapshots: dependencies: '@babel/code-frame': 7.24.7 index-to-position: 0.1.2 - type-fest: 4.24.0 + type-fest: 4.25.0 parse-link-header@2.0.0: dependencies: @@ -12175,7 +12175,7 @@ snapshots: dependencies: find-up-simple: 1.0.0 read-pkg: 9.0.1 - type-fest: 4.24.0 + type-fest: 4.25.0 read-pkg-up@7.0.1: dependencies: @@ -12195,7 +12195,7 @@ snapshots: '@types/normalize-package-data': 2.4.4 normalize-package-data: 6.0.2 parse-json: 8.1.0 - type-fest: 4.24.0 + type-fest: 4.25.0 unicorn-magic: 0.1.0 read-yaml-file@2.1.0: @@ -12924,7 +12924,7 @@ snapshots: type-fest@2.19.0: {} - type-fest@4.24.0: {} + type-fest@4.25.0: {} typed-array-buffer@1.0.2: dependencies: From 2bd4ede6df449e0998bd0df1f49da9a7af4b73e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jasmin=20M=C3=BCller?= <9011011+jazzlyn@users.noreply.github.com> Date: Sat, 24 Aug 2024 08:40:45 +0200 Subject: [PATCH 58/62] docs(docker): add ghworkflows example for GAR with Workload Identity (#30692) --- docs/usage/docker.md | 51 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/usage/docker.md b/docs/usage/docker.md index 84fb5f1e95d6be..0405e5d53a3e99 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -279,12 +279,51 @@ To make use of this authentication mechanism, specify the username as `AWS`: #### Google Container Registry / Google Artifact Registry -##### Using Application Default Credentials / Workload Identity (Self-Hosted only) +##### Using Workload Identity + +To let Renovate authenticate with [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity), you must: + +- Configure Workload Identity +- Give the Service Account the `artifactregistry.repositories.downloadArtifacts` permission + +###### With Application Default Credentials (self-hosted only) + +To let Renovate authenticate with [ADC](https://cloud.google.com/docs/authentication/provide-credentials-adc), you must: + +- Configure ADC as normal +- _Not_ provide a username, password or token + +Renovate will get the credentials with the [`google-auth-library`](https://www.npmjs.com/package/google-auth-library). + +###### With short-lived access token / GitHub Actions (self-hosted only) + +```yaml title="Example for Workload Identity plus Renovate host rules" +- name: authenticate to google cloud + id: auth + uses: google-github-actions/auth@v2.1.3 + with: + token_format: 'access_token' + workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ env.SERVICE_ACCOUNT }} + +- name: renovate + uses: renovatebot/github-action@v40.2.4 + env: + RENOVATE_HOST_RULES: | + [ + { + matchHost: "us-central1-docker.pkg.dev", + hostType: "docker", + username: "oauth2accesstoken", + password: "${{ steps.auth.outputs.access_token }}" + } + ] + with: + token: ${{ secrets.RENOVATE_TOKEN }} + configurationFile: .github/renovate.json5 +``` -Just configure [ADC](https://cloud.google.com/docs/authentication/provide-credentials-adc) / -[Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) as normal and _don't_ -provide a username, password or token. Renovate will automatically retrieve the credentials using the -google-auth-library. +You can find a full GitHub Workflow example on the [renovatebot/github-action](https://github.com/renovatebot/github-action) repository. ##### Using long-lived service account credentials @@ -386,7 +425,7 @@ If you have dependencies on Google Container Registry (and Artifact Registry) yo } ``` -##### Using short-lived access tokens +##### Using short-lived access token / Gitlab CI / Google Cloud Assume you are running GitLab CI in the Google Cloud, and you are storing your Docker images in the Google Container Registry (GCR). From a1a01a106b74e169f37c63583b8650a333d98350 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 06:52:11 +0000 Subject: [PATCH 59/62] chore(deps): update dependency google-github-actions/auth to v2.1.5 (#30994) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/usage/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/docker.md b/docs/usage/docker.md index 0405e5d53a3e99..9ac142ade579cd 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -300,7 +300,7 @@ Renovate will get the credentials with the [`google-auth-library`](https://www.n ```yaml title="Example for Workload Identity plus Renovate host rules" - name: authenticate to google cloud id: auth - uses: google-github-actions/auth@v2.1.3 + uses: google-github-actions/auth@v2.1.5 with: token_format: 'access_token' workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} From 2cb37e93df9dfbfb6bc5ff60a9aef25505f39f04 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:12:03 +0200 Subject: [PATCH 60/62] chore(deps): update dependency renovatebot/github-action to v40.2.6 (#30995) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/usage/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/docker.md b/docs/usage/docker.md index 9ac142ade579cd..0f134cfc0cbac2 100644 --- a/docs/usage/docker.md +++ b/docs/usage/docker.md @@ -307,7 +307,7 @@ Renovate will get the credentials with the [`google-auth-library`](https://www.n service_account: ${{ env.SERVICE_ACCOUNT }} - name: renovate - uses: renovatebot/github-action@v40.2.4 + uses: renovatebot/github-action@v40.2.6 env: RENOVATE_HOST_RULES: | [ From 8101e1036ebc8e00c2df54777cf2c328c87897ac Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:14:06 +0200 Subject: [PATCH 61/62] Apply suggestions from code review Co-authored-by: Michael Kriese --- lib/modules/datasource/deb/index.ts | 6 +++--- lib/util/cache/package/types.ts | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index 3b5d1773a0904e..b17168261c55a7 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -297,9 +297,9 @@ export class DebDatasource extends Datasource { * @returns The release result if the package is found, otherwise null. */ @cache({ - namespace: `datasource-${DebDatasource.id}-releases`, - key: (registryUrl: string, packageName: string) => - `${registryUrl}-${packageName}`, + namespace: `datasource-${DebDatasource.id}`, + key: ({registryUrl, packageName}: GetReleasesConfig) => + `${registryUrl}:${packageName}`, }) async getReleases({ registryUrl, diff --git a/lib/util/cache/package/types.ts b/lib/util/cache/package/types.ts index 153f76cf955dd1..eb3868d5d298bc 100644 --- a/lib/util/cache/package/types.ts +++ b/lib/util/cache/package/types.ts @@ -43,7 +43,6 @@ export type PackageCacheNamespace = | 'datasource-crate-metadata' | 'datasource-crate' | 'datasource-deb' - | 'datasource-deb-releases' | 'datasource-deno-details' | 'datasource-deno-versions' | 'datasource-deno' From 7d32385ff043eaf7635292222588ef4db15c1db3 Mon Sep 17 00:00:00 2001 From: oxdev03 <140103378+oxdev03@users.noreply.github.com> Date: Mon, 26 Aug 2024 19:21:59 +0000 Subject: [PATCH 62/62] format code --- lib/modules/datasource/deb/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/datasource/deb/index.ts b/lib/modules/datasource/deb/index.ts index b17168261c55a7..ad29d107df4977 100644 --- a/lib/modules/datasource/deb/index.ts +++ b/lib/modules/datasource/deb/index.ts @@ -298,7 +298,7 @@ export class DebDatasource extends Datasource { */ @cache({ namespace: `datasource-${DebDatasource.id}`, - key: ({registryUrl, packageName}: GetReleasesConfig) => + key: ({ registryUrl, packageName }: GetReleasesConfig) => `${registryUrl}:${packageName}`, }) async getReleases({