Skip to main content

combs_mesh/
codepoint.rs

1//! Unicode transport encoding — embeds an emoji's blocks in a plain
2//! `String` that survives text-only channels (chat, MCP tool results).
3//!
4//! ## Scheme (self-describing envelope, one per block)
5//!
6//! ```text
7//! [TAG char]  [2 × plane-15 length chars]  [ceil(len/2) × plane-16 data chars]
8//! ```
9//!
10//! - **Tag char**: `U+E0061 + block-type index` (TAG glyphs U+E0061..U+E006A
11//!   for the 10 block types). Marks the start of a block and names its type.
12//! - **Length**: payload length (bytes) as two 12-bit chunks, big-endian,
13//!   each stored in plane 15 (U+F0000..U+FFFFF, the Supplementary Private
14//!   Use Area-A): `U+F0000 + block-type index × 4096 + chunk`. Plane 15 is
15//!   split into 16 sub-ranges of 4096; the block type occupies sub-range
16//!   `index`, so length chars are type-checked against the tag char.
17//! - **Data**: the JSON payload bytes, two bytes (big-endian u16) per
18//!   codepoint in plane 16 (U+100000..U+10FFFF, Supplementary PUA-B):
19//!   `U+100000 + u16`. Odd payloads are zero-padded; the length field
20//!   trims the pad.
21//!
22//! Max payload is `2^24 - 1` bytes (24-bit length). All codepoints used
23//! are valid Unicode scalar values (no surrogates), so the output is a
24//! well-formed UTF-8/UTF-16 string on any platform. Decoders skip any
25//! non-marker text, so envelopes can ride inside ordinary prose.
26
27use crate::blocks::{Block, BlockTag};
28use crate::error::{MeshError, Result};
29
30/// Base of the TAG block-type marker chars (U+E0061 + index).
31pub const TAG_CHAR_BASE: u32 = 0xE0061;
32/// Base of plane 15 (Supplementary PUA-A).
33pub const PLANE15_BASE: u32 = 0xF0000;
34/// Base of plane 16 (Supplementary PUA-B).
35pub const PLANE16_BASE: u32 = 0x100000;
36/// Plane-15 sub-range size (16 sub-ranges of 4096 cover the plane).
37pub const SUBRANGE_SIZE: u32 = 4096;
38/// Maximum payload size (24-bit length field).
39pub const MAX_PAYLOAD: usize = 0xFF_FFFF;
40
41/// The tag char marking the start of a block of type `tag`.
42#[must_use]
43pub fn tag_char(tag: BlockTag) -> char {
44    // U+E0061 + index (0..10) is always a valid scalar value.
45    char::from_u32(TAG_CHAR_BASE + tag.index() as u32).unwrap_or('\u{E0061}')
46}
47
48/// Inverse of [`tag_char`]; `None` for any other char.
49#[must_use]
50pub fn tag_from_char(c: char) -> Option<BlockTag> {
51    let v = c as u32;
52    let idx = v.checked_sub(TAG_CHAR_BASE)?;
53    BlockTag::from_index(u8::try_from(idx).ok()?)
54}
55
56fn plane15_char(tag: BlockTag, chunk: u16) -> Result<char> {
57    if chunk >= SUBRANGE_SIZE as u16 {
58        return Err(MeshError::Unicode(format!("chunk {chunk} out of range")));
59    }
60    let v = PLANE15_BASE + tag.index() as u32 * SUBRANGE_SIZE + chunk as u32;
61    char::from_u32(v).ok_or_else(|| MeshError::Unicode(format!("invalid char U+{v:X}")))
62}
63
64fn plane15_value(c: char, tag: BlockTag) -> Result<u16> {
65    let v = c as u32;
66    let base = PLANE15_BASE + tag.index() as u32 * SUBRANGE_SIZE;
67    if !(base..base + SUBRANGE_SIZE).contains(&v) {
68        return Err(MeshError::Unicode(format!(
69            "expected plane-15 length char for {:?}, got U+{:X}",
70            tag, v
71        )));
72    }
73    Ok((v - base) as u16)
74}
75
76fn plane16_char(value: u16) -> Result<char> {
77    let v = PLANE16_BASE + value as u32;
78    char::from_u32(v).ok_or_else(|| MeshError::Unicode(format!("invalid char U+{v:X}")))
79}
80
81fn plane16_value(c: char) -> Result<u16> {
82    let v = c as u32;
83    if !(PLANE16_BASE..=PLANE16_BASE + 0xFFFF).contains(&v) {
84        return Err(MeshError::Unicode(format!(
85            "expected plane-16 data char, got U+{v:X}"
86        )));
87    }
88    Ok((v - PLANE16_BASE) as u16)
89}
90
91/// Encodes blocks to the Unicode envelope string (concatenated per block).
92pub fn encode_blocks(blocks: &[Block]) -> Result<String> {
93    let mut out = String::new();
94    for block in blocks {
95        let tag = block.tag();
96        let payload = block.payload()?;
97        if payload.len() > MAX_PAYLOAD {
98            return Err(MeshError::Unicode(format!(
99                "payload of {} bytes exceeds the 24-bit limit",
100                payload.len()
101            )));
102        }
103        out.push(tag_char(tag));
104        let len = payload.len() as u32;
105        out.push(plane15_char(tag, ((len >> 12) & 0xFFF) as u16)?);
106        out.push(plane15_char(tag, (len & 0xFFF) as u16)?);
107        let mut chunks = payload.chunks_exact(2);
108        for pair in &mut chunks {
109            out.push(plane16_char(u16::from_be_bytes([pair[0], pair[1]]))?);
110        }
111        let rem = chunks.remainder();
112        if let [last] = rem {
113            out.push(plane16_char(u16::from_be_bytes([*last, 0]))?);
114        }
115    }
116    Ok(out)
117}
118
119/// Decodes all block envelopes found in `s`. Non-marker chars are skipped,
120/// so the emoji may be embedded in arbitrary text. Any *started* envelope
121/// that is malformed (bad length chars, truncated data, bad JSON) is an
122/// error — never a panic.
123pub fn decode_blocks(s: &str) -> Result<Vec<Block>> {
124    let mut blocks = Vec::new();
125    let mut chars = s.chars();
126    while let Some(c) = chars.next() {
127        let Some(tag) = tag_from_char(c) else { continue };
128        let hi = next_char(&mut chars).and_then(|c| plane15_value(c, tag))?;
129        let lo = next_char(&mut chars).and_then(|c| plane15_value(c, tag))?;
130        let len = ((hi as usize) << 12) | lo as usize;
131        let count = len.div_ceil(2);
132        let mut payload = Vec::with_capacity(count * 2);
133        for _ in 0..count {
134            let value = next_char(&mut chars).and_then(plane16_value)?;
135            payload.extend_from_slice(&value.to_be_bytes());
136        }
137        payload.truncate(len);
138        blocks.push(Block::from_payload(tag, &payload)?);
139    }
140    Ok(blocks)
141}
142
143fn next_char(chars: &mut std::str::Chars<'_>) -> Result<char> {
144    chars
145        .next()
146        .ok_or_else(|| MeshError::Unicode("truncated envelope".into()))
147}