Repository: sindresorhus/dot-prop (js)

Issue:
escape()
Hi! 👋 
      
Firstly, thanks for your work on this project! 🙂

Today I used [patch-package](https://github.com/ds300/patch-package) to patch `dot-prop@6.0.1` for the project I'm working on.

<!-- 🔺️🔺️🔺️ PLEASE REPLACE THIS BLOCK with a description of your problem, and any other relevant context 🔺️🔺️🔺️ -->

Here is the diff that solved my problem:

```diff
diff --git a/node_modules/dot-prop/index.js b/node_modules/dot-prop/index.js
index aa9c5bb..57a49b5 100644
--- a/node_modules/dot-prop/index.js
+++ b/node_modules/dot-prop/index.js
@@ -134,5 +134,9 @@ module.exports = {
 		}
 
 		return true;
+	},
+
+	escape(prop) {
+		return prop.replace(/\./g, '\\.')
 	}
 };
```

<em>This issue body was [partially generated by patch-package](https://github.com/ds300/patch-package/issues/296).</em>


Pull request description:
Add `escapePath()`
```js
import {getProperty, escapePath} from 'dot-prop';

const object = {
	foo: {
		bar: ['👸🏻 You found me Mario!'],
	},
	'foo.bar[0]' : '🍄 The princess is in another castle!',
};
const escapedPath = escapePath('foo.bar[0]');

console.log(getProperty(object, escapedPath));
//=> '🍄 The princess is in another castle!'
```

Fixes #83

Patch:
diff --git a/index.d.ts b/index.d.ts
index 72981c5..843f8f9 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -108,3 +108,26 @@ console.log(object);
 ```
 */
 export function deleteProperty(object: Record<string, any>, path: string): boolean;
+
+/**
+Escape special characters in a path. Useful for sanitizing user input.
+
+@param path - The dot path to sanitize.
+
+@example
+```
+import {getProperty, escapePath} from 'dot-prop';
+
+const object = {
+	foo: {
+		bar: '👸🏻 You found me Mario!',
+	},
+	'foo.bar' : '🍄 The princess is in another castle!',
+};
+const escapedPath = escapePath('foo.bar');
+
+console.log(getProperty(object, escapedPath));
+//=> '🍄 The princess is in another castle!'
+```
+*/
+export function escapePath(path: string): string;
diff --git a/index.js b/index.js
index 0f4f053..abd5e30 100644
--- a/index.js
+++ b/index.js
@@ -180,11 +180,11 @@ export function getProperty(object, path, value) {
 		return value;
 	}
 
-	for (let i = 0; i < pathArray.length; i++) {
-		const key = pathArray[i];
+	for (let index = 0; index < pathArray.length; index++) {
+		const key = pathArray[index];
 
 		if (isStringIndex(object, key)) {
-			object = i === pathArray.length - 1 ? undefined : null;
+			object = index === pathArray.length - 1 ? undefined : null;
 		} else {
 			object = object[key];
 		}
@@ -195,7 +195,7 @@ export function getProperty(object, path, value) {
 			// if it didn't return `undefined`
 			// it would return `null` if `object` is `null`
 			// but we want `get({foo: null}, 'foo.bar')` to equal `undefined`, or the supplied value, not `null`
-			if (i !== pathArray.length - 1) {
+			if (index !== pathArray.length - 1) {
 				return value;
 			}
 
@@ -214,19 +214,20 @@ export function setProperty(object, path, value) {
 	const root = object;
 	const pathArray = getPathSegments(path);
 
-	for (let i = 0; i < pathArray.length; i++) {
-		const p = pathArray[i];
-		assertNotStringIndex(object, p);
+	for (let index = 0; index < pathArray.length; index++) {
+		const key = pathArray[index];
 
-		if (!isObject(object[p])) {
-			object[p] = Number.isInteger(pathArray[i + 1]) ? [] : {};
+		assertNotStringIndex(object, key);
+
+		if (!isObject(object[key])) {
+			object[key] = typeof pathArray[index + 1] === 'number' ? [] : {};
 		}
 
-		if (i === pathArray.length - 1) {
-			object[p] = value;
+		if (index === pathArray.length - 1) {
+			object[key] = value;
 		}
 
-		object = object[p];
+		object = object[key];
 	}
 
 	return root;
@@ -239,16 +240,17 @@ export function deleteProperty(object, path) {
 
 	const pathArray = getPathSegments(path);
 
-	for (let i = 0; i < pathArray.length; i++) {
-		const p = pathArray[i];
-		assertNotStringIndex(object, p);
+	for (let index = 0; index < pathArray.length; index++) {
+		const key = pathArray[index];
+
+		assertNotStringIndex(object, key);
 
-		if (i === pathArray.length - 1) {
-			delete object[p];
+		if (index === pathArray.length - 1) {
+			delete object[key];
 			return true;
 		}
 
-		object = object[p];
+		object = object[key];
 
 		if (!isObject(object)) {
 			return false;
@@ -266,18 +268,21 @@ export function hasProperty(object, path) {
 		return false;
 	}
 
-	// eslint-disable-next-line unicorn/no-for-loop
-	for (let i = 0; i < pathArray.length; i++) {
-		if (isObject(object)) {
-			if (!(pathArray[i] in object && !isStringIndex(object, pathArray[i]))) {
-				return false;
-			}
-
-			object = object[pathArray[i]];
-		} else {
+	for (const key of pathArray) {
+		if (!isObject(object) || !(key in object) || isStringIndex(object, key)) {
 			return false;
 		}
+
+		object = object[key];
 	}
 
 	return true;
 }
+
+export function escapePath(path) {
+	if (typeof path !== 'string') {
+		throw new TypeError('Expected a string');
+	}
+
+	return path.replace(/[\\.[]/g, '\\$&');
+}
diff --git a/readme.md b/readme.md
index 8eb58e8..828d09c 100644
--- a/readme.md
+++ b/readme.md
@@ -89,6 +89,25 @@ Delete the property at the given path.
 
 Returns a boolean of whether the property existed before being deleted.
 
+### escapePath(path)
+
+Escape special characters in a path. Useful for sanitizing user input.
+
+```js
+import {getProperty, escapePath} from 'dot-prop';
+
+const object = {
+	foo: {
+		bar: '👸🏻 You found me Mario!',
+	},
+	'foo.bar' : '🍄 The princess is in another castle!',
+};
+const escapedPath = escapePath('foo.bar');
+
+console.log(getProperty(object, escapedPath));
+//=> '🍄 The princess is in another castle!'
+```
+
 #### object
 
 Type: `object | array`


Test patch:
diff --git a/test.js b/test.js
index 56e8e7e..0430e58 100644
--- a/test.js
+++ b/test.js
@@ -1,5 +1,5 @@
 import test from 'ava';
-import {getProperty, setProperty, hasProperty, deleteProperty} from './index.js';
+import {getProperty, setProperty, hasProperty, deleteProperty, escapePath} from './index.js';
 
 test('getProperty', t => {
 	const fixture1 = {foo: {bar: 1}};
@@ -383,6 +383,25 @@ test('hasProperty', t => {
 	}, 'foo[0].bar.1'));
 });
 
+test('escapePath', t => {
+	t.is(escapePath('foo.bar[0]'), 'foo\\.bar\\[0]');
+	t.is(escapePath('foo\\.bar[0]'), 'foo\\\\\\.bar\\[0]');
+	t.is(escapePath('foo\\\.bar[0]'), 'foo\\\\\\.bar\\[0]'); // eslint-disable-line no-useless-escape
+	t.is(escapePath('foo\\\\.bar[0]'), 'foo\\\\\\\\\\.bar\\[0]');
+	t.is(escapePath('foo\\\\.bar\\\\[0]'), 'foo\\\\\\\\\\.bar\\\\\\\\\\[0]');
+	t.is(escapePath('foo[0].bar'), 'foo\\[0]\\.bar');
+	t.is(escapePath('foo.bar[0].baz'), 'foo\\.bar\\[0]\\.baz');
+	t.is(escapePath('[0].foo'), '\\[0]\\.foo');
+	t.is(escapePath(''), '');
+
+	t.throws(() => {
+		escapePath(0);
+	}, {
+		instanceOf: TypeError,
+		message: 'Expected a string',
+	});
+});
+
 test('prevent setting/getting `__proto__`', t => {
 	setProperty({}, '__proto__.unicorn', '🦄');
 	t.not({}.unicorn, '🦄'); // eslint-disable-line no-use-extend-native/no-use-extend-native