nehonix-uri-processor
Version:
A powerful URI processor for encoding, decoding, and analyzing URI data securely.
1,155 lines • 84.8 kB
JavaScript
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