@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
JavaScript
// 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
};