From 98c0abc87a71fc53384489824e4fe02ab2cd00de Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Wed, 17 Dec 2025 09:33:25 +0100 Subject: [PATCH 1/3] feat(cache): prevent cache invalidation during pako upgrade Signed-off-by: ElaineDeMattosSilvaB --- providers/caching/redis.js | 18 ++- test/providers/caching/redis.js | 191 ++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/providers/caching/redis.js b/providers/caching/redis.js index 5d32935d..cab69aeb 100644 --- a/providers/caching/redis.js +++ b/providers/caching/redis.js @@ -90,7 +90,23 @@ class RedisCache { let result try { - const buffer = Buffer.from(typeof cacheItem === 'string' ? cacheItem : cacheItem.toString(), 'base64') + // Ensure cacheItem is treated as a string + const dataString = typeof cacheItem === 'string' ? cacheItem : String(cacheItem) + + // Detect format: base64 (new) vs binary string (old) + // Base64 only contains A-Z, a-z, 0-9, +, /, and optional = padding + const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(dataString) + + let buffer + if (isBase64) { + // NEW format: base64 encoded (written by Pako 2.1.0) + buffer = Buffer.from(dataString, 'base64') + } else { + // OLD format: binary string (written by Pako 1.0.8 with { to: 'string' }) + // Use 'binary' encoding to preserve byte values + buffer = Buffer.from(dataString, 'binary') + } + result = pako.inflate(buffer, { to: 'string' }) } catch (err) { // Disregard decompression errors gracefully as cache may be stored in an older format, missing or expired. diff --git a/test/providers/caching/redis.js b/test/providers/caching/redis.js index c4c420f6..c3000015 100644 --- a/test/providers/caching/redis.js +++ b/test/providers/caching/redis.js @@ -7,6 +7,8 @@ const assert = require('assert') const redisCache = require('../../../providers/caching/redis') const { RedisCache } = require('../../../providers/caching/redis') const { GenericContainer } = require('testcontainers') +const pako1 = require('pako-1') +const pako2 = require('pako') const logger = { info: () => {}, @@ -105,6 +107,195 @@ describe('Redis Cache', () => { }) }) + describe('backward compatibility (pako 1.x -> pako 2.x)', () => { + const objectPrefix = '*!~%' + let mockClient, cache + const store = {} + + beforeEach(function () { + mockClient = { + get: async key => Promise.resolve(store[key]), + set: async (key, value) => { + store[key] = value + }, + del: async key => { + store[key] = null + }, + connect: async () => Promise.resolve(mockClient), + on: () => {}, + quit: sinon.stub().resolves() + } + sandbox.stub(RedisCache, 'buildRedisClient').returns(mockClient) + cache = redisCache({ logger }) + }) + + afterEach(function () { + sandbox.restore() + // Clear store + Object.keys(store).forEach(key => delete store[key]) + }) + + describe('Format Detection', () => { + it('should detect old binary string format correctly', () => { + const oldData = 'xÚ+JMÉ,V°ª5´³0²ä\u0002\u0000\u0011î\u0003ê' + const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(oldData) + assert.strictEqual(isBase64, false) + }) + + it('should detect new base64 format correctly', () => { + const newData = 'eJwrSszLLEnVUUpKLAIAESID6g==' + const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(newData) + assert.strictEqual(isBase64, true) + }) + }) + + describe('Reading OLD format data (pako 1.x binary string)', () => { + it('should read definition data (def_* key pattern)', async () => { + await cache.initialize() + + const originalValue = { + coordinates: { + type: 'nuget', + provider: 'nuget', + namespace: null, + name: 'xunit.core', + revision: '2.1.0' + }, + described: { + releaseDate: '2015-11-08T00:00:00.000Z', + urls: { + registry: 'https://www.nuget.org/packages/xunit.core/2.1.0', + download: 'https://www.nuget.org/api/v2/package/xunit.core/2.1.0' + } + }, + licensed: { + declared: 'Apache-2.0 OR MIT' + }, + files: 87, + _meta: { + schemaVersion: '1.6.1', + updated: '2015-11-08T12:00:00.000Z' + } + } + + const serialized = objectPrefix + JSON.stringify(originalValue) + + // compress with pako v1.x using binary string format (old format) + const oldFormatData = pako1.deflate(serialized, { to: 'string' }) + + // verify it's NOT base64 (binary string format) + assert.strictEqual(/^[A-Za-z0-9+/]+=*$/.test(oldFormatData), false) + + store['def_nuget/nuget/-/xunit.core/2.1.0'] = oldFormatData + + // read with NEW code (uses pako v2.x with format detection) + const result = await cache.get('def_nuget/nuget/-/xunit.core/2.1.0') + + assert.deepStrictEqual(result, originalValue) + assert.strictEqual(result.coordinates.name, 'xunit.core') + assert.strictEqual(result.licensed.declared, 'Apache-2.0 OR MIT') + }) + + it('should read harvest data (hrv_* key pattern)', async () => { + await cache.initialize() + + const originalValue = [ + { + type: 'component', + url: 'cd:/pypi/pypi/-/backports.ssl_match_hostname/3.7.0.2' + } + ] + + const serialized = objectPrefix + JSON.stringify(originalValue) + + // compress with pako v1.x using binary string format (old format) + const oldFormatData = pako1.deflate(serialized, { to: 'string' }) + + store['hrv_pypi/pypi/-/backports.ssl_match_hostname/3.7.0.2'] = oldFormatData + + // read with NEW code (uses pako v2.x with format detection) + const result = await cache.get('hrv_pypi/pypi/-/backports.ssl_match_hostname/3.7.0.2') + + assert.deepStrictEqual(result, originalValue) + assert.strictEqual(result.length, 1) + assert.strictEqual(result[0].type, 'component') + assert.strictEqual(result[0].url, 'cd:/pypi/pypi/-/backports.ssl_match_hostname/3.7.0.2') + }) + }) + + describe('Reading NEW format data (pako 2.x base64)', () => { + it('should read definition data (def_* key pattern)', async () => { + await cache.initialize() + + const originalValue = { + coordinates: { + type: 'npm', + provider: 'npmjs', + namespace: null, + name: 'lodash', + revision: '4.17.21' + }, + described: { + releaseDate: '2021-02-20T00:00:00.000Z', + urls: { + registry: 'https://www.npmjs.com/package/lodash', + download: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' + } + }, + licensed: { + declared: 'MIT' + }, + files: 1054, + _meta: { + schemaVersion: '1.6.1', + updated: '2021-02-20T12:00:00.000Z' + } + } + + const serialized = objectPrefix + JSON.stringify(originalValue) + + // compress with pako v2.x using base64 format (new format) + const deflated = pako2.deflate(serialized) + const newFormatData = Buffer.from(deflated).toString('base64') + + // verify it IS base64 + assert.strictEqual(/^[A-Za-z0-9+/]+=*$/.test(newFormatData), true) + + store['def_npm/npmjs/-/lodash/4.17.21'] = newFormatData + + // read with NEW code (uses pako v2.x with format detection) + const result = await cache.get('def_npm/npmjs/-/lodash/4.17.21') + + assert.deepStrictEqual(result, originalValue) + assert.strictEqual(result.coordinates.name, 'lodash') + assert.strictEqual(result.licensed.declared, 'MIT') + }) + + it('should read harvest data (hrv_* key pattern)', async () => { + await cache.initialize() + + const originalValue = [ + { type: 'component', url: 'cd:/npm/npmjs/-/express/4.18.0' }, + { type: 'component', url: 'cd:/npm/npmjs/-/axios/1.6.0' } + ] + + const serialized = objectPrefix + JSON.stringify(originalValue) + + // compress with pako v2.x using base64 format (new format) + const deflated = pako2.deflate(serialized) + const newFormatData = Buffer.from(deflated).toString('base64') + + store['hrv_npm/npmjs/-/my-package/1.0.0'] = newFormatData + + // read with NEW code (uses pako v2.x with format detection) + const result = await cache.get('hrv_npm/npmjs/-/my-package/1.0.0') + + assert.deepStrictEqual(result, originalValue) + assert.strictEqual(result.length, 2) + assert.strictEqual(result[0].url, 'cd:/npm/npmjs/-/express/4.18.0') + }) + }) + }) xdescribe('Integration Test', () => { let container, redisConfig From fee86d35eaa6bb3352d51e16899f889df7c7826e Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Wed, 17 Dec 2025 09:36:04 +0100 Subject: [PATCH 2/3] chore(config): improve redis port configuration Signed-off-by: ElaineDeMattosSilvaB --- bin/config.js | 3 ++- providers/caching/redisConfig.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/config.js b/bin/config.js index d3cefa22..649c9190 100644 --- a/bin/config.js +++ b/bin/config.js @@ -70,7 +70,8 @@ module.exports = { caching: { service: loadFactory(config.get('CACHING_PROVIDER') || 'memory', 'caching'), caching_redis_service: config.get('CACHING_REDIS_SERVICE'), - caching_redis_api_key: config.get('CACHING_REDIS_API_KEY') + caching_redis_api_key: config.get('CACHING_REDIS_API_KEY'), + caching_redis_port: config.get('CACHING_REDIS_PORT') || 6380 }, endpoints: { service: config.get('SERVICE_ENDPOINT') || 'http://localhost:4000', diff --git a/providers/caching/redisConfig.js b/providers/caching/redisConfig.js index d5b1f8f3..aa8ee107 100644 --- a/providers/caching/redisConfig.js +++ b/providers/caching/redisConfig.js @@ -23,7 +23,7 @@ function serviceFactory(options) { const realOptions = options || { service: config.get('CACHING_REDIS_SERVICE'), apiKey: config.get('CACHING_REDIS_API_KEY'), - port: Number(config.get('CACHING_REDIS_PORT')) || 6380 + port: Number(config.get('CACHING_REDIS_PORT')) } return redis(realOptions) } From 495b1f9ec4b2cbd04221acd91c3760c91334039d Mon Sep 17 00:00:00 2001 From: ElaineDeMattosSilvaB Date: Wed, 17 Dec 2025 09:37:28 +0100 Subject: [PATCH 3/3] chore: add pako 1 for unit testing Signed-off-by: ElaineDeMattosSilvaB --- package-lock.json | 29 ++++++++++++++++++++++++++--- package.json | 4 +++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52150e49..8e91292b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,8 @@ "node-mocks-http": "^1.17.2", "nodemon": "^3.1.10", "nyc": "^17.1.0", + "pako": "^2.1.0", + "pako-1": "npm:pako@^1.0.8", "prettier": "3.6.2", "proxyquire": "^2.0.1", "sinon": "^21.0.0", @@ -144,6 +146,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1129,9 +1132,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.0.tgz", - "integrity": "sha512-ZHzx7Z3rdlWL1mECydvpryWN/ETXJiCxdgQKTAH+djzIPe77HdnSizKBDi1TVDXZjXyOj2IqEG/vPw71ULF06w==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz", + "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -1164,6 +1167,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -1575,6 +1579,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz", "integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -1805,6 +1810,7 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -2183,6 +2189,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2232,6 +2239,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3032,6 +3040,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3258,6 +3267,7 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -4766,6 +4776,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -4809,6 +4820,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 16" }, @@ -5499,6 +5511,7 @@ "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.x" } @@ -8524,6 +8537,15 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/pako-1": { + "name": "pako", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, "license": "(MIT AND Zlib)" }, "node_modules/param-case": { @@ -11402,6 +11424,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index e520cd66..190e3319 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,8 @@ "sinon": "^21.0.0", "supertest": "^ 7.1.3", "testcontainers": "^11.2.1", - "typescript": "5.8.3" + "typescript": "5.8.3", + "pako": "^2.1.0", + "pako-1": "npm:pako@^1.0.8" } }