UNPKG

nehonix-uri-processor

Version:

A powerful URI processor for encoding, decoding, and analyzing URI data securely.

1,155 lines 84.8 kB
import punycode from "punycode"; import { ncu, NehonixCoreUtils } from "../utils/NehonixCoreUtils"; import { NehonixSharedUtils } from "../common/NehonixCommonUtils"; import NES from "./NehonixEnc.service"; import { htmlEntities } from "../utils/html.enties"; import { AppLogger } from "../common/AppLogger"; class NDS { // private static hasBase64Pattern = NehonixCoreUtils.hasBase64Pattern; // // private static hasPercentEncoding = NehonixSharedUtils.hasPercentEncoding; // private static enc: typeof NehonixEncService = NehonixEncService; // private static hasDoublePercentEncoding = // NehonixCoreUtils.hasDoublePercentEncoding; // private static hasHexEncoding = NehonixCoreUtils.hasHexEncoding; // private static hasUnicodeEncoding = NehonixCoreUtils.hasUnicodeEncoding; // private static hasRawHexString = NehonixCoreUtils.hasRawHexString; // private static calculateBase64Confidence = NES.calculateBase64Confidence; // private static hasHTMLEntityEncoding = NehonixCoreUtils.hasHTMLEntityEncoding; // private static hasJWTFormat = NehonixCoreUtils.hasJWTFormat; // private static hasPunycode = NehonixCoreUtils.hasPunycode; // private static decodeBase64 = NehonixCoreUtils.decodeB64; // private static decodeRawHexWithoutPrefix = NehonixCoreUtils.drwp; // In your detectEncoding function or a new function static detectMixedEncodings(input) { const detectedEncodings = []; // Check for percent encoding if (/%[0-9A-Fa-f]{2}/.test(input)) { detectedEncodings.push("percentEncoding"); } // Check for Base64 content const base64Regex = /[A-Za-z0-9+/=]{4,}/g; const potentialBase64 = input.match(base64Regex); if (potentialBase64) { for (const match of potentialBase64) { if (NehonixSharedUtils.isBase64(match)) { detectedEncodings.push("base64"); break; } } } // Add more checks as needed return detectedEncodings; } /** * Automatically detects and decodes a URI based on the detected encoding type * @param input The URI string to decode * @returns The decoded string according to the most probable encoding type */ static detectAndDecode(input) { // Special case for URLs with parameters if (input.includes("?") && input.includes("=")) { const urlParts = input.split("?"); const basePath = urlParts[0]; const queryString = urlParts[1]; // Split query parameters const params = queryString.split("&"); const decodedParams = params.map((param) => { const [key, value] = param.split("="); if (!value) return param; // Handle cases where parameter has no value // Try to detect encoding for each parameter value const detection = NDS.detectEncoding(value); if (detection.confidence > 0.8) { try { let decodedValue = value; switch (detection.mostLikely) { case "base64": let base64Input = value; // Ensure proper padding while (base64Input.length % 4 !== 0) { base64Input += "="; } base64Input = base64Input.replace(/-/g, "+").replace(/_/g, "/"); decodedValue = NehonixSharedUtils.decodeB64(base64Input); // Check if the result is still Base64-encoded if (NehonixCoreUtils.hasBase64Pattern(decodedValue)) { let nestedBase64 = decodedValue; while (nestedBase64.length % 4 !== 0) { nestedBase64 += "="; } nestedBase64 = nestedBase64 .replace(/-/g, "+") .replace(/_/g, "/"); decodedValue = NehonixSharedUtils.decodeB64(nestedBase64); } // Handle case where decoded value contains '&' (e.g., 'true&') if (decodedValue.includes("&")) { return `${key}=${decodedValue.split("&")[0]}`; // Take only the first part } break; case "rawHexadecimal": if (/^[0-9A-Fa-f]+$/.test(value) && value.length % 2 === 0) { decodedValue = NDS.decodeRawHex(value); } break; case "percentEncoding": decodedValue = NDS.decodePercentEncoding(value); break; case "doublepercent": decodedValue = NDS.decodeDoublePercentEncoding(value); break; } // Validate the decoded value to ensure it's readable text const printableChars = decodedValue.replace(/[^\x20-\x7E]/g, "").length; const printableRatio = printableChars / decodedValue.length; // Only use decoded value if it's mostly printable characters if (printableRatio > 0.7) { return `${key}=${decodedValue}`; } } catch (e) { AppLogger.warn(`Failed to decode parameter ${key}: ${e}`); } } return param; // Keep original for non-decodable params }); // Reconstruct URL with decoded parameters const decodedQueryString = decodedParams.join("&"); const decodedURL = `${basePath}?${decodedQueryString}`; if (decodedURL !== input) { const paramEncoding = params .map((param) => { const [key, value] = param.split("="); if (value) { return NDS.detectEncoding(value).mostLikely; } return "none"; }) .find((type) => type !== "plainText" && type !== "none") || "unknown"; return { val: () => decodedURL, encodingType: paramEncoding, confidence: 0.85, }; } } // Process nested encoding const detection = NDS.detectEncoding(input); let decodedValue = input; if (detection.isNested && detection.nestedTypes) { try { decodedValue = input; for (const encType of detection.nestedTypes) { decodedValue = NDS.decode({ encodingType: encType, input, }); } return { val: () => decodedValue, encodingType: detection.mostLikely, confidence: detection.confidence, nestedTypes: detection.nestedTypes, }; } catch (e) { AppLogger.error(`Error while decoding nested encodings:`, e); } } try { switch (detection.mostLikely) { case "percentEncoding": decodedValue = NDS.decodePercentEncoding(input); break; case "doublepercent": decodedValue = NDS.decodeDoublePercentEncoding(input); break; case "base64": let base64Input = input; while (base64Input.length % 4 !== 0) { base64Input += "="; } decodedValue = NehonixSharedUtils.decodeB64(base64Input.replace(/-/g, "+").replace(/_/g, "/")); break; case "hex": decodedValue = NDS.decodeHex(input); break; case "rawHexadecimal": decodedValue = NDS.decodeRawHex(input); break; case "unicode": decodedValue = NDS.decodeUnicode(input); break; case "htmlEntity": decodedValue = NDS.decodeHTMLEntities(input); break; case "punycode": decodedValue = NDS.decodePunycode(input); break; case "jwt": decodedValue = NDS.decodeJWT(input); break; default: if (input.includes("=")) { const parts = input.split("="); const value = parts[parts.length - 1]; if (value && value.length >= 6 && /^[0-9A-Fa-f]+$/.test(value) && value.length % 2 === 0) { try { const decodedParam = NDS.decodeRawHex(value); const printableChars = decodedParam.replace(/[^\x20-\x7E]/g, "").length; const printableRatio = printableChars / decodedParam.length; if (printableRatio > 0.7) { decodedValue = input.replace(value, decodedParam); return { val: () => decodedValue, encodingType: "rawHexadecimal", confidence: 0.8, }; } } catch (_a) { // Fall through to return original } } } decodedValue = input; } const printableChars = decodedValue.replace(/[^\x20-\x7E]/g, "").length; const printableRatio = printableChars / decodedValue.length; if (printableRatio < 0.7 && detection.mostLikely !== "plainText") { AppLogger.warn(`Decoded value contains too many unprintable characters (${printableRatio.toFixed(2)}), reverting to original`); decodedValue = input; } } catch (e) { AppLogger.error(`Error while decoding using ${detection.mostLikely}:`, e); decodedValue = input; } return { val: () => decodedValue, encodingType: detection.mostLikely, confidence: detection.confidence, }; } // Decode JWT static decodeJWT(input) { const parts = input.split("."); if (parts.length !== 3) throw new Error("Invalid JWT format"); try { // Décoder seulement les parties header et payload (pas la signature) const header = NehonixSharedUtils.decodeB64(parts[0].replace(/-/g, "+").replace(/_/g, "/")); const payload = NehonixSharedUtils.decodeB64(parts[1].replace(/-/g, "+").replace(/_/g, "/")); // Formater en JSON pour une meilleure lisibilité const headerObj = JSON.parse(header); const payloadObj = JSON.parse(payload); return JSON.stringify({ header: headerObj, payload: payloadObj, signature: "[signature]", // Ne pas décoder la signature }, null, 2); } catch (e) { throw new Error(`JWT decoding failed: ${e.message}`); } } // =============== DECODING METHODS =============== /** * Decodes percent encoding (URL) */ static decodePercentEncoding(input) { try { return decodeURIComponent(input); } catch (e) { // In case of error (invalid sequence), try to decode valid parts AppLogger.warn("Error while percent-decoding, attempting partial decoding"); return input.replace(/%[0-9A-Fa-f]{2}/g, (match) => { try { return decodeURIComponent(match); } catch (_a) { return match; } }); } } /** // * Decodes double percent encoding // */ // static decodeDoublePercentEncoding(input: string): string { // // First decode %25XX to %XX, then decode %XX // const firstPass = input.replace(/%25([0-9A-Fa-f]{2})/g, (match, hex) => { // return `%${hex}`; // }); // return NDS.decodePercentEncoding(firstPass); // } /** * Decodes hexadecimal encoding */ /** * Fix 1: Proper hex string decoding implementation */ static decodeHex(input) { // Remove any whitespace and convert to lowercase input = input.trim().toLowerCase(); // Check if input is a valid hex string if (!/^[0-9a-f]+$/.test(input)) { if (this.throwError) { throw new Error("Invalid hex string"); } } // Ensure even number of characters if (input.length % 2 !== 0) { throw new Error("Hex string must have an even number of characters"); } try { let result = ""; for (let i = 0; i < input.length; i += 2) { const hexByte = input.substring(i, i + 2); const charCode = parseInt(hexByte, 16); result += String.fromCharCode(charCode); } return result; } catch (e) { throw new Error(`Hex decoding failed: ${e.message}`); } } /** * Decodes Unicode encoding */ static decodeUnicode(input) { try { // Replace \uXXXX and \u{XXXXX} with their equivalent characters return input .replace(/\\u([0-9A-Fa-f]{4})/g, (match, hex) => { return String.fromCodePoint(parseInt(hex, 16)); }) .replace(/\\u\{([0-9A-Fa-f]+)\}/g, (match, hex) => { return String.fromCodePoint(parseInt(hex, 16)); }); } catch (e) { throw new Error(`Unicode decoding failed: ${e.message}`); } } /** * Decodes HTML entities */ static decodeHTMLEntities(input) { const entities = htmlEntities; // Replace named entities let result = input; for (const [entity, char] of Object.entries(entities)) { result = result.replace(new RegExp(entity, "g"), char); } // Replace numeric entities (decimal) result = result.replace(/&#(\d+);/g, (match, dec) => { return String.fromCodePoint(parseInt(dec, 10)); }); // Replace numeric entities (hexadecimal) result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => { return String.fromCodePoint(parseInt(hex, 16)); }); return result; } /** * Decodes punycode * Note: Requires the 'punycode' library */ static decodePunycode(input) { try { // If the punycode module is available if (typeof require !== "undefined") { // For URLs with international domains return input.replace(/xn--[a-z0-9-]+/g, (match) => { try { return punycode.decode(match.replace("xn--", "")); } catch (_a) { return match; } }); } else { // Alternative for browser (less accurate) // For a complete browser implementation, include a punycode library AppLogger.warn("Punycode module not available, limited punycode decoding"); return input; } } catch (e) { throw new Error(`Punycode decoding failed: ${e.message}`); } } /** * Automatically detects the encoding type(s) of a string (URI or raw text) * @param input The string to analyze * @param depth Internal recursion depth (default: 0) * @returns An object with detected types, confidence scores and the most likely one */ static detectEncoding(input, depth = 0) { const MAX_DEPTH = 3; if (depth > MAX_DEPTH || !input || input.length < 2) { return { types: ["plainText"], mostLikely: "plainText", confidence: 1.0, }; } const detectionScores = {}; const utils = NehonixSharedUtils; const isValidUrl = ncu.isValidUrl(input, NDS.default_checkurl_opt); // First, check for mixed encoding patterns const percentEncodedSegments = input.match(/%[0-9A-Fa-f]{2}/g); const hasPercentEncodedParts = percentEncodedSegments && percentEncodedSegments.length > 0; // Check what percentage of the input is percent-encoded if (hasPercentEncodedParts) { const encodedCharCount = percentEncodedSegments.length * 3; // Each %XX is 3 chars const encodedRatio = encodedCharCount / input.length; if (encodedRatio > 0.8) { // Mostly percent-encoded detectionScores["percentEncoding"] = 0.9; } else { // Partially percent-encoded detectionScores["partialPercentEncoding"] = 0.75 + encodedRatio * 0.2; // Also recognize it's still partially plain text detectionScores["plainText"] = 0.5 + (1 - encodedRatio) * 0.4; } } // Special handling for URLs try { if (isValidUrl) { // URL parameters may have individual encodings const url = new URL(input); if (url.search && url.search.length > 1) { // Track URL parameter encodings let hasEncodedParams = false; for (const [_, value] of new URLSearchParams(url.search)) { // Check for common encodings in parameter values if (/%[0-9A-Fa-f]{2}/.test(value)) { detectionScores["percentEncoding"] = Math.max(detectionScores["percentEncoding"] || 0, 0.85); hasEncodedParams = true; } if (/^[A-Za-z0-9+\/=]{4,}$/.test(value)) { detectionScores["base64"] = Math.max(detectionScores["base64"] || 0, 0.82); hasEncodedParams = true; } if (/^[0-9A-Fa-f]+$/.test(value) && value.length % 2 === 0) { detectionScores["rawHexadecimal"] = Math.max(detectionScores["rawHexadecimal"] || 0, 0.8); hasEncodedParams = true; } if (/\\u[0-9A-Fa-f]{4}/.test(value)) { detectionScores["unicode"] = Math.max(detectionScores["unicode"] || 0, 0.85); hasEncodedParams = true; } if (/\\x[0-9A-Fa-f]{2}/.test(value)) { detectionScores["jsEscape"] = Math.max(detectionScores["jsEscape"] || 0, 0.83); hasEncodedParams = true; } } if (hasEncodedParams) { detectionScores["url"] = 0.9; // High confidence this is a URL with encoded params } } } } catch (e) { // URL parsing failed, continue with normal detection } // Standard encoding detection checks const detectionChecks = [ { type: "doublepercent", fn: utils.isDoublePercent, score: 0.95 }, { type: "percentEncoding", fn: utils.isPercentEncoding, score: 0.9, partialDetectionFn: (s) => { const matches = s.match(/%[0-9A-Fa-f]{2}/g); const isPartial = matches !== null && matches.length > 0; const ratio = isPartial ? (matches.length * 3) / s.length : 0; return { isPartial, ratio }; }, }, { type: "base64", fn: utils.isBase64, score: 0.9, minLength: 4, partialDetectionFn: (s) => { const base64Segments = s.match(/[A-Za-z0-9+\/=]{4,}/g); const isPartial = base64Segments !== null && base64Segments.some((seg) => seg.length >= 4); let totalBase64Length = 0; if (isPartial && base64Segments) { totalBase64Length = base64Segments.reduce((sum, seg) => sum + seg.length, 0); } return { isPartial, ratio: totalBase64Length / s.length }; }, }, { type: "urlSafeBase64", fn: utils.isUrlSafeBase64, score: 0.93, minLength: 4, }, { type: "base32", fn: utils.isBase32, score: 0.88, minLength: 8 }, { type: "asciihex", fn: utils.isAsciiHex, score: 0.85 }, { type: "asciioct", fn: utils.isAsciiOct, score: 0.85 }, { type: "hex", fn: utils.isHex, score: 0.8, minLength: 6, partialDetectionFn: (s) => { const hexSegments = s.match(/[0-9A-Fa-f]{6,}/g); const isPartial = hexSegments !== null && hexSegments.length > 0; let totalHexLength = 0; if (isPartial && hexSegments) { totalHexLength = hexSegments.reduce((sum, seg) => sum + seg.length, 0); } return { isPartial, ratio: totalHexLength / s.length }; }, }, { type: "rawHexadecimal", fn: utils.hasRawHexString, score: 0.85, minLength: 4, }, { type: "unicode", fn: utils.isUnicode, score: 0.8, partialDetectionFn: (s) => { const unicodeMatches = s.match(/\\u[0-9A-Fa-f]{4}/g); const isPartial = unicodeMatches !== null && unicodeMatches.length > 0; let totalUnicodeLength = 0; if (isPartial && unicodeMatches) { totalUnicodeLength = unicodeMatches.reduce((sum, seg) => sum + seg.length, 0); } return { isPartial, ratio: totalUnicodeLength / s.length }; }, }, { type: "htmlEntity", fn: utils.isHtmlEntity, score: 0.8, partialDetectionFn: (s) => { const entityMatches = s.match(/&[a-zA-Z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/g); const isPartial = entityMatches !== null && entityMatches.length > 0; let totalEntityLength = 0; if (isPartial && entityMatches) { totalEntityLength = entityMatches.reduce((sum, seg) => sum + seg.length, 0); } return { isPartial, ratio: totalEntityLength / s.length }; }, }, { type: "decimalHtmlEntity", fn: utils.isDecimalHtmlEntity, score: 0.83 }, { type: "quotedPrintable", fn: utils.isQuotedPrintable, score: 0.77 }, { type: "punycode", fn: utils.isPunycode, score: 0.9 }, { type: "rot13", fn: utils.isRot13.bind(utils), score: 0.9 }, { type: "utf7", fn: utils.isUtf7, score: 0.75 }, { type: "jsEscape", fn: utils.isJsEscape, score: 0.8, partialDetectionFn: (s) => { const jsEscapeMatches = s.match(/\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\[0-7]{3}/g); const isPartial = jsEscapeMatches !== null && jsEscapeMatches.length > 0; let totalEscapeLength = 0; if (isPartial && jsEscapeMatches) { totalEscapeLength = jsEscapeMatches.reduce((sum, seg) => sum + seg.length, 0); } return { isPartial, ratio: totalEscapeLength / s.length }; }, }, { type: "cssEscape", fn: utils.isCssEscape, score: 0.78 }, { type: "jwt", fn: utils.hasJWTFormat, score: 0.95, minLength: 15 }, ]; for (const { type, fn, score, minLength, partialDetectionFn, } of detectionChecks) { // Skip checks if input is too short for this encoding if (minLength && input.length < minLength) continue; try { // First, try full detection if (fn(input)) { detectionScores[type] = score; // Try to verify by decoding and checking result try { const decoded = NDS.decodeSingle(input, type); if (decoded && decoded !== input) { // Calculate how "sensible" the decoded result is const printableChars = decoded.replace(/[^\x20-\x7E]/g, "").length; const printableRatio = printableChars / decoded.length; if (printableRatio > 0.8) { // Boost confidence for successful decoding detectionScores[type] += 0.05; } else if (printableRatio < 0.5) { // Reduce confidence for gibberish output detectionScores[type] -= 0.1; } } } catch (_) { // Failed to decode, reduce confidence slightly detectionScores[type] -= 0.1; } } // Then, try partial detection if available else if (partialDetectionFn) { const partialResult = partialDetectionFn(input); if (partialResult.isPartial) { // Calculate confidence based on the ratio of encoded content const partialConfidence = 0.6 + partialResult.ratio * 0.3; detectionScores[`partial${type.charAt(0).toUpperCase() + type.slice(1)}`] = partialConfidence; // If a significant portion is encoded, try to decode those parts if (partialResult.ratio > 0.3) { try { const partialDecode = NDS.tryPartialDecode(input, type); if (partialDecode.success) { // Successful partial decoding boosts confidence detectionScores[`partial${type.charAt(0).toUpperCase() + type.slice(1)}`] += 0.05; } } catch (_) { // Partial decoding failed, continue } } } } } catch (e) { // Skip failed detection checks } } // Try recursive nested encoding detection if we're still shallow if (depth < MAX_DEPTH) { const nested = NDS.detectNestedEncoding(input, depth + 1); if (nested.isNested) { const nestedKey = `nested:${nested.outerType}+${nested.innerType}`; detectionScores[nestedKey] = nested.confidenceScore; } } // Check for mixed encoding patterns if (Object.keys(detectionScores).length > 1) { // If we have multiple different encodings, it might be mixed const encodingTypes = Object.keys(detectionScores); if (encodingTypes.some((type) => type.startsWith("partial"))) { detectionScores["mixedEncoding"] = 0.85; // High confidence this is mixed encoding } } // Fallback: plain text if no encodings detected if (Object.keys(detectionScores).length === 0) { detectionScores["plainText"] = 1.0; } else { // Always include plainText as a possibility with appropriate confidence // The more encoded it seems, the less likely it's plain text const maxNonPlainTextScore = Math.max(...Object.entries(detectionScores) .filter(([type]) => type !== "plainText") .map(([_, score]) => score)); if (maxNonPlainTextScore < 0.8) { // If other encoding confidence is low, plain text is still likely detectionScores["plainText"] = 1.0 - maxNonPlainTextScore; } } // Sort by confidence const sorted = Object.entries(detectionScores).sort((a, b) => b[1] - a[1]); // Build the result const result = { types: sorted.map(([type]) => type), mostLikely: sorted[0][0], confidence: sorted[0][1], }; // Add partial encoding info if detected const partialEncodings = sorted .filter(([type]) => type.startsWith("partial")) .map(([type, score]) => ({ type: type.replace("partial", "").toLowerCase(), confidence: score, })); if (partialEncodings.length > 0) { result.partialEncodings = partialEncodings; } // Include nested encoding info if available if (depth < MAX_DEPTH) { const nested = NDS.detectNestedEncoding(input, depth + 1); if (nested.isNested) { result.isNested = true; if (nested.outerType && nested.innerType) result.nestedTypes = [nested.outerType, nested.innerType]; } } return result; } /** * Attempts to decode parts of a string that appear to be encoded * @param input The potentially partially encoded string * @param encodingType The encoding type to try * @returns Object indicating success and decoded parts */ static tryPartialDecode(input, encodingType) { try { switch (encodingType) { case "percentEncoding": // Replace percent-encoded segments return { success: true, decoded: input.replace(/%[0-9A-Fa-f]{2}/g, (match) => { try { return decodeURIComponent(match); } catch (_a) { return match; } }), }; case "htmlEntity": // Replace HTML entities return { success: true, decoded: input.replace(/&[a-zA-Z]+;|&#[0-9]+;|&#x[0-9a-fA-F]+;/g, (match) => { try { const tempEl = document.createElement("div"); tempEl.innerHTML = match; return tempEl.textContent || match; } catch (_a) { return match; } }), }; case "unicode": // Replace Unicode escape sequences return { success: true, decoded: input.replace(/\\u[0-9A-Fa-f]{4}/g, (match) => { try { return String.fromCharCode(parseInt(match.slice(2), 16)); } catch (_a) { return match; } }), }; case "jsEscape": // Replace JavaScript escape sequences return { success: true, decoded: input.replace(/\\x[0-9A-Fa-f]{2}|\\u[0-9A-Fa-f]{4}|\\[0-7]{3}/g, (match) => { try { return JSON.parse(`"${match}"`); } catch (_a) { return match; } }), }; default: return { success: false }; } } catch (e) { return { success: false }; } } /** * Helper function to detect nested encodings * @param input The string to analyze * @param depth Current recursion depth * @returns Information about detected nested encodings */ static detectNestedEncoding(input, depth = 0) { // Implementation similar to the original, with improved detection for partial encodings const MAX_DEPTH = 3; if (depth > MAX_DEPTH) { return { isNested: false, confidenceScore: 0 }; } try { // First identify the most likely outer encoding const outerResult = NDS.detectEncoding(input, depth); if (outerResult.mostLikely === "plainText") { return { isNested: false, confidenceScore: 0 }; } // Try to decode the outer layer let decoded; try { decoded = NDS.decodeSingle(input, outerResult.mostLikely); } catch (e) { return { isNested: false, confidenceScore: 0 }; } if (!decoded || decoded === input) { return { isNested: false, confidenceScore: 0 }; } // Check for inner encoding in the decoded result const innerResult = NDS.detectEncoding(decoded, depth + 1); if (innerResult.mostLikely === "plainText") { return { isNested: false, confidenceScore: 0 }; } // Validate by trying to decode both layers try { const fullyDecoded = NDS.decodeSingle(decoded, innerResult.mostLikely); if (fullyDecoded && fullyDecoded !== decoded && fullyDecoded !== input) { // Calculate confidence based on how "clean" the decoded result is const printableRatio = fullyDecoded.replace(/[^\x20-\x7E]/g, "").length / fullyDecoded.length; const confidenceBoost = printableRatio > 0.8 ? 0.1 : 0; return { isNested: true, outerType: outerResult.mostLikely, innerType: innerResult.mostLikely, confidenceScore: Math.min(0.95, outerResult.confidence * 0.7 + innerResult.confidence * 0.3 + confidenceBoost), }; } } catch (e) { // Decoding failed, probably not nested } return { isNested: false, confidenceScore: 0 }; } catch (e) { return { isNested: false, confidenceScore: 0 }; } } //new /** * Decodes ROT13 encoded text */ static decodeRot13(input) { return input.replace(/[a-zA-Z]/g, (char) => { const code = char.charCodeAt(0); // For uppercase letters (A-Z) if (code >= 65 && code <= 90) { return String.fromCharCode(((code - 65 + 13) % 26) + 65); } // For lowercase letters (a-z) else if (code >= 97 && code <= 122) { return String.fromCharCode(((code - 97 + 13) % 26) + 97); } return char; }); } /** * Decodes Base32 encoded text */ static decodeBase32(input) { // Base32 alphabet (RFC 4648) const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // Remove padding characters and whitespace const cleanInput = input .toUpperCase() .replace(/=+$/, "") .replace(/\s/g, ""); let bits = ""; let result = ""; // Convert each character to its 5-bit binary representation for (let i = 0; i < cleanInput.length; i++) { const char = cleanInput[i]; const index = alphabet.indexOf(char); if (index === -1) throw new Error(`Invalid Base32 character: ${char}`); // Convert to 5-bit binary bits += index.toString(2).padStart(5, "0"); } // Process 8 bits at a time to construct bytes for (let i = 0; i + 8 <= bits.length; i += 8) { const byte = bits.substring(i, i + 8); result += String.fromCharCode(parseInt(byte, 2)); } return result; } /** * Decodes URL-safe Base64 encoded text */ static decodeUrlSafeBase64(input) { // Convert URL-safe characters back to standard Base64 const standardBase64 = input .replace(/-/g, "+") .replace(/_/g, "/") .replace(/=+$/, ""); // Remove padding if present // Add padding if needed let padded = standardBase64; while (padded.length % 4 !== 0) { padded += "="; } return NehonixSharedUtils.decodeB64(padded); } /** * Decodes JavaScript escape sequences */ static decodeJsEscape(input) { if (!input.includes("\\")) return input; try { // Handle various JavaScript escape sequences return input.replace(/\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[0-7]{1,3}|.)/g, (match, escape) => { if (escape.startsWith("x")) { // Hex escape \xFF return String.fromCharCode(parseInt(escape.substring(1), 16)); } else if (escape.startsWith("u")) { // Unicode escape \uFFFF return String.fromCharCode(parseInt(escape.substring(1), 16)); } else if (/^[0-7]+$/.test(escape)) { // Octal escape \000 return String.fromCharCode(parseInt(escape, 8)); } else { // Single character escapes like \n, \t, etc. switch (escape) { case "n": return "\n"; case "t": return "\t"; case "r": return "\r"; case "b": return "\b"; case "f": return "\f"; case "v": return "\v"; case "0": return "\0"; default: return escape; // For \", \', \\, etc. } } }); } catch (e) { AppLogger.warn("JS escape decode error:", e); return input; } } static decodeCharacterEscapes(input) { // Handle JavaScript/C-style character escapes: \x74\x72\x75\x65 return input.replace(/\\x([0-9A-Fa-f]{2})|\\([0-7]{1,3})|\\u([0-9A-Fa-f]{4})/g, (match, hex, octal, unicode) => { if (hex) { return String.fromCharCode(parseInt(hex, 16)); } else if (octal) { return String.fromCharCode(parseInt(octal, 8)); } else if (unicode) { return String.fromCharCode(parseInt(unicode, 16)); } return match; }); } /** * Decodes CSS escape sequences */ static decodeCssEscape(input) { return (input // Handle Unicode escapes with variable-length hex digits .replace(/\\([0-9A-Fa-f]{1,6})\s?/g, (_, hex) => String.fromCharCode(parseInt(hex, 16))) // Handle simple character escapes (any non-hex character that's escaped) .replace(/\\(.)/g, (_, char) => char)); } /** * Decodes UTF-7 encoded text */ static decodeUtf7(input) { let result = ""; let inBase64 = false; let base64Chars = ""; for (let i = 0; i < input.length; i++) { if (inBase64) { if (input[i] === "-") { // End of Base64 section if (base64Chars.length > 0) { // Convert accumulated Base64 to UTF-16 and then to string try { const bytes = NehonixSharedUtils.decodeB64(base64Chars); // UTF-7 encodes 16-bit Unicode chars as Base64 for (let j = 0; j < bytes.length; j += 2) { const charCode = bytes.charCodeAt(j) | (bytes.charCodeAt(j + 1) << 8); result += String.fromCharCode(charCode); } } catch (e) { // On error, just append the raw text result += "+" + base64Chars + "-"; } } else if (base64Chars === "") { // "+- is just a literal '+' result += "+"; } inBase64 = false; base64Chars = ""; } else if ((input[i] >= "A" && input[i] <= "Z") || (input[i] >= "a" && input[i] <= "z") || (input[i] >= "0" && input[i] <= "9") || input[i] === "+" || input[i] === "/") { // Valid Base64 character base64Chars += input[i]; } else { // Invalid character ends Base64 section if (base64Chars.length > 0) { try { const bytes = NehonixSharedUtils.decodeB64(base64Chars); for (let j = 0; j < bytes.length; j += 2) { const charCode = bytes.charCodeAt(j) | (bytes.charCodeAt(j + 1) << 8); result += String.fromCharCode(charCode); } } catch (e) { result += "+" + base64Chars; } } inBase64 = false; base64Chars = ""; result += input[i]; } } else if (input[i] === "+") { if (i + 1 < input.length && input[i + 1] === "-") { // '+-' is a literal '+' result += "+"; i++; // Skip the next character } else { // Start of Base64 section inBase64 = true; base64Chars = ""; } } else { // Regular character result += input[i]; } } // Handle unclosed Base64 section if (inBase64 && base64Chars.length > 0) { result += "+" + base64Chars; } return result; } /** * Decodes Quoted-Printable encoded text */ static decodeQuotedPrintable(input) { // Remove soft line breaks (=<CR><LF>) let cleanInput = input.replace(/=(?:\r\n|\n|\r)/g, ""); // Decode hex characters return cleanInput.replace(/=([0-9A-Fa-f]{2})/g, (_, hex) => { return String.fromCharCode(parseInt(hex, 16)); }); } /** * Decodes decimal HTML entity encoded text */ static decodeDecimalHtmlEntity(input) { return input.replace(/&#(\d+);/g, (_, dec) => { return String.fromCharCode(parseInt(dec, 10)); }); } /** * Decodes ASCII hex encoded text (where ASCII values are represented as hex) */ static decodeAsciiHex(input) { // Match pairs of hex digits const hexPairs = input.match(/[0-9A-Fa-f]{2}/g); if (!hexPairs) return input; return hexPairs .map((hex) => String.fromCharCode(parseInt(hex, 16))) .join(""); } /** * Decodes ASCII octal encoded text */ static decodeAsciiOct(input) { // Match 3-digit octal codes return input.replace(/\\([0-7]{3})/g, (_, oct) => { return String.fromCharCode(parseInt(oct, 8)); }); } /** * Auto-detects encoding and recursively decodes until plaintext * @param input The encoded string * @param maxIterations Maximum number of decoding iterations to prevent infinite loops * @returns Fully decoded plaintext */ static decodeAnyToPlaintext(input, opt = { output: { encodeUrl: false }, }) { this.throwError = false; let result = input; let lastResult = ""; let iterations = 0; let confidence = 0; let encodingType = "UNKNOWN_TYPE"; const maxIterations = opt.maxIterations || 12; const decodingHistory = []; // Smart initial handling for URLs const isUrl = ncu.isValidUrl(result, NDS.default_checkurl_opt); if (isUrl) { try { const parsedUrl = new URL(result); // Process query parameters const paramProcessed = NDS.handleUriParameters(result, maxIterations, opt); if (paramProcessed !== result) { result = paramProcessed; decodingHistory.push({ result, type: "urlParameters", confidence: 0.9, }); } // Process pathname if it contains percent encoding if (/%[0-9A-Fa-f]{2}/.test(parsedUrl.pathname)) { const decodedPath = NDS.decodePartial(parsedUrl.pathname, "percentEncoding"); const newUrl = `${parsedUrl.protocol}//${parsedUrl.host}${decodedPath}${parsedUrl.search}${parsedUrl.hash}`; if (newUrl !== result) { result = newUrl; decodingHistory.push({ result, type: "urlPathPercentEncoding", confidence: 0.85, }); } } } catch (e) { AppLogger.warn("URL processing error:", e); } } // Iterate until no changes or max iterations reached while (iterations < maxIterations && result !== lastResult) { lastResult = result; const detection = NDS.detectEncoding(result); // Stop if confident it's plain text if (detection.mostLikely === "plainText" && detection.confidence > 0.85) { confidence = detection.confidence; encodingType = "plainText"; break; } // Try all detected encoding types in order of confidence const typesToTry = detection.types .map((type) => { var _a, _b; return ({ type, confidence: type === detection.mostLikely ? detection.confidence : ((_b = (_a = d