use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
pub const GLYPH_PROTOCOL_PREFIX: &[u8] = b"25a1";
pub const MAX_PAYLOAD_BYTES: usize = 64 * 1024;
pub const SUPPORTED_FORMATS: &[&str] = &["glyf", "colrv0", "colrv1"];
#[inline]
pub fn is_pua(cp: u32) -> bool {
(0xE000..=0xF8FF).contains(&cp) || (0xF_0000..=0xF_FFFD).contains(&cp) || (0x10_0000..=0x10_FFFD).contains(&cp) }
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GlyphCommand {
Support,
Query { cp: u32 },
Register {
cp: u32,
payload: GlyphPayload,
reply: ReplyMode,
},
Clear { cp: Option<u32> },
}
const MAX_COLR_GLYPHS: u16 = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GlyphPayload {
Glyf { glyf: Vec<u8>, upm: u16 },
ColrV0 { container: ColrContainer, upm: u16 },
ColrV1 { container: ColrContainer, upm: u16 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColrContainer {
pub glyphs: Vec<Vec<u8>>,
pub colr: Vec<u8>,
pub cpal: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ReplyMode {
None,
#[default]
All,
ErrorsOnly,
}
impl ReplyMode {
pub fn emit_success(self) -> bool {
matches!(self, ReplyMode::All)
}
pub fn emit_error(self) -> bool {
matches!(self, ReplyMode::All | ReplyMode::ErrorsOnly)
}
fn from_wire(raw: &[u8]) -> Self {
match raw {
b"0" => ReplyMode::None,
b"2" => ReplyMode::ErrorsOnly,
_ => ReplyMode::All,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryStatus {
Free,
System,
Glossary,
Both,
}
impl QueryStatus {
pub fn as_str(self) -> &'static str {
match self {
QueryStatus::Free => "",
QueryStatus::System => "system",
QueryStatus::Glossary => "glossary",
QueryStatus::Both => "system,glossary",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RegisterError {
OutOfNamespace,
CompositeUnsupported,
HintingUnsupported,
MalformedPayload,
PayloadTooLarge,
}
impl RegisterError {
fn as_str(self) -> &'static str {
match self {
RegisterError::OutOfNamespace => "out_of_namespace",
RegisterError::CompositeUnsupported => "composite_unsupported",
RegisterError::HintingUnsupported => "hinting_unsupported",
RegisterError::MalformedPayload => "malformed_payload",
RegisterError::PayloadTooLarge => "payload_too_large",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
NotGlyphProtocol,
Malformed(&'static str),
RegisterFailed {
cp: u32,
reason: RegisterError,
reply: ReplyMode,
},
ClearOutOfNamespace,
}
pub fn parse(body: &[u8]) -> Result<GlyphCommand, ParseError> {
if !body.starts_with(GLYPH_PROTOCOL_PREFIX) {
return Err(ParseError::NotGlyphProtocol);
}
let rest = &body[GLYPH_PROTOCOL_PREFIX.len()..];
let rest = rest
.strip_prefix(b";")
.ok_or(ParseError::Malformed("missing verb separator"))?;
let (verb, rest) = split_once(rest, b';');
let verb = trim(verb);
if verb.len() != 1 {
return Err(ParseError::Malformed("verb must be a single byte"));
}
match verb[0] {
b's' => parse_support(rest),
b'q' => parse_query(rest),
b'r' => parse_register(rest),
b'c' => parse_clear(rest),
_ => Err(ParseError::Malformed("unknown verb")),
}
}
fn parse_support(_rest: &[u8]) -> Result<GlyphCommand, ParseError> {
Ok(GlyphCommand::Support)
}
fn parse_query(rest: &[u8]) -> Result<GlyphCommand, ParseError> {
let params = parse_params(rest);
let cp_raw = params
.get("cp")
.ok_or(ParseError::Malformed("query missing cp"))?;
if cp_raw.contains(&b',') {
return Err(ParseError::Malformed("cp must be a single codepoint"));
}
let cp = parse_hex_cp(cp_raw).ok_or(ParseError::Malformed("query cp invalid hex"))?;
Ok(GlyphCommand::Query { cp })
}
fn parse_register(rest: &[u8]) -> Result<GlyphCommand, ParseError> {
let (control, payload_b64) = split_last(rest, b';');
let params = parse_params(control);
let cp_raw = params
.get("cp")
.ok_or(ParseError::Malformed("register missing cp"))?;
if cp_raw.contains(&b',') {
return Err(ParseError::Malformed("cp must be a single codepoint"));
}
let cp =
parse_hex_cp(cp_raw).ok_or(ParseError::Malformed("register cp invalid hex"))?;
let reply = params
.get("reply")
.map(|v| ReplyMode::from_wire(v))
.unwrap_or_default();
if !is_pua(cp) {
return Err(ParseError::RegisterFailed {
cp,
reason: RegisterError::OutOfNamespace,
reply,
});
}
let fmt = params.get("fmt").copied().unwrap_or(b"glyf");
if fmt != b"glyf" && fmt != b"colrv0" && fmt != b"colrv1" {
return Err(ParseError::Malformed("register fmt unknown"));
}
let upm = match params.get("upm") {
Some(raw) => {
parse_decimal_u16(raw).ok_or(ParseError::Malformed("register upm invalid"))?
}
None => 1000,
};
if upm == 0 {
return Err(ParseError::Malformed("register upm must be non-zero"));
}
let payload_b64 = trim(payload_b64);
let raw = BASE64
.decode(payload_b64)
.map_err(|_| ParseError::RegisterFailed {
cp,
reason: RegisterError::MalformedPayload,
reply,
})?;
if raw.len() > MAX_PAYLOAD_BYTES {
return Err(ParseError::RegisterFailed {
cp,
reason: RegisterError::PayloadTooLarge,
reply,
});
}
if raw.is_empty() {
return Err(ParseError::RegisterFailed {
cp,
reason: RegisterError::MalformedPayload,
reply,
});
}
let payload = match fmt {
b"glyf" => GlyphPayload::Glyf { glyf: raw, upm },
b"colrv0" => {
let container = parse_colr_container(&raw)
.map_err(|reason| ParseError::RegisterFailed { cp, reason, reply })?;
GlyphPayload::ColrV0 { container, upm }
}
b"colrv1" => {
let container = parse_colr_container(&raw)
.map_err(|reason| ParseError::RegisterFailed { cp, reason, reply })?;
GlyphPayload::ColrV1 { container, upm }
}
_ => unreachable!("fmt validated above"),
};
Ok(GlyphCommand::Register { cp, payload, reply })
}
fn parse_colr_container(data: &[u8]) -> Result<ColrContainer, RegisterError> {
let mut cur = Cursor::new(data);
let n_glyphs = cur.u16_be().ok_or(RegisterError::MalformedPayload)?;
if n_glyphs == 0 || n_glyphs > MAX_COLR_GLYPHS {
return Err(RegisterError::MalformedPayload);
}
let mut glyphs: Vec<Vec<u8>> = Vec::with_capacity(n_glyphs as usize);
for _ in 0..n_glyphs {
let glyf_len = cur.u16_be().ok_or(RegisterError::MalformedPayload)? as usize;
let glyf = cur
.slice(glyf_len)
.ok_or(RegisterError::MalformedPayload)?
.to_vec();
glyphs.push(glyf);
}
let colr_len = cur.u16_be().ok_or(RegisterError::MalformedPayload)? as usize;
if colr_len == 0 {
return Err(RegisterError::MalformedPayload);
}
let colr = cur
.slice(colr_len)
.ok_or(RegisterError::MalformedPayload)?
.to_vec();
let cpal_len = cur.u16_be().ok_or(RegisterError::MalformedPayload)? as usize;
let cpal = cur
.slice(cpal_len)
.ok_or(RegisterError::MalformedPayload)?
.to_vec();
if cur.remaining() != 0 {
return Err(RegisterError::MalformedPayload);
}
Ok(ColrContainer { glyphs, colr, cpal })
}
struct Cursor<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn remaining(&self) -> usize {
self.data.len().saturating_sub(self.pos)
}
fn u16_be(&mut self) -> Option<u16> {
if self.pos + 2 > self.data.len() {
return None;
}
let hi = self.data[self.pos] as u16;
let lo = self.data[self.pos + 1] as u16;
self.pos += 2;
Some((hi << 8) | lo)
}
fn slice(&mut self, n: usize) -> Option<&'a [u8]> {
if self.pos + n > self.data.len() {
return None;
}
let s = &self.data[self.pos..self.pos + n];
self.pos += n;
Some(s)
}
}
fn parse_clear(rest: &[u8]) -> Result<GlyphCommand, ParseError> {
let params = parse_params(rest);
match params.get("cp") {
Some(cp_raw) => {
if cp_raw.contains(&b',') {
return Err(ParseError::Malformed("cp must be a single codepoint"));
}
let cp = parse_hex_cp(cp_raw)
.ok_or(ParseError::Malformed("clear cp invalid hex"))?;
if !is_pua(cp) {
return Err(ParseError::ClearOutOfNamespace);
}
Ok(GlyphCommand::Clear { cp: Some(cp) })
}
None => Ok(GlyphCommand::Clear { cp: None }),
}
}
fn parse_params(data: &[u8]) -> Params<'_> {
let mut out = Params::default();
for part in data.split(|&b| b == b';') {
let part = trim(part);
if part.is_empty() {
continue;
}
if let Some(eq) = part.iter().position(|&b| b == b'=') {
let k = trim(&part[..eq]);
let v = trim(&part[eq + 1..]);
out.insert(k, v);
}
}
out
}
fn parse_hex_cp(raw: &[u8]) -> Option<u32> {
let raw = trim(raw);
if raw.is_empty() || raw.len() > 6 {
return None;
}
let mut out: u32 = 0;
for &b in raw {
let d = match b {
b'0'..=b'9' => b - b'0',
b'a'..=b'f' => b - b'a' + 10,
b'A'..=b'F' => b - b'A' + 10,
_ => return None,
} as u32;
out = (out << 4) | d;
}
if out > 0x10FFFF || (0xD800..=0xDFFF).contains(&out) {
return None;
}
Some(out)
}
fn parse_decimal_u16(raw: &[u8]) -> Option<u16> {
let raw = trim(raw);
if raw.is_empty() {
return None;
}
let mut out: u32 = 0;
for &b in raw {
if !b.is_ascii_digit() {
return None;
}
out = out.checked_mul(10)?.checked_add((b - b'0') as u32)?;
if out > u16::MAX as u32 {
return None;
}
}
Some(out as u16)
}
fn split_once(data: &[u8], sep: u8) -> (&[u8], &[u8]) {
if let Some(pos) = data.iter().position(|&b| b == sep) {
(&data[..pos], &data[pos + 1..])
} else {
(data, &[])
}
}
fn split_last(data: &[u8], sep: u8) -> (&[u8], &[u8]) {
if let Some(pos) = data.iter().rposition(|&b| b == sep) {
(&data[..pos], &data[pos + 1..])
} else {
(data, &[])
}
}
fn trim(data: &[u8]) -> &[u8] {
let mut start = 0;
let mut end = data.len();
while start < end && matches!(data[start], b' ' | b'\t' | b'\r' | b'\n') {
start += 1;
}
while end > start && matches!(data[end - 1], b' ' | b'\t' | b'\r' | b'\n') {
end -= 1;
}
&data[start..end]
}
#[derive(Default)]
struct Params<'a> {
entries: Vec<(&'a [u8], &'a [u8])>,
}
impl<'a> Params<'a> {
fn insert(&mut self, k: &'a [u8], v: &'a [u8]) {
for e in &mut self.entries {
if e.0 == k {
e.1 = v;
return;
}
}
self.entries.push((k, v));
}
fn get(&self, k: &str) -> Option<&&'a [u8]> {
self.entries
.iter()
.find(|e| e.0 == k.as_bytes())
.map(|e| &e.1)
}
}
pub(crate) fn format_support_response(fmts: &[&str]) -> String {
format!("\x1b_25a1;s;fmt={}\x1b\\", fmts.join(","))
}
pub fn format_query_response(cp: u32, status: QueryStatus) -> String {
format!("\x1b_25a1;q;cp={:x};status={}\x1b\\", cp, status.as_str())
}
pub(crate) fn format_register_ok(cp: u32) -> String {
format!("\x1b_25a1;r;cp={:x};status=0\x1b\\", cp)
}
pub(crate) fn format_register_error(cp: u32, reason: RegisterError) -> String {
format!(
"\x1b_25a1;r;cp={:x};status=1;reason={}\x1b\\",
cp,
reason.as_str()
)
}
pub(crate) fn format_clear_ok(cp: Option<u32>) -> String {
match cp {
Some(cp) => format!("\x1b_25a1;c;cp={:x};status=0\x1b\\", cp),
None => String::from("\x1b_25a1;c;status=0\x1b\\"),
}
}
pub(crate) fn format_clear_error_out_of_namespace() -> String {
String::from("\x1b_25a1;c;status=1;reason=out_of_namespace\x1b\\")
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
fn b64(data: &[u8]) -> String {
BASE64.encode(data)
}
#[test]
fn rejects_non_glyph_protocol_bodies() {
assert_eq!(parse(b"G,a=T;payload"), Err(ParseError::NotGlyphProtocol));
assert_eq!(parse(b""), Err(ParseError::NotGlyphProtocol));
}
#[test]
fn is_pua_covers_all_three_ranges() {
assert!(is_pua(0xE000));
assert!(is_pua(0xE0A0)); assert!(is_pua(0xF8FF)); assert!(is_pua(0xF_0000)); assert!(is_pua(0xF_FFFD));
assert!(is_pua(0x10_0000));
assert!(is_pua(0x10_FFFD));
}
#[test]
fn is_pua_excludes_real_text_and_emoji() {
assert!(!is_pua(0x0061)); assert!(!is_pua(0x002D)); assert!(!is_pua(0x1F600)); assert!(!is_pua(0xFFFE)); assert!(!is_pua(0xF_FFFE)); assert!(!is_pua(0x10_FFFF)); }
#[test]
fn parses_query_single_codepoint() {
let got = parse(b"25a1;q;cp=E0A0").unwrap();
assert_eq!(got, GlyphCommand::Query { cp: 0xE0A0 });
}
#[test]
fn query_accepts_non_pua_codepoints() {
let got = parse(b"25a1;q;cp=61").unwrap();
assert_eq!(got, GlyphCommand::Query { cp: 0x61 });
}
#[test]
fn query_rejects_sequence() {
assert!(matches!(
parse(b"25a1;q;cp=2D,3E"),
Err(ParseError::Malformed(_))
));
}
#[test]
fn query_rejects_surrogate() {
assert!(matches!(
parse(b"25a1;q;cp=D800"),
Err(ParseError::Malformed(_))
));
}
#[test]
fn parses_register_at_pua_codepoint() {
let payload = b64(&[0x01, 0x02, 0x03]);
let body = format!("25a1;r;cp=E0A0;upm=1000;{}", payload);
let got = parse(body.as_bytes()).unwrap();
assert_eq!(
got,
GlyphCommand::Register {
cp: 0xE0A0,
payload: GlyphPayload::Glyf {
glyf: vec![0x01, 0x02, 0x03],
upm: 1000,
},
reply: ReplyMode::All,
}
);
}
#[test]
fn parses_register_with_explicit_fmt() {
let payload = b64(&[0xAA]);
let body = format!("25a1;r;cp=E0A0;fmt=glyf;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()).unwrap(),
GlyphCommand::Register {
payload: GlyphPayload::Glyf { .. },
..
}
));
}
#[test]
fn register_defaults_upm_to_1000() {
let payload = b64(&[0x01]);
let body = format!("25a1;r;cp=E0A0;{}", payload);
let got = parse(body.as_bytes()).unwrap();
if let GlyphCommand::Register {
payload: GlyphPayload::Glyf { upm, .. },
..
} = got
{
assert_eq!(upm, 1000);
} else {
panic!("expected glyf register");
}
}
#[test]
fn register_rejects_non_pua_codepoint() {
let payload = b64(&[0x01]);
let body = format!("25a1;r;cp=61;upm=1000;{}", payload);
assert_eq!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
cp: 0x61,
reason: RegisterError::OutOfNamespace,
reply: ReplyMode::All,
})
);
}
#[test]
fn register_requires_cp() {
let payload = b64(&[0x01]);
let body = format!("25a1;r;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
}
#[test]
fn register_accepts_each_pua_range() {
for &cp_hex in &[0xE0A0u32, 0xF_0000, 0x10_0000] {
let payload = b64(b"x");
let body = format!("25a1;r;cp={:x};upm=1000;{}", cp_hex, payload);
assert!(matches!(
parse(body.as_bytes()).unwrap(),
GlyphCommand::Register { .. }
));
}
}
#[test]
fn register_rejects_unknown_fmt() {
let payload = b64(b"x");
let body = format!("25a1;r;cp=E0A0;fmt=svg;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
}
#[test]
fn register_rejects_bad_base64() {
let body = b"25a1;r;cp=E0A0;upm=1000;$$$$not_base64";
assert!(matches!(
parse(body),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn register_rejects_oversized_payload() {
let payload = b64(&vec![0u8; MAX_PAYLOAD_BYTES + 1]);
let body = format!("25a1;r;cp=E0A0;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::PayloadTooLarge,
..
})
));
}
#[test]
fn register_rejects_zero_upm() {
let payload = b64(b"x");
let body = format!("25a1;r;cp=E0A0;upm=0;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
}
#[test]
fn register_rejects_empty_payload() {
let body = b"25a1;r;cp=E0A0;upm=1000;";
assert!(matches!(
parse(body),
Err(ParseError::RegisterFailed {
cp: 0xE0A0,
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn register_rejects_missing_payload_separator() {
let body = b"25a1;r;cp=E0A0";
assert!(matches!(
parse(body),
Err(ParseError::RegisterFailed {
cp: 0xE0A0,
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn register_rejects_cp_above_unicode_max() {
let payload = b64(b"x");
let body = format!("25a1;r;cp=110000;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
let body = format!("25a1;r;cp=FFFFFF;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
}
#[test]
fn register_rejects_empty_cp_value() {
let payload = b64(b"x");
let body = format!("25a1;r;cp=;upm=1000;{}", payload);
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::Malformed(_))
));
}
#[test]
fn query_rejects_cp_above_unicode_max() {
let body = b"25a1;q;cp=110000";
assert!(matches!(parse(body), Err(ParseError::Malformed(_))));
}
#[test]
fn clear_single_pua_slot() {
let got = parse(b"25a1;c;cp=E0A0").unwrap();
assert_eq!(got, GlyphCommand::Clear { cp: Some(0xE0A0) });
}
#[test]
fn clear_rejects_non_pua_cp() {
assert_eq!(parse(b"25a1;c;cp=61"), Err(ParseError::ClearOutOfNamespace));
assert_eq!(
parse(b"25a1;c;cp=1F600"),
Err(ParseError::ClearOutOfNamespace)
);
}
#[test]
fn clear_rejects_sequence_cp() {
assert!(matches!(
parse(b"25a1;c;cp=E0A0,E0A1"),
Err(ParseError::Malformed(_))
));
}
#[test]
fn clear_all() {
let got = parse(b"25a1;c").unwrap();
assert_eq!(got, GlyphCommand::Clear { cp: None });
}
#[test]
fn parses_support_with_no_params() {
assert_eq!(parse(b"25a1;s").unwrap(), GlyphCommand::Support);
}
#[test]
fn support_ignores_unknown_params() {
assert_eq!(
parse(b"25a1;s;future=1;anything=else").unwrap(),
GlyphCommand::Support
);
}
#[test]
fn support_response_advertises_glyf_colrv0_colrv1() {
assert_eq!(
format_support_response(SUPPORTED_FORMATS),
"\x1b_25a1;s;fmt=glyf,colrv0,colrv1\x1b\\"
);
}
#[test]
fn support_response_encodes_subset_and_empty() {
assert_eq!(
format_support_response(&["glyf", "colrv0"]),
"\x1b_25a1;s;fmt=glyf,colrv0\x1b\\"
);
assert_eq!(format_support_response(&[]), "\x1b_25a1;s;fmt=\x1b\\");
}
#[test]
fn unknown_verb_is_malformed() {
assert!(matches!(
parse(b"25a1;z;cp=0061"),
Err(ParseError::Malformed(_))
));
}
fn build_container(glyphs: &[&[u8]], colr: &[u8], cpal: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(glyphs.len() as u16).to_be_bytes());
for g in glyphs {
out.extend_from_slice(&(g.len() as u16).to_be_bytes());
out.extend_from_slice(g);
}
out.extend_from_slice(&(colr.len() as u16).to_be_bytes());
out.extend_from_slice(colr);
out.extend_from_slice(&(cpal.len() as u16).to_be_bytes());
out.extend_from_slice(cpal);
out
}
#[test]
fn parses_colrv0_single_glyph() {
let container = build_container(&[&[0xAA, 0xBB]], &[0x01; 14], &[0x02; 12]);
let body = format!("25a1;r;cp=E0A0;fmt=colrv0;upm=1000;{}", b64(&container));
let got = parse(body.as_bytes()).unwrap();
match got {
GlyphCommand::Register {
cp: 0xE0A0,
payload:
GlyphPayload::ColrV0 {
container: c,
upm: 1000,
},
reply: ReplyMode::All,
} => {
assert_eq!(c.glyphs.len(), 1);
assert_eq!(c.glyphs[0], vec![0xAA, 0xBB]);
assert_eq!(c.colr.len(), 14);
assert_eq!(c.cpal.len(), 12);
}
other => panic!("expected colrv0 register, got {:?}", other),
}
}
#[test]
fn parses_colrv1_multi_glyph_with_empty_cpal() {
let container = build_container(
&[&[0x01], &[0x02, 0x03], &[0x04, 0x05, 0x06]],
&[0xF0; 32],
&[],
);
let body = format!("25a1;r;cp=100000;fmt=colrv1;upm=2048;{}", b64(&container));
let got = parse(body.as_bytes()).unwrap();
match got {
GlyphCommand::Register {
cp: 0x100000,
payload:
GlyphPayload::ColrV1 {
container: c,
upm: 2048,
},
reply: ReplyMode::All,
} => {
assert_eq!(c.glyphs.len(), 3);
assert_eq!(c.glyphs[2], vec![0x04, 0x05, 0x06]);
assert_eq!(c.colr.len(), 32);
assert!(c.cpal.is_empty());
}
other => panic!("expected colrv1 register, got {:?}", other),
}
}
#[test]
fn colr_rejects_zero_glyphs() {
let container = build_container(&[], &[0x00; 4], &[]);
let body = format!("25a1;r;cp=E0A0;fmt=colrv0;upm=1000;{}", b64(&container));
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn colr_rejects_empty_colr_table() {
let container = build_container(&[&[0x01]], &[], &[]);
let body = format!("25a1;r;cp=E0A0;fmt=colrv0;upm=1000;{}", b64(&container));
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn colr_rejects_truncated_payload() {
let mut bad = Vec::new();
bad.extend_from_slice(&2u16.to_be_bytes());
bad.extend_from_slice(&1u16.to_be_bytes());
bad.push(0xAA);
let body = format!("25a1;r;cp=E0A0;fmt=colrv1;upm=1000;{}", b64(&bad));
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn colr_rejects_trailing_garbage() {
let mut container = build_container(&[&[0x01]], &[0x00; 4], &[]);
container.push(0xFF);
let body = format!("25a1;r;cp=E0A0;fmt=colrv0;upm=1000;{}", b64(&container));
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn colr_rejects_excessive_glyph_count() {
let mut bad = Vec::new();
bad.extend_from_slice(&(MAX_COLR_GLYPHS + 1).to_be_bytes());
let body = format!("25a1;r;cp=E0A0;fmt=colrv0;upm=1000;{}", b64(&bad));
assert!(matches!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
reason: RegisterError::MalformedPayload,
..
})
));
}
#[test]
fn register_defaults_reply_to_all() {
let payload = b64(&[0x01]);
let body = format!("25a1;r;cp=E0A0;upm=1000;{}", payload);
match parse(body.as_bytes()).unwrap() {
GlyphCommand::Register { reply, .. } => {
assert_eq!(reply, ReplyMode::All);
}
other => panic!("expected register, got {:?}", other),
}
}
#[test]
fn register_accepts_every_reply_level() {
let payload = b64(&[0x01]);
for (raw, expected) in [
("0", ReplyMode::None),
("1", ReplyMode::All),
("2", ReplyMode::ErrorsOnly),
] {
let body = format!("25a1;r;cp=E0A0;reply={};upm=1000;{}", raw, payload);
match parse(body.as_bytes()).unwrap() {
GlyphCommand::Register { reply, .. } => {
assert_eq!(
reply, expected,
"reply={} should map to {:?}",
raw, expected
);
}
other => panic!("expected register, got {:?}", other),
}
}
}
#[test]
fn register_reply_propagates_on_parse_failure() {
let payload = b64(&[0x01]);
let body = format!("25a1;r;cp=61;reply=0;upm=1000;{}", payload);
assert_eq!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
cp: 0x61,
reason: RegisterError::OutOfNamespace,
reply: ReplyMode::None,
})
);
}
#[test]
fn register_reply_unknown_values_fall_back_to_all() {
let payload = b64(&[0x01]);
for bad in ["3", "true", "yes", "01", ""].iter() {
let body = format!("25a1;r;cp=E0A0;reply={};upm=1000;{}", bad, payload);
match parse(body.as_bytes()).unwrap() {
GlyphCommand::Register { reply, .. } => {
assert_eq!(
reply,
ReplyMode::All,
"reply={:?} should fall back to All",
bad
);
}
other => panic!("expected register, got {:?}", other),
}
}
}
#[test]
fn reply_mode_emit_matrix() {
assert!(ReplyMode::All.emit_success());
assert!(ReplyMode::All.emit_error());
assert!(!ReplyMode::ErrorsOnly.emit_success());
assert!(ReplyMode::ErrorsOnly.emit_error());
assert!(!ReplyMode::None.emit_success());
assert!(!ReplyMode::None.emit_error());
}
#[test]
fn colr_register_respects_pua_check_before_fmt_parse() {
let container = build_container(&[&[0x01]], &[0x00; 4], &[]);
let body = format!("25a1;r;cp=61;fmt=colrv0;upm=1000;{}", b64(&container));
assert_eq!(
parse(body.as_bytes()),
Err(ParseError::RegisterFailed {
cp: 0x61,
reason: RegisterError::OutOfNamespace,
reply: ReplyMode::All,
})
);
}
#[test]
fn query_response_encodes_coverage_names() {
assert_eq!(
format_query_response(0xE0A0, QueryStatus::Free),
"\x1b_25a1;q;cp=e0a0;status=\x1b\\"
);
assert_eq!(
format_query_response(0xE0A0, QueryStatus::System),
"\x1b_25a1;q;cp=e0a0;status=system\x1b\\"
);
assert_eq!(
format_query_response(0xE0A0, QueryStatus::Glossary),
"\x1b_25a1;q;cp=e0a0;status=glossary\x1b\\"
);
assert_eq!(
format_query_response(0xE0A0, QueryStatus::Both),
"\x1b_25a1;q;cp=e0a0;status=system,glossary\x1b\\"
);
}
#[test]
fn register_responses() {
assert_eq!(
format_register_ok(0xE0A0),
"\x1b_25a1;r;cp=e0a0;status=0\x1b\\"
);
assert_eq!(
format_register_error(0x61, RegisterError::OutOfNamespace),
"\x1b_25a1;r;cp=61;status=1;reason=out_of_namespace\x1b\\"
);
assert_eq!(
format_register_error(0xE0A0, RegisterError::CompositeUnsupported),
"\x1b_25a1;r;cp=e0a0;status=1;reason=composite_unsupported\x1b\\"
);
}
#[test]
fn clear_responses() {
assert_eq!(
format_clear_ok(Some(0xE0A0)),
"\x1b_25a1;c;cp=e0a0;status=0\x1b\\"
);
assert_eq!(format_clear_ok(None), "\x1b_25a1;c;status=0\x1b\\");
assert_eq!(
format_clear_error_out_of_namespace(),
"\x1b_25a1;c;status=1;reason=out_of_namespace\x1b\\"
);
}
#[test]
fn unknown_params_are_ignored() {
let got = parse(b"25a1;q;cp=E0A0;future=1").unwrap();
assert_eq!(got, GlyphCommand::Query { cp: 0xE0A0 });
}
}