1use anyhow::{bail, ensure, Context, Result};
6use base64::Engine;
7use tokio::io::{AsyncRead, AsyncReadExt};
8
9pub const MAX_INSTRUCTION: usize = 8192;
11pub const MAX_ELEMENTS: usize = 128;
13pub const VERSION: &str = "VERSION_1_3_0";
15pub const BLOB_CHUNK: usize = 6048;
17pub const MAX_CLIPBOARD: usize = 64 * 1024;
19pub const PORT: u16 = 4822;
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct Instruction {
23 pub opcode: String,
24 pub args: Vec<String>,
25}
26
27impl Instruction {
28 pub fn new(opcode: &str, args: &[&str]) -> Self {
29 Self {
30 opcode: opcode.into(),
31 args: args.iter().map(|s| s.to_string()).collect(),
32 }
33 }
34 pub fn arg(&self, i: usize) -> &str {
35 self.args.get(i).map(String::as_str).unwrap_or("")
36 }
37}
38
39fn element(out: &mut String, v: &str) {
40 out.push_str(&v.chars().count().to_string());
41 out.push('.');
42 out.push_str(v);
43}
44
45pub fn encode(opcode: &str, args: &[&str]) -> String {
46 let mut out = String::new();
47 element(&mut out, opcode);
48 for a in args {
49 out.push(',');
50 element(&mut out, a);
51 }
52 out.push(';');
53 out
54}
55
56pub fn encode_ins(i: &Instruction) -> String {
57 let args: Vec<&str> = i.args.iter().map(String::as_str).collect();
58 encode(&i.opcode, &args)
59}
60
61pub struct Reader<R> {
63 inner: R,
64 buf: Vec<u8>,
65}
66
67impl<R: AsyncRead + Unpin> Reader<R> {
68 pub fn new(inner: R) -> Self {
69 Self {
70 inner,
71 buf: Vec::new(),
72 }
73 }
74
75 pub async fn next(&mut self) -> Result<Option<Instruction>> {
77 loop {
78 if let Some((ins, used)) = parse(&self.buf)? {
79 self.buf.drain(..used);
80 return Ok(Some(ins));
81 }
82 ensure!(
83 self.buf.len() <= MAX_INSTRUCTION,
84 "instruction longer than {MAX_INSTRUCTION} bytes"
85 );
86 let mut chunk = [0u8; 4096];
87 let n = self.inner.read(&mut chunk).await?;
88 if n == 0 {
89 ensure!(self.buf.is_empty(), "connection closed mid-instruction");
90 return Ok(None);
91 }
92 self.buf.extend_from_slice(&chunk[..n]);
93 }
94 }
95}
96
97pub fn parse(b: &[u8]) -> Result<Option<(Instruction, usize)>> {
100 let mut pos = 0;
101 let mut elements: Vec<String> = Vec::new();
102 loop {
103 let dot = match b[pos..].iter().take(12).position(|&c| c == b'.') {
105 Some(d) => d,
106 None if b.len() - pos < 12 => return Ok(None),
107 None => bail!("element length is not a number followed by '.'"),
108 };
109 let len_text = std::str::from_utf8(&b[pos..pos + dot])?;
110 ensure!(
111 !len_text.is_empty() && len_text.bytes().all(|c| c.is_ascii_digit()),
112 "element length {len_text:?} is not a number"
113 );
114 let chars: usize = len_text.parse()?;
115 ensure!(chars <= MAX_INSTRUCTION, "element of {chars} characters");
116 pos += dot + 1;
117 let start = pos;
119 let mut seen = 0;
120 while seen < chars {
121 let Some(&lead) = b.get(pos) else {
122 return Ok(None);
123 };
124 let width = match lead {
125 0x00..=0x7f => 1,
126 0xc0..=0xdf => 2,
127 0xe0..=0xef => 3,
128 0xf0..=0xf7 => 4,
129 _ => bail!("invalid UTF-8 in an element"),
130 };
131 pos += width;
132 seen += 1;
133 }
134 if pos >= b.len() {
135 return Ok(None);
136 }
137 let value = std::str::from_utf8(&b[start..pos]).context("element is not UTF-8")?;
138 elements.push(value.to_string());
139 ensure!(
140 elements.len() <= MAX_ELEMENTS,
141 "more than {MAX_ELEMENTS} elements"
142 );
143 match b[pos] {
144 b',' => pos += 1,
145 b';' => {
146 pos += 1;
147 let mut it = elements.into_iter();
148 let opcode = it.next().unwrap_or_default();
149 return Ok(Some((
150 Instruction {
151 opcode,
152 args: it.collect(),
153 },
154 pos,
155 )));
156 }
157 c => bail!("expected ',' or ';' after an element, got {:?}", c as char),
158 }
159 }
160}
161
162const NAMED: &[(&str, u32)] = &[
166 ("Return", 0xff0d),
167 ("Enter", 0xff0d),
168 ("BackSpace", 0xff08),
169 ("Tab", 0xff09),
170 ("Escape", 0xff1b),
171 ("Delete", 0xffff),
172 ("Home", 0xff50),
173 ("Left", 0xff51),
174 ("Up", 0xff52),
175 ("Right", 0xff53),
176 ("Down", 0xff54),
177 ("Page_Up", 0xff55),
178 ("Page_Down", 0xff56),
179 ("End", 0xff57),
180 ("Insert", 0xff63),
181 ("Shift_L", 0xffe1),
182 ("Control_L", 0xffe3),
183 ("Alt_L", 0xffe9),
184 ("Super_L", 0xffeb),
185 ("space", 0x20),
186];
187
188pub fn keysym_of_char(c: char) -> u32 {
190 match c {
191 '\n' | '\r' => 0xff0d,
192 '\t' => 0xff09,
193 c if (c as u32) < 0x100 => c as u32,
194 c => 0x0100_0000 + c as u32,
195 }
196}
197
198pub fn keysym_of_name(name: &str) -> Result<u32> {
200 if let Some((_, k)) = NAMED.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)) {
201 return Ok(*k);
202 }
203 if let Some(n) = name.strip_prefix('F').and_then(|n| n.parse::<u32>().ok()) {
204 ensure!((1..=35).contains(&n), "function keys are F1-F35");
205 return Ok(0xffbe + n - 1);
206 }
207 let mut chars = name.chars();
208 if let (Some(c), None) = (chars.next(), chars.clone().next()) {
209 return Ok(keysym_of_char(c));
210 }
211 if let Some(hex) = name.strip_prefix("0x") {
212 return u32::from_str_radix(hex, 16).context("keysym is not hex");
213 }
214 bail!("unknown key {name:?}: use Return, Escape, Tab, BackSpace, an arrow, F1-F35, a character or 0x<keysym>")
215}
216
217pub fn char_of_keysym(k: u32) -> Option<char> {
219 match k {
220 0x20..=0x7e | 0xa0..=0xff => char::from_u32(k),
221 0x0100_0100..=0x0110_ffff => char::from_u32(k - 0x0100_0000),
222 _ => None,
223 }
224}
225
226pub fn name_of_keysym(k: u32) -> String {
227 if let Some((n, _)) = NAMED.iter().find(|(_, v)| *v == k) {
228 return n.to_string();
229 }
230 if (0xffbe..0xffbe + 35).contains(&k) {
231 return format!("F{}", k - 0xffbe + 1);
232 }
233 match char_of_keysym(k) {
234 Some(c) => c.to_string(),
235 None => format!("0x{k:x}"),
236 }
237}
238
239pub fn color(s: &str) -> Result<[u8; 3]> {
244 let h = s.trim_start_matches('#');
245 ensure!(h.len() == 6, "colour must be #rrggbb");
246 let v = u32::from_str_radix(h, 16).context("colour must be #rrggbb")?;
247 Ok([(v >> 16) as u8, (v >> 8) as u8, v as u8])
248}
249
250pub const MAX_TEXT_SIDE: u32 = 4096;
252
253pub fn render_text(
255 text: &str,
256 fg: [u8; 3],
257 bg: Option<[u8; 3]>,
258 scale: u32,
259) -> Result<(Vec<u8>, u32, u32)> {
260 use font8x8::UnicodeFonts;
261 let lines: Vec<&str> = text.split('\n').collect();
262 let cols = lines
263 .iter()
264 .map(|l| l.chars().count())
265 .max()
266 .unwrap_or(0)
267 .max(1) as u32;
268 let w = cols * 8 * scale;
269 let h = lines.len() as u32 * 8 * scale;
270 ensure!(
271 w <= MAX_TEXT_SIDE && h <= MAX_TEXT_SIDE,
272 "text renders larger than {MAX_TEXT_SIDE} pixels; shorten it or lower scale"
273 );
274 let mut img = image::RgbaImage::from_pixel(
275 w,
276 h,
277 match bg {
278 Some([r, g, b]) => image::Rgba([r, g, b, 255]),
279 None => image::Rgba([0, 0, 0, 0]),
280 },
281 );
282 for (row, line) in lines.iter().enumerate() {
283 for (col, c) in line.chars().enumerate() {
284 let glyph = font8x8::BASIC_FONTS
285 .get(c)
286 .or_else(|| font8x8::LATIN_FONTS.get(c))
287 .or_else(|| font8x8::BASIC_FONTS.get('?'))
288 .unwrap_or([0; 8]);
289 for (gy, bits) in glyph.iter().enumerate() {
290 for gx in 0..8 {
291 if bits & (1 << gx) == 0 {
292 continue;
293 }
294 for sy in 0..scale {
295 for sx in 0..scale {
296 let x = (col as u32 * 8 + gx) * scale + sx;
297 let y = (row as u32 * 8 + gy as u32) * scale + sy;
298 img.put_pixel(x, y, image::Rgba([fg[0], fg[1], fg[2], 255]));
299 }
300 }
301 }
302 }
303 }
304 }
305 let mut png = Vec::new();
306 img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)?;
307 Ok((png, w, h))
308}
309
310pub fn blobs(stream: &str, data: &[u8]) -> String {
312 let b64 = base64::engine::general_purpose::STANDARD.encode(data);
313 let mut out = String::new();
314 for chunk in b64.as_bytes().chunks(BLOB_CHUNK) {
315 out.push_str(&encode(
316 "blob",
317 &[stream, std::str::from_utf8(chunk).unwrap_or("")],
318 ));
319 }
320 out
321}
322
323pub fn unbase64(s: &str) -> Result<Vec<u8>> {
324 base64::engine::general_purpose::STANDARD
325 .decode(s)
326 .context("blob is not base64")
327}
328
329pub const STATUS_SERVER_ERROR: u32 = 0x0200;
331pub const STATUS_CLIENT_UNAUTHORIZED: u32 = 0x0301;
332pub const STATUS_CLIENT_FORBIDDEN: u32 = 0x0303;