Buckets:
| function deepClone (obj) { | |
| if (obj === null || typeof obj !== 'object') { | |
| return obj | |
| } | |
| if (obj instanceof Date) { | |
| return new Date(obj.getTime()) | |
| } | |
| if (obj instanceof Array) { | |
| const cloned = [] | |
| for (let i = 0; i < obj.length; i++) { | |
| cloned[i] = deepClone(obj[i]) | |
| } | |
| return cloned | |
| } | |
| if (typeof obj === 'object') { | |
| const cloned = Object.create(Object.getPrototypeOf(obj)) | |
| for (const key in obj) { | |
| if (Object.prototype.hasOwnProperty.call(obj, key)) { | |
| cloned[key] = deepClone(obj[key]) | |
| } | |
| } | |
| return cloned | |
| } | |
| return obj | |
| } | |
| function parsePath (path) { | |
| const parts = [] | |
| let current = '' | |
| let inBrackets = false | |
| let inQuotes = false | |
| let quoteChar = '' | |
| for (let i = 0; i < path.length; i++) { | |
| const char = path[i] | |
| if (!inBrackets && char === '.') { | |
| if (current) { | |
| parts.push(current) | |
| current = '' | |
| } | |
| } else if (char === '[') { | |
| if (current) { | |
| parts.push(current) | |
| current = '' | |
| } | |
| inBrackets = true | |
| } else if (char === ']' && inBrackets) { | |
| // Always push the current value when closing brackets, even if it's an empty string | |
| parts.push(current) | |
| current = '' | |
| inBrackets = false | |
| inQuotes = false | |
| } else if ((char === '"' || char === "'") && inBrackets) { | |
| if (!inQuotes) { | |
| inQuotes = true | |
| quoteChar = char | |
| } else if (char === quoteChar) { | |
| inQuotes = false | |
| quoteChar = '' | |
| } else { | |
| current += char | |
| } | |
| } else { | |
| current += char | |
| } | |
| } | |
| if (current) { | |
| parts.push(current) | |
| } | |
| return parts | |
| } | |
| function setValue (obj, parts, value) { | |
| let current = obj | |
| for (let i = 0; i < parts.length - 1; i++) { | |
| const key = parts[i] | |
| // Type safety: Check if current is an object before using 'in' operator | |
| if (typeof current !== 'object' || current === null || !(key in current)) { | |
| return false // Path doesn't exist, don't create it | |
| } | |
| if (typeof current[key] !== 'object' || current[key] === null) { | |
| return false // Path doesn't exist properly | |
| } | |
| current = current[key] | |
| } | |
| const lastKey = parts[parts.length - 1] | |
| if (lastKey === '*') { | |
| if (Array.isArray(current)) { | |
| for (let i = 0; i < current.length; i++) { | |
| current[i] = value | |
| } | |
| } else if (typeof current === 'object' && current !== null) { | |
| for (const key in current) { | |
| if (Object.prototype.hasOwnProperty.call(current, key)) { | |
| current[key] = value | |
| } | |
| } | |
| } | |
| } else { | |
| // Type safety: Check if current is an object before using 'in' operator | |
| if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) { | |
| current[lastKey] = value | |
| } | |
| } | |
| return true | |
| } | |
| function removeKey (obj, parts) { | |
| let current = obj | |
| for (let i = 0; i < parts.length - 1; i++) { | |
| const key = parts[i] | |
| // Type safety: Check if current is an object before using 'in' operator | |
| if (typeof current !== 'object' || current === null || !(key in current)) { | |
| return false // Path doesn't exist, don't create it | |
| } | |
| if (typeof current[key] !== 'object' || current[key] === null) { | |
| return false // Path doesn't exist properly | |
| } | |
| current = current[key] | |
| } | |
| const lastKey = parts[parts.length - 1] | |
| if (lastKey === '*') { | |
| if (Array.isArray(current)) { | |
| // For arrays, we can't really "remove" all items as that would change indices | |
| // Instead, we set them to undefined which will be omitted by JSON.stringify | |
| for (let i = 0; i < current.length; i++) { | |
| current[i] = undefined | |
| } | |
| } else if (typeof current === 'object' && current !== null) { | |
| for (const key in current) { | |
| if (Object.prototype.hasOwnProperty.call(current, key)) { | |
| delete current[key] | |
| } | |
| } | |
| } | |
| } else { | |
| // Type safety: Check if current is an object before using 'in' operator | |
| if (typeof current === 'object' && current !== null && lastKey in current && Object.prototype.hasOwnProperty.call(current, lastKey)) { | |
| delete current[lastKey] | |
| } | |
| } | |
| return true | |
| } | |
| // Sentinel object to distinguish between undefined value and non-existent path | |
| const PATH_NOT_FOUND = Symbol('PATH_NOT_FOUND') | |
| function getValueIfExists (obj, parts) { | |
| let current = obj | |
| for (const part of parts) { | |
| if (current === null || current === undefined) { | |
| return PATH_NOT_FOUND | |
| } | |
| // Type safety: Check if current is an object before property access | |
| if (typeof current !== 'object' || current === null) { | |
| return PATH_NOT_FOUND | |
| } | |
| // Check if the property exists before accessing it | |
| if (!(part in current)) { | |
| return PATH_NOT_FOUND | |
| } | |
| current = current[part] | |
| } | |
| return current | |
| } | |
| function getValue (obj, parts) { | |
| let current = obj | |
| for (const part of parts) { | |
| if (current === null || current === undefined) { | |
| return undefined | |
| } | |
| // Type safety: Check if current is an object before property access | |
| if (typeof current !== 'object' || current === null) { | |
| return undefined | |
| } | |
| current = current[part] | |
| } | |
| return current | |
| } | |
| function redactPaths (obj, paths, censor, remove = false) { | |
| for (const path of paths) { | |
| const parts = parsePath(path) | |
| if (parts.includes('*')) { | |
| redactWildcardPath(obj, parts, censor, path, remove) | |
| } else { | |
| if (remove) { | |
| removeKey(obj, parts) | |
| } else { | |
| // Get value only if path exists - single traversal | |
| const value = getValueIfExists(obj, parts) | |
| if (value === PATH_NOT_FOUND) { | |
| continue | |
| } | |
| const actualCensor = typeof censor === 'function' | |
| ? censor(value, parts) | |
| : censor | |
| setValue(obj, parts, actualCensor) | |
| } | |
| } | |
| } | |
| } | |
| function redactWildcardPath (obj, parts, censor, originalPath, remove = false) { | |
| const wildcardIndex = parts.indexOf('*') | |
| if (wildcardIndex === parts.length - 1) { | |
| const parentParts = parts.slice(0, -1) | |
| let current = obj | |
| for (const part of parentParts) { | |
| if (current === null || current === undefined) return | |
| // Type safety: Check if current is an object before property access | |
| if (typeof current !== 'object' || current === null) return | |
| current = current[part] | |
| } | |
| if (Array.isArray(current)) { | |
| if (remove) { | |
| // For arrays, set all items to undefined which will be omitted by JSON.stringify | |
| for (let i = 0; i < current.length; i++) { | |
| current[i] = undefined | |
| } | |
| } else { | |
| for (let i = 0; i < current.length; i++) { | |
| const indexPath = [...parentParts, i.toString()] | |
| const actualCensor = typeof censor === 'function' | |
| ? censor(current[i], indexPath) | |
| : censor | |
| current[i] = actualCensor | |
| } | |
| } | |
| } else if (typeof current === 'object' && current !== null) { | |
| if (remove) { | |
| // Collect keys to delete to avoid issues with deleting during iteration | |
| const keysToDelete = [] | |
| for (const key in current) { | |
| if (Object.prototype.hasOwnProperty.call(current, key)) { | |
| keysToDelete.push(key) | |
| } | |
| } | |
| for (const key of keysToDelete) { | |
| delete current[key] | |
| } | |
| } else { | |
| for (const key in current) { | |
| const keyPath = [...parentParts, key] | |
| const actualCensor = typeof censor === 'function' | |
| ? censor(current[key], keyPath) | |
| : censor | |
| current[key] = actualCensor | |
| } | |
| } | |
| } | |
| } else { | |
| redactIntermediateWildcard(obj, parts, censor, wildcardIndex, originalPath, remove) | |
| } | |
| } | |
| function redactIntermediateWildcard (obj, parts, censor, wildcardIndex, originalPath, remove = false) { | |
| const beforeWildcard = parts.slice(0, wildcardIndex) | |
| const afterWildcard = parts.slice(wildcardIndex + 1) | |
| const pathArray = [] // Cached array to avoid allocations | |
| function traverse (current, pathLength) { | |
| if (pathLength === beforeWildcard.length) { | |
| if (Array.isArray(current)) { | |
| for (let i = 0; i < current.length; i++) { | |
| pathArray[pathLength] = i.toString() | |
| traverse(current[i], pathLength + 1) | |
| } | |
| } else if (typeof current === 'object' && current !== null) { | |
| for (const key in current) { | |
| pathArray[pathLength] = key | |
| traverse(current[key], pathLength + 1) | |
| } | |
| } | |
| } else if (pathLength < beforeWildcard.length) { | |
| const nextKey = beforeWildcard[pathLength] | |
| // Type safety: Check if current is an object before using 'in' operator | |
| if (current && typeof current === 'object' && current !== null && nextKey in current) { | |
| pathArray[pathLength] = nextKey | |
| traverse(current[nextKey], pathLength + 1) | |
| } | |
| } else { | |
| // Check if afterWildcard contains more wildcards | |
| if (afterWildcard.includes('*')) { | |
| // Recursively handle remaining wildcards | |
| // Wrap censor to prepend current path context | |
| const wrappedCensor = typeof censor === 'function' | |
| ? (value, path) => { | |
| const fullPath = [...pathArray.slice(0, pathLength), ...path] | |
| return censor(value, fullPath) | |
| } | |
| : censor | |
| redactWildcardPath(current, afterWildcard, wrappedCensor, originalPath, remove) | |
| } else { | |
| // No more wildcards, apply the redaction directly | |
| if (remove) { | |
| removeKey(current, afterWildcard) | |
| } else { | |
| const actualCensor = typeof censor === 'function' | |
| ? censor(getValue(current, afterWildcard), [...pathArray.slice(0, pathLength), ...afterWildcard]) | |
| : censor | |
| setValue(current, afterWildcard, actualCensor) | |
| } | |
| } | |
| } | |
| } | |
| if (beforeWildcard.length === 0) { | |
| traverse(obj, 0) | |
| } else { | |
| let current = obj | |
| for (let i = 0; i < beforeWildcard.length; i++) { | |
| const part = beforeWildcard[i] | |
| if (current === null || current === undefined) return | |
| // Type safety: Check if current is an object before property access | |
| if (typeof current !== 'object' || current === null) return | |
| current = current[part] | |
| pathArray[i] = part | |
| } | |
| if (current !== null && current !== undefined) { | |
| traverse(current, beforeWildcard.length) | |
| } | |
| } | |
| } | |
| function buildPathStructure (pathsToClone) { | |
| if (pathsToClone.length === 0) { | |
| return null // No paths to redact | |
| } | |
| // Parse all paths and organize by depth | |
| const pathStructure = new Map() | |
| for (const path of pathsToClone) { | |
| const parts = parsePath(path) | |
| let current = pathStructure | |
| for (let i = 0; i < parts.length; i++) { | |
| const part = parts[i] | |
| if (!current.has(part)) { | |
| current.set(part, new Map()) | |
| } | |
| current = current.get(part) | |
| } | |
| } | |
| return pathStructure | |
| } | |
| function selectiveClone (obj, pathStructure) { | |
| if (!pathStructure) { | |
| return obj // No paths to redact, return original | |
| } | |
| function cloneSelectively (source, pathMap, depth = 0) { | |
| if (!pathMap || pathMap.size === 0) { | |
| return source // No more paths to clone, return reference | |
| } | |
| if (source === null || typeof source !== 'object') { | |
| return source | |
| } | |
| if (source instanceof Date) { | |
| return new Date(source.getTime()) | |
| } | |
| if (Array.isArray(source)) { | |
| const cloned = [] | |
| for (let i = 0; i < source.length; i++) { | |
| const indexStr = i.toString() | |
| if (pathMap.has(indexStr) || pathMap.has('*')) { | |
| cloned[i] = cloneSelectively(source[i], pathMap.get(indexStr) || pathMap.get('*')) | |
| } else { | |
| cloned[i] = source[i] // Share reference for non-redacted items | |
| } | |
| } | |
| return cloned | |
| } | |
| // Handle objects | |
| const cloned = Object.create(Object.getPrototypeOf(source)) | |
| for (const key in source) { | |
| if (Object.prototype.hasOwnProperty.call(source, key)) { | |
| if (pathMap.has(key) || pathMap.has('*')) { | |
| cloned[key] = cloneSelectively(source[key], pathMap.get(key) || pathMap.get('*')) | |
| } else { | |
| cloned[key] = source[key] // Share reference for non-redacted properties | |
| } | |
| } | |
| } | |
| return cloned | |
| } | |
| return cloneSelectively(obj, pathStructure) | |
| } | |
| function validatePath (path) { | |
| if (typeof path !== 'string') { | |
| throw new Error('Paths must be (non-empty) strings') | |
| } | |
| if (path === '') { | |
| throw new Error('Invalid redaction path ()') | |
| } | |
| // Check for double dots | |
| if (path.includes('..')) { | |
| throw new Error(`Invalid redaction path (${path})`) | |
| } | |
| // Check for comma-separated paths (invalid syntax) | |
| if (path.includes(',')) { | |
| throw new Error(`Invalid redaction path (${path})`) | |
| } | |
| // Check for unmatched brackets | |
| let bracketCount = 0 | |
| let inQuotes = false | |
| let quoteChar = '' | |
| for (let i = 0; i < path.length; i++) { | |
| const char = path[i] | |
| if ((char === '"' || char === "'") && bracketCount > 0) { | |
| if (!inQuotes) { | |
| inQuotes = true | |
| quoteChar = char | |
| } else if (char === quoteChar) { | |
| inQuotes = false | |
| quoteChar = '' | |
| } | |
| } else if (char === '[' && !inQuotes) { | |
| bracketCount++ | |
| } else if (char === ']' && !inQuotes) { | |
| bracketCount-- | |
| if (bracketCount < 0) { | |
| throw new Error(`Invalid redaction path (${path})`) | |
| } | |
| } | |
| } | |
| if (bracketCount !== 0) { | |
| throw new Error(`Invalid redaction path (${path})`) | |
| } | |
| } | |
| function validatePaths (paths) { | |
| if (!Array.isArray(paths)) { | |
| throw new TypeError('paths must be an array') | |
| } | |
| for (const path of paths) { | |
| validatePath(path) | |
| } | |
| } | |
| function slowRedact (options = {}) { | |
| const { | |
| paths = [], | |
| censor = '[REDACTED]', | |
| serialize = JSON.stringify, | |
| strict = true, | |
| remove = false | |
| } = options | |
| // Validate paths upfront to match fast-redact behavior | |
| validatePaths(paths) | |
| // Build path structure once during setup, not on every call | |
| const pathStructure = buildPathStructure(paths) | |
| return function redact (obj) { | |
| if (strict && (obj === null || typeof obj !== 'object')) { | |
| if (obj === null || obj === undefined) { | |
| return serialize ? serialize(obj) : obj | |
| } | |
| if (typeof obj !== 'object') { | |
| return serialize ? serialize(obj) : obj | |
| } | |
| } | |
| // Only clone paths that need redaction | |
| const cloned = selectiveClone(obj, pathStructure) | |
| const original = obj // Keep reference to original for restore | |
| let actualCensor = censor | |
| if (typeof censor === 'function') { | |
| actualCensor = censor | |
| } | |
| redactPaths(cloned, paths, actualCensor, remove) | |
| if (serialize === false) { | |
| cloned.restore = function () { | |
| return deepClone(original) // Full clone only when restore is called | |
| } | |
| return cloned | |
| } | |
| if (typeof serialize === 'function') { | |
| return serialize(cloned) | |
| } | |
| return JSON.stringify(cloned) | |
| } | |
| } | |
| module.exports = slowRedact | |
Xet Storage Details
- Size:
- 15.1 kB
- Xet hash:
- 0397f6530ae64789cc0628c30551a11416055374b7df8a00c1bbe9e57f0b23a4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.