UNPKG

@fairfox/zws

Version:

Zero-width steganography utilities for invisibly embedding data in text using Unicode zero-width characters

102 lines (101 loc) 3.34 kB
// src/index.ts var START_MARKER = "​‌"; var END_MARKER = "‌​"; var ZERO_BIT = "​"; var ONE_BIT = "‌"; var MAX_DATA_LENGTH = 100; var MAX_ENCODED_LENGTH = MAX_DATA_LENGTH * 16; function hasEmbeddedData(text) { if (typeof text !== "string") return false; return text.includes(START_MARKER); } function embed(text, data) { const normalizedText = typeof text !== "string" ? String(text || "") : text; const normalizedData = typeof data !== "string" ? String(data || "") : data; if (hasEmbeddedData(normalizedText)) { return normalizedText; } try { const encodedData = encodeData(normalizedData); const marker = START_MARKER + encodedData + END_MARKER; const sentenceEnd = normalizedText.search(/[.!?]\s/); if (sentenceEnd !== -1) { const insertPos = sentenceEnd + 1; return normalizedText.substring(0, insertPos) + marker + normalizedText.substring(insertPos); } return normalizedText + marker; } catch (error) { console.warn("Failed to embed data:", error instanceof Error ? error.message : String(error)); return normalizedText; } } function extract(text) { if (typeof text !== "string") return ""; if (!hasEmbeddedData(text)) { return ""; } try { const pattern = new RegExp(`${START_MARKER}([${ZERO_BIT}${ONE_BIT}]*)${END_MARKER}`); const match = text.match(pattern); if (!match) { return ""; } return decodeData(match[1] || ""); } catch (error) { console.warn("Data extraction failed:", error); return ""; } } function getCleanText(text) { if (typeof text !== "string") return String(text || ""); return text.replace(new RegExp(`${START_MARKER}[${ZERO_BIT}${ONE_BIT}]*${END_MARKER}`, "g"), ""); } function encodeData(data) { if (data.length > MAX_DATA_LENGTH) { throw new Error(`Data too long: ${data.length} characters. Maximum allowed: ${MAX_DATA_LENGTH} characters.`); } for (let i = 0;i < data.length; i++) { const codePoint = data.codePointAt(i) || 0; if (codePoint > 65535) { throw new Error(`Invalid character in data: "${data[i]}" (U+${codePoint.toString(16).toUpperCase()}). Data must use UTF-8 safe characters only.`); } } const binary = Array.from(data).map((char) => char.charCodeAt(0).toString(2).padStart(16, "0")).join(""); return binary.split("").map((bit) => bit === "0" ? ZERO_BIT : ONE_BIT).join(""); } function decodeData(encodedBinary) { if (encodedBinary.length > MAX_ENCODED_LENGTH) { throw new Error(`Encoded data too long: ${encodedBinary.length} characters. Maximum allowed: ${MAX_ENCODED_LENGTH} characters.`); } const cleanBinary = encodedBinary.split("").filter((char) => char === ZERO_BIT || char === ONE_BIT).map((char) => char === ZERO_BIT ? "0" : "1").join(""); const chars = []; for (let i = 0;i < cleanBinary.length; i += 16) { const charBinary = cleanBinary.substring(i, i + 16); if (charBinary.length === 16) { const codePoint = Number.parseInt(charBinary, 2); chars.push(String.fromCharCode(codePoint)); } } return chars.join(""); } var zws = { START_MARKER, END_MARKER, ZERO_BIT, ONE_BIT, MAX_DATA_LENGTH, MAX_ENCODED_LENGTH, hasEmbeddedData, embed, extract, getCleanText, encodeData, decodeData }; var src_default = zws; export { src_default as default };