aboutsummaryrefslogtreecommitdiffstats
path: root/src/osc5577.c
blob: dd2ec2675821f5f7e9a95d6fa1e2399174aebf46 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
#define _GNU_SOURCE
#include "osc5577.h"

#include <stdlib.h>
#include <string.h>

void osc_scan_reset(osc_scan_t *s) {
  s->state = OS_GROUND;
  s->len = 0;
  s->overflow = false;
}

/* Split "<version>;<verb>;<rest...>" and dispatch. The payload keeps every
 * separator after the verb, so `status;a;b;c` is the text "a;b;c". */
static void dispatch(const char *body, size_t len, osc5577_fn cb, void *ud) {
  if (len < 5 || memcmp(body, "5577;", 5) != 0) return;
  const char *p = body + 5;
  const char *end = body + len;

  const char *semi = memchr(p, ';', (size_t)(end - p));
  if (!semi) return;
  /* An unknown version is ignored *entirely*, so a future version cannot be
   * half-interpreted by an older build. */
  if (semi - p != 1 || *p != '1') return;
  p = semi + 1;

  semi = memchr(p, ';', (size_t)(end - p));
  size_t verb_len = semi ? (size_t)(semi - p) : (size_t)(end - p);
  char verb[32];
  if (verb_len >= sizeof verb) return;
  memcpy(verb, p, verb_len);
  verb[verb_len] = 0;

  const char *payload = semi ? semi + 1 : end;
  size_t plen = (size_t)(end - payload);
  char *buf = malloc(plen + 1);
  memcpy(buf, payload, plen);
  buf[plen] = 0;
  cb(verb, buf, ud);
  free(buf);
}

void osc_scan_feed(osc_scan_t *s, const uint8_t *data, size_t len,
                   osc5577_fn cb, void *ud) {
  for (size_t i = 0; i < len; i++) {
    uint8_t c = data[i];
    switch (s->state) {
    case OS_GROUND:
      if (c == 0x1b) s->state = OS_ESC;
      break;

    case OS_ESC:
      if (c == ']') {
        s->state = OS_BODY;
        s->len = 0;
        s->overflow = false;
      } else {
        s->state = c == 0x1b ? OS_ESC : OS_GROUND;
      }
      break;

    case OS_BODY:
      if (c == 0x07) { /* BEL terminator, as accepted for OSC 0/2 */
        if (!s->overflow) dispatch(s->buf, s->len, cb, ud);
        s->state = OS_GROUND;
      } else if (c == 0x1b) {
        s->state = OS_BODY_ESC;
      } else if (c < 0x20) {
        s->state = OS_GROUND; /* a control byte ends a malformed OSC */
      } else if (s->len < sizeof s->buf) {
        s->buf[s->len++] = (char)c;
      } else {
        s->overflow = true; /* keep scanning to the terminator, then drop */
      }
      break;

    case OS_BODY_ESC:
      if (c == '\\') { /* ST */
        if (!s->overflow) dispatch(s->buf, s->len, cb, ud);
        s->state = OS_GROUND;
      } else if (c == ']') {
        s->state = OS_BODY; /* a new OSC started inside one: restart */
        s->len = 0;
        s->overflow = false;
      } else {
        s->state = OS_GROUND;
      }
      break;
    }
  }
}

static int hexval(char c) {
  if (c >= '0' && c <= '9') return c - '0';
  if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  return -1;
}

size_t osc5577_unescape(const char *in, size_t in_len, char *out, size_t cap) {
  size_t n = 0;
  for (size_t i = 0; i < in_len && n + 1 < cap; i++) {
    if (in[i] == '%' && i + 2 < in_len) {
      int hi = hexval(in[i + 1]), lo = hexval(in[i + 2]);
      if (hi >= 0 && lo >= 0) {
        out[n++] = (char)(hi * 16 + lo);
        i += 2;
        continue;
      }
      /* An invalid escape is left literal rather than dropping text. */
    }
    out[n++] = in[i];
  }
  out[n] = 0;
  return n;
}

bool osc5577_valid_id(const char *id) {
  size_t n = strlen(id);
  if (n == 0 || n > 32) return false;
  for (size_t i = 0; i < n; i++) {
    char c = id[i];
    bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
              (c >= '0' && c <= '9') || c == '_' || c == '-';
    if (!ok) return false;
  }
  return true;
}