From dddf0394f678ffc931d8b819a844b4173faf0ce1 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 24 Aug 2026 20:35:33 -0400 Subject: [PATCH 1/4] test: cover safe-integer boundary in large number encoding Numbers with 16 digits are unconditionally encoded as "===" strings, even when they round-trip through a double without precision loss. These tests fail on the current implementation. --- test/util-file.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/util-file.test.js b/test/util-file.test.js index e66a04e..cc2ab21 100644 --- a/test/util-file.test.js +++ b/test/util-file.test.js @@ -402,6 +402,21 @@ describe('openapi-format CLI file tests', () => { expect(result).toEqual({name: 'John', age: 30}); }); + it('should keep Number.MAX_SAFE_INTEGER as a number in YAML', async () => { + const result = await parseString(`value: ${Number.MAX_SAFE_INTEGER}`); + expect(result).toEqual({value: Number.MAX_SAFE_INTEGER}); + }); + + it('should keep Number.MIN_SAFE_INTEGER as a number in YAML', async () => { + const result = await parseString(`value: ${Number.MIN_SAFE_INTEGER}`); + expect(result).toEqual({value: Number.MIN_SAFE_INTEGER}); + }); + + it('should encode YAML integers beyond the safe integer range', async () => { + const result = await parseString('positive: 9007199254740993\nnegative: -9007199254740993'); + expect(result).toEqual({positive: '9007199254740993===', negative: '-9007199254740993==='}); + }); + it('should preserve quoted JSON examples with high-precision numbers and following schemas', async () => { const yamlString = `openapi: 3.1.0 info: @@ -749,6 +764,30 @@ components: const output = encodeLargeNumbers(input); expect(output).toBe(input); }); + + test('should not encode Number.MAX_SAFE_INTEGER', () => { + const input = `key: ${Number.MAX_SAFE_INTEGER}\n`; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: 9007199254740991\n'); + }); + + test('should not encode Number.MIN_SAFE_INTEGER', () => { + const input = `key: ${Number.MIN_SAFE_INTEGER}\n`; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: -9007199254740991\n'); + }); + + test('should not encode a 16 digit integer that fits in a double', () => { + const input = 'key: 1234567890123456\n'; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: 1234567890123456\n'); + }); + + test('should encode an integer beyond Number.MAX_SAFE_INTEGER', () => { + const input = 'key: 9007199254740993\n'; + const output = encodeLargeNumbers(input); + expect(output).toBe('key: "9007199254740993==="\n'); + }); }); describe('addQuotesToRefInString function', () => { From f67a262055f7878a27ce261aac838ea609d8aecf Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 24 Aug 2026 20:36:29 -0400 Subject: [PATCH 2/4] fix: only encode number literals that lose precision as strings Both encode paths treated any literal with more than 15 digits as unsafe. Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER have 16 digits, so they were rewritten to "===" strings despite being exactly representable. Gate the check on an actual round-trip: keep the digit-count fast path for short literals, and for longer ones only encode when Number(source) does not reproduce the original literal. --- utils/file.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/utils/file.js b/utils/file.js index 3df9bd6..4ae7fcc 100644 --- a/utils/file.js +++ b/utils/file.js @@ -497,6 +497,23 @@ async function getRemoteFile(filePath) { return inputContent; } +/** + * Check whether a numeric literal survives a round-trip through a JS number. + * Literals with 15 digits or fewer always fit in a double, anything longer is + * only unsafe when parsing it back changes the value or its notation. + * @param {string} source - The raw numeric literal. + * @returns {boolean} True when the literal cannot be represented exactly. + */ +function isUnsafeNumberLiteral(source) { + const parsed = Number(source).toString(); + if (parsed.includes('e')) return true; + + const digitCount = source.replace(/[^0-9]/g, '').length; + if (digitCount <= 15) return false; + + return parsed !== source; +} + /** * Convert large number value safely before parsing * @param inputContent Input content. @@ -510,7 +527,7 @@ function encodeLargeNumbers(inputContent) { const rgx = new RegExp(endChar, 'g'); const number = rawInput.replace(/: /g, '').replace(rgx, ''); // Handle large numbers safely in javascript - if (Number(number).toString().includes('e') || number.replace('.', '').length > 15) { + if (isUnsafeNumberLiteral(number)) { return `: "${number}==="${endChar}`; } else { return `: ${number}${endChar}`; @@ -535,8 +552,7 @@ function encodeLargeNumberScalars(doc) { } const source = value.source; - const digitCount = source.replace(/[^0-9]/g, '').length; - if (Number(source).toString().includes('e') || digitCount > 15) { + if (isUnsafeNumberLiteral(source)) { value.value = `${source}===`; value.type = 'QUOTE_DOUBLE'; } From 05e73e7b6cd400536c350ff5436dd991a7b6289d Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Mon, 24 Aug 2026 21:14:44 -0400 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- utils/file.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/utils/file.js b/utils/file.js index 4ae7fcc..42f3491 100644 --- a/utils/file.js +++ b/utils/file.js @@ -498,12 +498,12 @@ async function getRemoteFile(filePath) { } /** - * Check whether a numeric literal survives a round-trip through a JS number. - * Literals with 15 digits or fewer always fit in a double, anything longer is - * only unsafe when parsing it back changes the value or its notation. + * Check whether a numeric literal survives a round-trip through `Number(...)` and + * `Number#toString()`. + * Short literals (<= 15 digits) are treated as safe; longer ones are considered unsafe + * when parsing/stringifying changes the literal or produces exponential notation. * @param {string} source - The raw numeric literal. - * @returns {boolean} True when the literal cannot be represented exactly. - */ + * @returns {boolean} True when the literal would not round-trip as the same string. function isUnsafeNumberLiteral(source) { const parsed = Number(source).toString(); if (parsed.includes('e')) return true; From b6b6d79088ab3fa7a11ae74e15afc2ba2de9aedb Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 25 Aug 2026 18:46:04 -0400 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Dimitri Mitropoulos --- utils/file.js | 1 + 1 file changed, 1 insertion(+) diff --git a/utils/file.js b/utils/file.js index 42f3491..fa0fb4c 100644 --- a/utils/file.js +++ b/utils/file.js @@ -504,6 +504,7 @@ async function getRemoteFile(filePath) { * when parsing/stringifying changes the literal or produces exponential notation. * @param {string} source - The raw numeric literal. * @returns {boolean} True when the literal would not round-trip as the same string. + */ function isUnsafeNumberLiteral(source) { const parsed = Number(source).toString(); if (parsed.includes('e')) return true;