JSON Pointer Get Set And Remove (RFC 6901)

Resolve, assign and delete values with RFC 6901 JSON Pointers, including escape handling, array indexes and prototype pollution guards.

JavascriptData Integrity

Code

snippet
const BLOCKED = new Set(["__proto__", "prototype", "constructor"]);

export function parseJsonPointer(pointer) {
  if (typeof pointer !== "string") throw new TypeError("pointer must be a string");
  if (pointer === "") return [];
  if (!pointer.startsWith("/")) throw new SyntaxError("pointer must start with /");
  return pointer
    .slice(1)
    .split("/")
    .map((token) => token.replace(/~1/g, "/").replace(/~0/g, "~"));
}

function assertSegment(token) {
  if (BLOCKED.has(token)) throw new TypeError("unsafe pointer segment: " + token);
}

function arrayIndex(token, allowAppend, length) {
  if (token === "-") {
    if (!allowAppend) throw new RangeError("append is only allowed for set");
    return length;
  }
  if (!/^(0|[1-9]\d*)$/.test(token)) throw new RangeError("invalid array index: " + token);
  const index = Number(token);
  const max = allowAppend ? length : length - 1;
  if (index > max) throw new RangeError("array index out of range: " + token);
  return index;
}

export function getJsonPointer(document, pointer) {
  let current = document;
  for (const token of parseJsonPointer(pointer)) {
    if (current === null || typeof current !== "object") return undefined;
    if (Array.isArray(current)) {
      if (!/^(0|[1-9]\d*)$/.test(token)) return undefined;
      const index = Number(token);
      if (index >= current.length) return undefined;
      current = current[index];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
      current = current[token];
    }
  }
  return current;
}

export function setJsonPointer(document, pointer, value) {
  const tokens = parseJsonPointer(pointer);
  if (tokens.length === 0) throw new TypeError("cannot set the document root");
  let current = document;
  for (let index = 0; index < tokens.length - 1; index += 1) {
    const token = tokens[index];
    assertSegment(token);
    if (current === null || typeof current !== "object") {
      throw new TypeError("parent path does not exist");
    }
    if (Array.isArray(current)) {
      current = current[arrayIndex(token, false, current.length)];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) {
        throw new RangeError("missing path segment: " + token);
      }
      current = current[token];
    }
  }
  const last = tokens[tokens.length - 1];
  assertSegment(last);
  if (current === null || typeof current !== "object") {
    throw new TypeError("parent path does not exist");
  }
  if (Array.isArray(current)) {
    const index = arrayIndex(last, true, current.length);
    if (index === current.length) current.push(value);
    else current[index] = value;
  } else {
    current[last] = value;
  }
  return document;
}

export function removeJsonPointer(document, pointer) {
  const tokens = parseJsonPointer(pointer);
  if (tokens.length === 0) throw new TypeError("cannot remove the document root");
  let current = document;
  for (let index = 0; index < tokens.length - 1; index += 1) {
    const token = tokens[index];
    assertSegment(token);
    if (current === null || typeof current !== "object") return undefined;
    if (Array.isArray(current)) {
      const position = arrayIndex(token, false, current.length);
      current = current[position];
    } else {
      if (!Object.prototype.hasOwnProperty.call(current, token)) return undefined;
      current = current[token];
    }
  }
  const last = tokens[tokens.length - 1];
  assertSegment(last);
  if (current === null || typeof current !== "object") return undefined;
  if (Array.isArray(current)) {
    const position = arrayIndex(last, false, current.length);
    return current.splice(position, 1)[0];
  }
  if (!Object.prototype.hasOwnProperty.call(current, last)) return undefined;
  const removed = current[last];
  delete current[last];
  return removed;
}