From b9f3ea2f42945abf2b930531b2012592a84cf6ba Mon Sep 17 00:00:00 2001 From: okxint Date: Sat, 25 Jul 2026 12:06:59 +0530 Subject: [PATCH] fix(core): escape dots in object keys when flattening to prevent path collision --- .../core/src/v3/utils/flattenAttributes.ts | 37 +++++++++++++++++-- packages/core/test/flattenAttributes.test.ts | 15 ++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/core/src/v3/utils/flattenAttributes.ts b/packages/core/src/v3/utils/flattenAttributes.ts index 7852e855340..739109a3f3b 100644 --- a/packages/core/src/v3/utils/flattenAttributes.ts +++ b/packages/core/src/v3/utils/flattenAttributes.ts @@ -119,8 +119,9 @@ class AttributeFlattener { if (obj instanceof Map) { for (const [key, value] of obj) { if (!this.canAddMoreAttributes()) break; - // Use the key directly if it's a string, otherwise convert it - const keyStr = typeof key === "string" ? key : String(key); + // Use the key directly if it's a string, otherwise convert it. + // Escape dots so keys containing "." are not mistaken for path separators. + const keyStr = typeof key === "string" ? key.replace(/\./g, "\\.") : String(key); this.#processValue(value, `${prefix || "map"}.${keyStr}`, depth); } return; @@ -204,7 +205,10 @@ class AttributeFlattener { break; } - const newPrefix = `${prefix ? `${prefix}.` : ""}${Array.isArray(obj) ? `[${key}]` : key}`; + // Escape dots in string keys so they are not mistaken for path separators + // when unflattening. Array indices use bracket notation and need no escaping. + const escapedKey = Array.isArray(obj) ? `[${key}]` : key.replace(/\./g, "\\."); + const newPrefix = `${prefix ? `${prefix}.` : ""}${escapedKey}`; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { @@ -248,6 +252,31 @@ class AttributeFlattener { } } } +/** + * Splits a flattened attribute key on dots that are NOT escaped with a backslash, + * then unescapes any `\.` sequences in each part back to `.`. + * + * This allows object keys that contain literal dots (e.g. "Key 0.002mm") to + * round-trip through flatten/unflatten without being treated as path separators. + */ +function splitOnUnescapedDots(key: string): string[] { + const parts: string[] = []; + let current = ""; + for (let i = 0; i < key.length; i++) { + if (key[i] === "\\" && i + 1 < key.length && key[i + 1] === ".") { + current += "."; + i++; // skip the escaped dot + } else if (key[i] === ".") { + parts.push(current); + current = ""; + } else { + current += key[i]; + } + } + parts.push(current); + return parts; +} + export function unflattenAttributes( obj: Attributes, filteredKeys?: string[], @@ -277,7 +306,7 @@ export function unflattenAttributes( continue; } - const parts = key.split(".").reduce( + const parts = splitOnUnescapedDots(key).reduce( (acc, part) => { if (part.startsWith("[") && part.endsWith("]")) { // Handle array indices more precisely diff --git a/packages/core/test/flattenAttributes.test.ts b/packages/core/test/flattenAttributes.test.ts index 345a5f42fc6..e242c1cbc94 100644 --- a/packages/core/test/flattenAttributes.test.ts +++ b/packages/core/test/flattenAttributes.test.ts @@ -1,6 +1,21 @@ import { flattenAttributes, unflattenAttributes } from "../src/v3/utils/flattenAttributes.js"; describe("flattenAttributes", () => { + it("round-trips object keys that contain dots", () => { + // Keys with dots were previously split on the dot, turning "Key 0.002mm" into + // a nested path { "Key 0": { "002mm": ... } } on unflatten. + const input = { "Key 0.002mm": 31.4 }; + const flattened = flattenAttributes(input); + expect(flattened).toEqual({ "Key 0\\.002mm": 31.4 }); + expect(unflattenAttributes(flattened)).toEqual(input); + + // Nested object where an intermediate key contains a dot + const nested = { outer: { "a.b": { inner: 1 } } }; + const flatNested = flattenAttributes(nested); + expect(flatNested).toEqual({ "outer.a\\.b.inner": 1 }); + expect(unflattenAttributes(flatNested)).toEqual(nested); + }); + it("handles number keys correctly", () => { expect(flattenAttributes({ bar: { "25": "foo" } })).toEqual({ "bar.25": "foo" }); expect(unflattenAttributes({ "bar.25": "foo" })).toEqual({ bar: { "25": "foo" } });