Form IDs were baked into .form files at conversion time, preventing a form from being displayed more than once. dfm2form now writes a placeholder ID (0), and formServerSendForm streams the file directly from disk, assigning a unique ID on the fly via rewriteFormId. This eliminates formServerLoadFile, the in-memory form store, and the -i flag from dfm2form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
317 lines
7.9 KiB
C
317 lines
7.9 KiB
C
// formsrv.c - Remote forms server library implementation
|
|
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "formsrv.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Constants
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define MAX_MSG_LEN 4096
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct FormServerS {
|
|
FormTransportT *transport;
|
|
EventCallbackT eventCallback;
|
|
void *eventUserData;
|
|
int32_t nextFormId;
|
|
char msgBuf[MAX_MSG_LEN];
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Prototypes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool parseToken(const char **p, char *buf, int32_t bufSize);
|
|
static void rewriteFormId(char *line, int32_t lineSize, int32_t formId);
|
|
static void sendCommand(FormServerT *server, const char *fmt, ...);
|
|
static bool skipSpaces(const char **p);
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Internal helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static bool parseToken(const char **p, char *buf, int32_t bufSize)
|
|
{
|
|
skipSpaces(p);
|
|
if (**p == '\0') {
|
|
return false;
|
|
}
|
|
|
|
int32_t i = 0;
|
|
|
|
if (**p == '"') {
|
|
// Quoted string — read until closing quote
|
|
(*p)++;
|
|
while (**p != '\0' && **p != '"') {
|
|
if (**p == '\\' && *(*p + 1) != '\0') {
|
|
(*p)++;
|
|
char c = **p;
|
|
switch (c) {
|
|
case 'n': c = '\n'; break;
|
|
case 'r': c = '\r'; break;
|
|
case 't': c = '\t'; break;
|
|
case '"': c = '"'; break;
|
|
case '\\': c = '\\'; break;
|
|
default: break;
|
|
}
|
|
if (i < bufSize - 1) {
|
|
buf[i++] = c;
|
|
}
|
|
} else {
|
|
if (i < bufSize - 1) {
|
|
buf[i++] = **p;
|
|
}
|
|
}
|
|
(*p)++;
|
|
}
|
|
if (**p == '"') {
|
|
(*p)++;
|
|
}
|
|
} else {
|
|
// Bare token — read until whitespace
|
|
while (**p != '\0' && **p != ' ' && **p != '\t') {
|
|
if (i < bufSize - 1) {
|
|
buf[i++] = **p;
|
|
}
|
|
(*p)++;
|
|
}
|
|
}
|
|
|
|
buf[i] = '\0';
|
|
return i > 0;
|
|
}
|
|
|
|
|
|
static void rewriteFormId(char *line, int32_t lineSize, int32_t formId)
|
|
{
|
|
// Protocol lines have the form: COMMAND <formId> <rest...>
|
|
// Skip command prefix (non-space chars)
|
|
char *p = line;
|
|
while (*p && *p != ' ') {
|
|
p++;
|
|
}
|
|
// Skip spaces
|
|
while (*p == ' ') {
|
|
p++;
|
|
}
|
|
|
|
// p now points at the placeholder ID token ("0")
|
|
// Find the end of this token
|
|
char *idStart = p;
|
|
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') {
|
|
p++;
|
|
}
|
|
char *idEnd = p;
|
|
|
|
// Format the new ID
|
|
char idBuf[16];
|
|
int32_t idLen = snprintf(idBuf, sizeof(idBuf), "%d", formId);
|
|
|
|
int32_t oldLen = (int32_t)(idEnd - idStart);
|
|
int32_t shift = idLen - oldLen;
|
|
|
|
// Check that the rewritten line still fits
|
|
int32_t curLen = (int32_t)strlen(line);
|
|
if (curLen + shift >= lineSize - 1) {
|
|
return;
|
|
}
|
|
|
|
// Shift the remainder of the line
|
|
if (shift != 0) {
|
|
memmove(idStart + idLen, idEnd, strlen(idEnd) + 1);
|
|
}
|
|
|
|
// Write the new ID (no NUL — the memmove already placed the rest)
|
|
memcpy(idStart, idBuf, idLen);
|
|
}
|
|
|
|
|
|
static void sendCommand(FormServerT *server, const char *fmt, ...)
|
|
{
|
|
char buf[MAX_MSG_LEN];
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
va_end(args);
|
|
|
|
server->transport->writeMessage(buf, server->transport->ctx);
|
|
}
|
|
|
|
|
|
static bool skipSpaces(const char **p)
|
|
{
|
|
while (**p == ' ' || **p == '\t') {
|
|
(*p)++;
|
|
}
|
|
return **p != '\0';
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void formServerBindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
|
|
const char *eventName)
|
|
{
|
|
sendCommand(server, "EVENT.BIND %d %d %s", formId, ctrlId, eventName);
|
|
}
|
|
|
|
|
|
FormServerT *formServerCreate(FormTransportT *transport)
|
|
{
|
|
FormServerT *server = (FormServerT *)calloc(1, sizeof(FormServerT));
|
|
if (server == NULL) {
|
|
return NULL;
|
|
}
|
|
server->transport = transport;
|
|
server->nextFormId = 1;
|
|
return server;
|
|
}
|
|
|
|
|
|
void formServerDestroy(FormServerT *server)
|
|
{
|
|
if (server == NULL) {
|
|
return;
|
|
}
|
|
free(server);
|
|
}
|
|
|
|
|
|
void formServerDestroyForm(FormServerT *server, int32_t formId)
|
|
{
|
|
sendCommand(server, "FORM.DESTROY %d", formId);
|
|
}
|
|
|
|
|
|
void formServerHideForm(FormServerT *server, int32_t formId)
|
|
{
|
|
sendCommand(server, "FORM.HIDE %d", formId);
|
|
}
|
|
|
|
|
|
bool formServerPollEvent(FormServerT *server)
|
|
{
|
|
int bytesRead = server->transport->readMessage(
|
|
server->msgBuf, MAX_MSG_LEN - 1, server->transport->ctx);
|
|
|
|
if (bytesRead <= 0) {
|
|
return false;
|
|
}
|
|
|
|
server->msgBuf[bytesRead] = '\0';
|
|
|
|
// Parse: EVENT <formId> <ctrlId> <eventName> [<data>]
|
|
const char *p = server->msgBuf;
|
|
char token[256];
|
|
|
|
if (!parseToken(&p, token, sizeof(token))) {
|
|
return false;
|
|
}
|
|
if (strcmp(token, "EVENT") != 0) {
|
|
return false;
|
|
}
|
|
|
|
// formId
|
|
if (!parseToken(&p, token, sizeof(token))) {
|
|
return false;
|
|
}
|
|
int32_t formId = (int32_t)atoi(token);
|
|
|
|
// ctrlId
|
|
if (!parseToken(&p, token, sizeof(token))) {
|
|
return false;
|
|
}
|
|
int32_t ctrlId = (int32_t)atoi(token);
|
|
|
|
// eventName
|
|
char eventName[64];
|
|
if (!parseToken(&p, eventName, sizeof(eventName))) {
|
|
return false;
|
|
}
|
|
|
|
// Remaining data (rest of line after skipping spaces)
|
|
skipSpaces(&p);
|
|
const char *data = p;
|
|
|
|
if (server->eventCallback != NULL) {
|
|
server->eventCallback(formId, ctrlId, eventName, data,
|
|
server->eventUserData);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
int32_t formServerSendForm(FormServerT *server, const char *path)
|
|
{
|
|
FILE *f = fopen(path, "r");
|
|
if (f == NULL) {
|
|
fprintf(stderr, "formsrv: cannot open '%s': %s\n", path, strerror(errno));
|
|
return -1;
|
|
}
|
|
|
|
int32_t formId = server->nextFormId++;
|
|
char lineBuf[MAX_MSG_LEN];
|
|
|
|
while (fgets(lineBuf, sizeof(lineBuf), f) != NULL) {
|
|
// Strip trailing newline
|
|
int32_t len = (int32_t)strlen(lineBuf);
|
|
while (len > 0 && (lineBuf[len - 1] == '\n' || lineBuf[len - 1] == '\r')) {
|
|
lineBuf[--len] = '\0';
|
|
}
|
|
|
|
// Skip empty lines
|
|
if (len == 0) {
|
|
continue;
|
|
}
|
|
|
|
rewriteFormId(lineBuf, sizeof(lineBuf), formId);
|
|
server->transport->writeMessage(lineBuf, server->transport->ctx);
|
|
}
|
|
|
|
fclose(f);
|
|
return formId;
|
|
}
|
|
|
|
|
|
void formServerSetEventCallback(FormServerT *server, EventCallbackT cb,
|
|
void *userData)
|
|
{
|
|
server->eventCallback = cb;
|
|
server->eventUserData = userData;
|
|
}
|
|
|
|
|
|
void formServerSetProp(FormServerT *server, int32_t formId, int32_t ctrlId,
|
|
const char *prop, const char *value)
|
|
{
|
|
sendCommand(server, "CTRL.SET %d %d %s=%s", formId, ctrlId, prop, value);
|
|
}
|
|
|
|
|
|
void formServerShowForm(FormServerT *server, int32_t formId)
|
|
{
|
|
sendCommand(server, "FORM.SHOW %d", formId);
|
|
}
|
|
|
|
|
|
void formServerUnbindEvent(FormServerT *server, int32_t formId, int32_t ctrlId,
|
|
const char *eventName)
|
|
{
|
|
sendCommand(server, "EVENT.UNBIND %d %d %s", formId, ctrlId, eventName);
|
|
}
|