mirror of
https://github.com/inzerosight/ZWUS-rs.git
synced 2026-09-18 08:05:46 +00:00
Add ZWUS-7 support and default encoding
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "zwus"
|
name = "zwus"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
description = "Zero Width Unicode Steganography — hide text in invisible characters."
|
description = "Zero Width Unicode Steganography — hide text in invisible characters."
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -4,7 +4,7 @@ Zero Width Unicode Steganography — hide text inside invisible characters.
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
zwus = "0.2"
|
zwus = "0.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
@@ -25,14 +25,14 @@ assert_eq!(decoded, vec![72, 101, 108]);
|
|||||||
|
|
||||||
### Base
|
### Base
|
||||||
|
|
||||||
Higher base = shorter output, but more likely visible in some renderers.
|
Base 7 ranks printable ASCII so common English characters use one or two digits. Base 6 keeps Unicode code points as numbers. Some platforms strip particular zero-width characters.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use zwus::Zwus;
|
use zwus::Zwus;
|
||||||
|
|
||||||
Zwus::encode_string_with_base("hi", 3); // safest
|
Zwus::encode_string_with_base("hi", 3); // safest
|
||||||
Zwus::encode_string_with_base("hi", 6); // default, compact
|
Zwus::encode_string_with_base("hi", 6); // code point encoding
|
||||||
Zwus::encode_string_with_base("hi", 8); // most compact
|
Zwus::encode_string_with_base("hi", 7); // default, compact for ordinary English text
|
||||||
```
|
```
|
||||||
|
|
||||||
Decode must match the encode base:
|
Decode must match the encode base:
|
||||||
@@ -44,6 +44,8 @@ let encoded = Zwus::encode_string_with_base("hi", 6);
|
|||||||
Zwus::decode_to_string_with_base(&encoded, 6);
|
Zwus::decode_to_string_with_base(&encoded, 6);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Number arrays use ordinary base digits in every standard. Base 7's frequency ranking applies only to strings. Decoding ignores visible text mixed into a payload.
|
||||||
|
|
||||||
### Embedded in visible text
|
### Embedded in visible text
|
||||||
|
|
||||||
Non-ZWUS characters are automatically ignored during decoding, so hidden payloads survive being mixed into normal text.
|
Non-ZWUS characters are automatically ignored during decoding, so hidden payloads survive being mixed into normal text.
|
||||||
|
|||||||
80
src/lib.rs
80
src/lib.rs
@@ -1,7 +1,7 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
pub const DEFAULT_BASE: u8 = 6;
|
pub const DEFAULT_BASE: u8 = 7;
|
||||||
pub const SUPPORTED_BASES: [u8; 3] = [3, 6, 8];
|
pub const SUPPORTED_BASES: [u8; 3] = [3, 6, 7];
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
struct Alphabet {
|
struct Alphabet {
|
||||||
@@ -21,11 +21,10 @@ const BASE_6: Alphabet = Alphabet {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const BASE_8: Alphabet = Alphabet {
|
const BASE_7: Alphabet = Alphabet {
|
||||||
unifier: '\u{200C}',
|
unifier: '\u{200C}',
|
||||||
digits: &[
|
digits: &[
|
||||||
'\u{200D}', '\u{200F}', '\u{00AD}', '\u{2060}', '\u{200B}', '\u{200E}', '\u{180E}',
|
'\u{200D}', '\u{200F}', '\u{00AD}', '\u{2060}', '\u{200B}', '\u{200E}', '\u{FEFF}',
|
||||||
'\u{FEFF}',
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,8 +33,8 @@ fn alphabet(base: u8) -> Alphabet {
|
|||||||
match base {
|
match base {
|
||||||
3 => BASE_3,
|
3 => BASE_3,
|
||||||
6 => BASE_6,
|
6 => BASE_6,
|
||||||
8 => BASE_8,
|
7 => BASE_7,
|
||||||
_ => panic!("Unsupported base {base}. Use 3, 6, or 8."),
|
_ => panic!("Unsupported base {base}. Use 3, 6, or 7."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,52 +97,99 @@ fn decode_numbers(text: &str, base: u8) -> Vec<u32> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PRIORITY: &str = "te aoinshrdlucmfwypvbgkjqxz.,!?'-:;()0123456789ETAOINSHRDLUCMFWYPVBGKJQXZ";
|
||||||
|
|
||||||
|
fn ascii_order() -> Vec<char> {
|
||||||
|
let mut order = Vec::new();
|
||||||
|
for c in PRIORITY
|
||||||
|
.chars()
|
||||||
|
.chain((32..=126).filter_map(char::from_u32))
|
||||||
|
{
|
||||||
|
if !order.contains(&c) {
|
||||||
|
order.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rank_of(c: char, order: &[char]) -> u32 {
|
||||||
|
order
|
||||||
|
.iter()
|
||||||
|
.position(|&x| x == c)
|
||||||
|
.map(|i| i as u32)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
if (c as u32) < 32 {
|
||||||
|
c as u32 + 95
|
||||||
|
} else {
|
||||||
|
c as u32
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point_of(rank: u32, order: &[char]) -> Option<char> {
|
||||||
|
if rank < 95 {
|
||||||
|
order.get(rank as usize).copied()
|
||||||
|
} else {
|
||||||
|
char::from_u32(if rank < 127 { rank - 95 } else { rank })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Zero Width Unicode Standard (ZWUS)
|
/// Zero Width Unicode Standard (ZWUS)
|
||||||
pub struct Zwus;
|
pub struct Zwus;
|
||||||
|
|
||||||
impl Zwus {
|
impl Zwus {
|
||||||
/// Encode a string using base 6 (default/compact).
|
/// Encode a string using base 7 (default/compact).
|
||||||
pub fn encode_string(text: &str) -> String {
|
pub fn encode_string(text: &str) -> String {
|
||||||
Self::encode_string_with_base(text, DEFAULT_BASE)
|
Self::encode_string_with_base(text, DEFAULT_BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode a string using base 3, 6, or 8.
|
/// Encode a string using base 3, 6, or 7.
|
||||||
pub fn encode_string_with_base(text: &str, base: u8) -> String {
|
pub fn encode_string_with_base(text: &str, base: u8) -> String {
|
||||||
encode_numbers(text.chars().map(|c| c as u32), base)
|
if base == 7 {
|
||||||
|
let order = ascii_order();
|
||||||
|
encode_numbers(text.chars().map(|c| rank_of(c, &order)), base)
|
||||||
|
} else {
|
||||||
|
encode_numbers(text.chars().map(|c| c as u32), base)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode numbers using base 6 (default/compact).
|
/// Encode numbers using base 7 (default/compact).
|
||||||
pub fn encode_number_array(numbers: &[u32]) -> String {
|
pub fn encode_number_array(numbers: &[u32]) -> String {
|
||||||
Self::encode_number_array_with_base(numbers, DEFAULT_BASE)
|
Self::encode_number_array_with_base(numbers, DEFAULT_BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode numbers using base 3, 6, or 8.
|
/// Encode numbers using base 3, 6, or 7.
|
||||||
pub fn encode_number_array_with_base(numbers: &[u32], base: u8) -> String {
|
pub fn encode_number_array_with_base(numbers: &[u32], base: u8) -> String {
|
||||||
encode_numbers(numbers.iter().copied(), base)
|
encode_numbers(numbers.iter().copied(), base)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode to string using base 6 (default/compact).
|
/// Decode to string using base 7 (default/compact).
|
||||||
/// Non-ZWUS chars are ignored automatically.
|
/// Non-ZWUS chars are ignored automatically.
|
||||||
pub fn decode_to_string(text: &str) -> String {
|
pub fn decode_to_string(text: &str) -> String {
|
||||||
Self::decode_to_string_with_base(text, DEFAULT_BASE)
|
Self::decode_to_string_with_base(text, DEFAULT_BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode to string using base 3, 6, or 8.
|
/// Decode to string using base 3, 6, or 7.
|
||||||
/// Non-ZWUS chars are ignored automatically.
|
/// Non-ZWUS chars are ignored automatically.
|
||||||
pub fn decode_to_string_with_base(text: &str, base: u8) -> String {
|
pub fn decode_to_string_with_base(text: &str, base: u8) -> String {
|
||||||
|
let order = (base == 7).then(ascii_order);
|
||||||
decode_numbers(text, base)
|
decode_numbers(text, base)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(char::from_u32)
|
.filter_map(|n| {
|
||||||
|
order
|
||||||
|
.as_ref()
|
||||||
|
.map_or_else(|| char::from_u32(n), |o| point_of(n, o))
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode to numbers using base 6 (default/compact).
|
/// Decode to numbers using base 7 (default/compact).
|
||||||
/// Non-ZWUS chars are ignored automatically.
|
/// Non-ZWUS chars are ignored automatically.
|
||||||
pub fn decode_to_number_array(text: &str) -> Vec<u32> {
|
pub fn decode_to_number_array(text: &str) -> Vec<u32> {
|
||||||
Self::decode_to_number_array_with_base(text, DEFAULT_BASE)
|
Self::decode_to_number_array_with_base(text, DEFAULT_BASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode to numbers using base 3, 6, or 8.
|
/// Decode to numbers using base 3, 6, or 7.
|
||||||
/// Non-ZWUS chars are ignored automatically.
|
/// Non-ZWUS chars are ignored automatically.
|
||||||
pub fn decode_to_number_array_with_base(text: &str, base: u8) -> Vec<u32> {
|
pub fn decode_to_number_array_with_base(text: &str, base: u8) -> Vec<u32> {
|
||||||
decode_numbers(text, base)
|
decode_numbers(text, base)
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,11 +1,11 @@
|
|||||||
use zwus::Zwus;
|
use zwus::Zwus;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_base_is_6() {
|
fn default_base_is_7() {
|
||||||
let text = "secret 🦀";
|
let text = "secret 🦀";
|
||||||
let numbers = [0, 72, 101];
|
let numbers = [0, 72, 101];
|
||||||
let encoded_text = Zwus::encode_string_with_base(text, 6);
|
let encoded_text = Zwus::encode_string_with_base(text, 7);
|
||||||
let encoded_numbers = Zwus::encode_number_array_with_base(&numbers, 6);
|
let encoded_numbers = Zwus::encode_number_array_with_base(&numbers, 7);
|
||||||
|
|
||||||
assert_eq!(Zwus::encode_string(text), encoded_text);
|
assert_eq!(Zwus::encode_string(text), encoded_text);
|
||||||
assert_eq!(Zwus::decode_to_string(&encoded_text), text);
|
assert_eq!(Zwus::decode_to_string(&encoded_text), text);
|
||||||
@@ -14,9 +14,9 @@ fn default_base_is_6() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip_string_base_3_6_8() {
|
fn roundtrip_string_base_3_6_7() {
|
||||||
let text = "secret 🦀 unicode";
|
let text = "secret 🦀 unicode";
|
||||||
for base in [3u8, 6, 8] {
|
for base in [3u8, 6, 7] {
|
||||||
let encoded = Zwus::encode_string_with_base(text, base);
|
let encoded = Zwus::encode_string_with_base(text, base);
|
||||||
let decoded = Zwus::decode_to_string_with_base(&encoded, base);
|
let decoded = Zwus::decode_to_string_with_base(&encoded, base);
|
||||||
assert_eq!(decoded, text);
|
assert_eq!(decoded, text);
|
||||||
@@ -24,9 +24,9 @@ fn roundtrip_string_base_3_6_8() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn roundtrip_number_array_base_3_6_8() {
|
fn roundtrip_number_array_base_3_6_7() {
|
||||||
let nums = [0u32, 72, 101, 108, 108, 111, 0x10FFFF];
|
let nums = [0u32, 72, 101, 108, 108, 111, 0x10FFFF];
|
||||||
for base in [3u8, 6, 8] {
|
for base in [3u8, 6, 7] {
|
||||||
let encoded = Zwus::encode_number_array_with_base(&nums, base);
|
let encoded = Zwus::encode_number_array_with_base(&nums, base);
|
||||||
let decoded = Zwus::decode_to_number_array_with_base(&encoded, base);
|
let decoded = Zwus::decode_to_number_array_with_base(&encoded, base);
|
||||||
assert_eq!(decoded, nums);
|
assert_eq!(decoded, nums);
|
||||||
@@ -38,6 +38,11 @@ fn decode_ignores_non_zwus_characters() {
|
|||||||
let hidden = Zwus::encode_string_with_base("secret", 6);
|
let hidden = Zwus::encode_string_with_base("secret", 6);
|
||||||
let mixed = format!("te{hidden}xt");
|
let mixed = format!("te{hidden}xt");
|
||||||
assert_eq!(Zwus::decode_to_string_with_base(&mixed, 6), "secret");
|
assert_eq!(Zwus::decode_to_string_with_base(&mixed, 6), "secret");
|
||||||
|
let hidden = Zwus::encode_string_with_base("secret", 7);
|
||||||
|
assert_eq!(
|
||||||
|
Zwus::decode_to_string_with_base(&format!("te{hidden}xt"), 7),
|
||||||
|
"secret"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -45,12 +50,12 @@ fn npm_vectors_decode_to_test123_checkmark() {
|
|||||||
// Paste encoded outputs from your npm package here:
|
// Paste encoded outputs from your npm package here:
|
||||||
// zwus.encodeString("Test123!✅", 3)
|
// zwus.encodeString("Test123!✅", 3)
|
||||||
// zwus.encodeString("Test123!✅", 6)
|
// zwus.encodeString("Test123!✅", 6)
|
||||||
// zwus.encodeString("Test123!✅", 8)
|
// zwus.encodeString("Test123!✅", 7)
|
||||||
//
|
//
|
||||||
// Keep these as raw strings to preserve invisible chars exactly.
|
// Keep these as raw strings to preserve invisible chars exactly.
|
||||||
let npm_base_3 = r#""#;
|
let npm_base_3 = r#""#;
|
||||||
let npm_base_6 = r#""#;
|
let npm_base_6 = r#""#;
|
||||||
let npm_base_8 = r#""#;
|
let npm_base_7 = "\u{feff}\u{feff}\u{200c}\u{200f}\u{200c}\u{200f}\u{200d}\u{200c}\u{200d}\u{200c}\u{200e}\u{2060}\u{200c}\u{200e}\u{200b}\u{200c}\u{200e}\u{200e}\u{200c}\u{200b}\u{200f}\u{200c}\u{200b}\u{200f}\u{200d}\u{feff}\u{200d}";
|
||||||
|
|
||||||
let expected = "Test123!✅";
|
let expected = "Test123!✅";
|
||||||
|
|
||||||
@@ -63,11 +68,12 @@ fn npm_vectors_decode_to_test123_checkmark() {
|
|||||||
"Paste base-6 encoded payload into npm_base_6"
|
"Paste base-6 encoded payload into npm_base_6"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!npm_base_8.is_empty(),
|
!npm_base_7.is_empty(),
|
||||||
"Paste base-8 encoded payload into npm_base_8"
|
"Paste base-7 encoded payload into npm_base_7"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(Zwus::decode_to_string_with_base(npm_base_3, 3), expected);
|
assert_eq!(Zwus::decode_to_string_with_base(npm_base_3, 3), expected);
|
||||||
assert_eq!(Zwus::decode_to_string_with_base(npm_base_6, 6), expected);
|
assert_eq!(Zwus::decode_to_string_with_base(npm_base_6, 6), expected);
|
||||||
assert_eq!(Zwus::decode_to_string_with_base(npm_base_8, 8), expected);
|
assert_eq!(Zwus::decode_to_string_with_base(npm_base_7, 7), expected);
|
||||||
|
assert_eq!(Zwus::encode_string_with_base(expected, 7), npm_base_7);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user