🏡 index : quine-global/webdriverio.git

import safeRegexTest from 'safe-regex2'

export const SENSITIVE_DATA_REPLACER = '**MASKED**'

const skipError = (aFunction: Function) => {
    try {
        return aFunction()
    } catch {
        return undefined
    }
}

/**
 * Parses a comma-separated string of regular expressions into an array of RegExp objects.
 * Supports both `/pattern/flags` and plain pattern formats.
 *
 * @param {string | undefined} maskingRegexString - The string containing regex patterns.
 * @returns {(RegExp[] | undefined)} Array of RegExp objects or undefined if input is invalid.
 */
export const parseMaskingPatterns = (maskingRegexString: string | undefined) => {
    if (typeof maskingRegexString !== 'string') { return undefined }
    const regexStrings = maskingRegexString?.split(/,\s*/).filter((regexStr) => regexStr.trim() !== '')

    return regexStrings?.map((regexStr) => {
        const regexParts = regexStr.match(/^\/(.*?)\/([gimsuy]*)$/)
        if (!regexParts && safeRegexTest(regexStr)) {
            // When passing only a simple string without `/` or flags, aka `(--key=)([^ ]*)`
            return skipError(() => new RegExp(regexStr))
        }

        if (regexParts?.[1] && safeRegexTest(regexParts[1])) {
            // Case with flag `/(--key=)([^ ]*)/i` or without flag `/(--key=)([^ ]*)/`
            return  skipError(() => (
                regexParts[2]
                    ? new RegExp(regexParts[1], regexParts[2])
                    : new RegExp(regexParts[1])))
        }

        return undefined
    }).filter((regex) => regex !== undefined)
}

/**
 * Masks sensitive data in a string using the provided masking patterns.
 *
 * - If a pattern has no capturing groups, the whole match is replaced with the mask.
 * - If a pattern has capturing groups, each group is replaced with the mask, preserving the rest of the match.
 *
 * @param {string} text - The text to mask.
 * @param {RegExp[] | undefined} maskingPatterns - Array of RegExp patterns to use for masking.
 * @returns {string} The masked text, or the original value if not a string or if no patterns are provided.
 */
export const mask = (text: string, maskingPatterns: RegExp[] | undefined) => {
    if (!maskingPatterns || typeof text !== 'string') { return text }

    const endsWithNewline = text.endsWith('\n')
    let maskedText = text
    maskingPatterns.forEach((maskingRegex) => {

        // Replace the whole match when no capturing groups or each capturing group
        maskedText = maskedText.replace(maskingRegex, (fullMatch, ...capturedGroupsAndMore) => {
            const capturedGroups = capturedGroupsAndMore.slice(0, capturedGroupsAndMore.length - 2)

            if (capturedGroups.length === 0) {
                return SENSITIVE_DATA_REPLACER
            }

            let matchedMaskedText = fullMatch
            capturedGroups.forEach((group) => {
                matchedMaskedText = matchedMaskedText.replace(group, SENSITIVE_DATA_REPLACER )
            })
            return matchedMaskedText
        })
    })

    // Ensure a trailing newline is preserved if present in the original text
    if (endsWithNewline && !maskedText.endsWith('\n')) {
        maskedText += '\n'
    }

    return maskedText
}